提供maxsplit时string.split对短字符串的行为

时间:2015-05-07 01:49:47

标签: python string python-2.7 split

我最近遇到了python2.7中string.split方法的一些有趣行为,特别是关于短蜇(少于约25个字符,见下文),这涉及对比行为:

# Without maxplsit
my_string.split('A')

# With maxsplit=1
my_string.split('A', 1)

对于短字符串,第二种方法实际上较慢,我很好奇为什么。

测试

这首先来自于我的同事发现的一个小时间调用:

# Without maxsplit
$ python -m timeit -s "json_line='a|b|c'" "part_one='|'.split(json_line)[0]"
1000000 loops, best of 3: 0.274 usec per loop
# With maxsplit
$ python -m timeit -s "json_line='a|b|c'" "part_one='|'.split(json_line,1)[0]"
1000000 loops, best of 3: 0.461 usec per loop

我认为这当然很好奇,所以我整理了一个更详细的测试。首先,我编写了以下小函数,该函数生成由前十个大写字母组成的指定长度的随机字符串:

from random import choice

# 'A' through 'J'
choices = map(chr, range(65, 75))

def make_random_string(length):
    return ''.join(choice(choices) for i in xrange(length))

然后我写了几个测试器函数来重复分割和计算指定长度的随机生成的字符串:

from timeit import timeit

def time_split_of_size(str_length, n_strs_to_split):
    times = []
    data = [make_random_string(str_length) for i in xrange(n_strs_to_split)]
    for s in data:
        t = timeit("'{s}'.split('A')".format(s=s),
                   setup="from __main__ import make_random_string",
                   number=1000)
        times.append(t)
    return times

def time_split_of_size_with_maxcount(str_length, n_strs_to_split):
    times = []
    data = [make_random_string(str_length) for i in xrange(n_strs_to_split)]
    for s in data:
        t = timeit("'{s}'.split('A', 1)".format(s=s),
                   setup="from __main__ import make_random_string",
                   number=1000)
        times.append(t)
    return times

然后我在不同大小的字符串上运行这些测试方法:

from collections import OrderedDict
d = OrderedDict({})
for str_length in xrange(10, 10*1000, 25):
    no_maxcount = mean(time_split_of_size(str_length, 20))
    with_maxcount = mean(time_split_of_size_with_maxcount(str_length, 20))
    d[str_length] = [no_maxcount, with_maxcount]

这为您提供了您期望的行为,对于maxsplit = 1和O(n)的方法,O(1)一直分割。以下是字符串长度的时间图,几乎看不见的绿色曲线是maxsplit=1而蓝色曲线没有:

StringSplitTiming

尽管如此,我的同事为小叮咬发现的行为是真实的。这里有一些代码可以解释很多短暂的分裂:

from collections import OrderedDict
d = OrderedDict({})
for str_length in xrange(1, 50, 2):
    no_maxcount = mean(time_split_of_size(str_length, 500))
    with_maxcount = mean(time_split_of_size_with_maxcount(str_length, 500))
    d[str_length] = [no_maxcount, with_maxcount]

得到以下结果:

StringSplitShortString

对于长度小于25个字符的字符串,似乎有一些开销。绿色曲线的形状也很奇怪,它是如何在平整之前平行增加的。

我看了一下源代码,你可以在这里找到:

stringobject.c(第1449行) stringlib/split.h(第105行)

但没有任何明显的事情向我跳出来。

知道在为短字符串传递maxsplit时导致开销的原因是什么?

2 个答案:

答案 0 :(得分:4)

差异实际上与string_split内部的内容无关。实际上,即使没有要进行拆分,默认拆分时间内该功能的时间总是比maxsplit=1略长。并且它不是PyArg_ParseTuple的差异(如果没有检测解释器,我能得到的最好的报告说无论哪种方式都需要0ns,所以无论有什么不同,它都不重要)。< / p>

不同之处在于需要额外的字节码来传递额外的参数。

正如Stefan Pochmann建议的那样,您可以通过使用明确的maxsplit=-1进行测试来说明这一点:

In [212]: %timeit ''.split('|')
1000000 loops, best of 3: 267 ns per loop
In [213]: %timeit ''.split('|', 1)
1000000 loops, best of 3: 303 ns per loop
In [214]: %timeit ''.split('|', -1)
1000000 loops, best of 3: 307 ns per loop

因此,即使在这个最小的示例中,-1也比1略慢。但我们正在谈论4ns的额外工作。 (我很确定这4ns是因为preallocating a list of size 12 instead of size 2,但我不想通过探查器来确保。)

与此同时,一个NOP字节码需要32ns才能在我的系统上进行评估(从另一个答案我仍然试图找到......)。我无法想象LOAD_CONSTNOP更快。

所以,直到你做足够的工作来压倒32ns +,不通过maxsplit参数将节省你的时间。

如果不是很明显,这里有两种情况的反汇编:

  1           0 LOAD_CONST               0 ('')
              3 LOAD_ATTR                0 (split)
              6 LOAD_CONST               1 ('|')
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 RETURN_VALUE

  1           0 LOAD_CONST               0 ('')
              3 LOAD_ATTR                0 (split)
              6 LOAD_CONST               1 ('|')
              9 LOAD_CONST               3 (-1)
             12 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             15 RETURN_VALUE

对于类似的例子:

In [217]: %timeit int()
10000000 loops, best of 3: 94 ns per loop
In [218]: %timeit int(0)
10000000 loops, best of 3: 134 ns per loop
In [235]: %timeit [0].pop()
1000000 loops, best of 3: 229 ns per loop
In [236]: %timeit [0].pop(0)
1000000 loops, best of 3: 270 ns per loop

因此LOAD_CONST在这两种情况下大约需要40ns,就像传递-1而不是split的参数一样。

Python 3.4有点难以测试,因为它会缓存一些2.7不具备的东西,但看起来传递一个额外的参数大概是33ns - 如果它是#533ns关键字参数。因此,如果您需要在Python 3中将小字符串拆分十亿次,请使用s.split('|', 10),而不是s.split('|', maxsplit=10)

答案 1 :(得分:0)

正确的初始测试(原始测试有json_line and'|'混合)

python -m timeit -s "json_line='a|b|c'" "part_one=json_line.split('|')[0]"
1000000 loops, best of 3: 0.239 usec per loop
python -m timeit -s "json_line='a|b|c'" "part_one=json_line.split('|',1)[0]"
1000000 loops, best of 3: 0.267 usec per loop

时差较小。