Python string 'join' is faster (?) than '+', but what's wrong here?
我在之前的一篇文章中询问了质量动态字符串连接的最有效方法,并建议我使用连接方法,这是最好、最简单和最快的方法(正如大家所说)。但当我玩字符串连接时,我发现了一些奇怪的(?)结果。我肯定发生了什么事,但我不太明白。以下是我所做的:
我定义了这些功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import timeit def x(): s=[] for i in range(100): # Other codes here... s.append("abcdefg"[i%7]) return ''.join(s) def y(): s='' for i in range(100): # Other codes here... s+="abcdefg"[i%7] return s def z(): s='' for i in range(100): # Other codes here... s=s+"abcdefg"[i%7] return s def p(): s=[] for i in range(100): # Other codes here... s+="abcdefg"[i%7] return ''.join(s) def q(): s=[] for i in range(100): # Other codes here... s = s + ["abcdefg"[i%7]] return ''.join(s) |
我尝试在整个函数中保持其他内容(串联除外)几乎相同。然后,我用下面的注释结果进行了测试(在Windows32位计算机上使用python 3.1.1 idle):
1 2 3 4 5 | timeit.timeit(x) # 31.54912480500002 timeit.timeit(y) # 23.533029429999942 timeit.timeit(z) # 22.116181330000018 timeit.timeit(p) # 37.718607439999914 timeit.timeit(q) # 108.60377576499991 |
号
这意味着strng=strng+dyn-strng是最快的。虽然时间上的差异没有那么大(除了最后一个),但我想知道为什么会发生这种情况。这是因为我使用的是python 3.1.1,它提供了"+"作为最有效的方法吗?我应该使用"+"作为加入的替代方法吗?或者,我做了一些非常愚蠢的事情吗?或者什么?请解释清楚。
我们中的一些python提交者,我认为主要是rigo和hettinger,他们(在去2.5的路上,我相信)特意优化了一些非常常见的adocx1(0)枯萎病的特殊情况,他们认为已经证明,初学者永远不会被掩盖,
我相信这条线索证明我们应该更严厉地反对他们。现在,他们优化了
啊,好吧——我放弃了。操作,@mshsayem,继续,到处使用+=功能,在琐碎的、微小的、不相关的情况下享受你不相关的20%加速,你最好尽情享受它们——因为有一天,当你看不到它的到来时,在一个重要的、大型的操作中,你会被迎面而来的200%减速的拖车撞到(除非你不走运D是2000%的1;-)。记住:如果你觉得"Python的速度太慢了",记住,它很可能是你最喜欢的
对于我们中的其他人——那些明白这意味着什么的人来说,我们应该忘记小效率,比如说97%的时间,我会一直衷心地推荐
所以,对于我们其他人来说,这里有一套更有意义和有趣的测量方法:
1 2 | $ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 's="".join(r)' 1000 loops, best of 3: 319 usec per loop |
900串297个字符的每个,直接加入名单当然是最快的,但运营商害怕在此之前必须做附加。但是:
1 2 3 4 | $ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 's=""' 'for x in r: s+=x' 1000 loops, best of 3: 779 usec per loop $ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 'z=[]' 'for x in r: z.append(x)' '"".join(z)' 1000 loops, best of 3: 538 usec per loop |
号
…有了一个非常重要的数据量(非常少的100千字节——以每一种方式都需要一毫秒的可测量分数),即使是普通的好的老
1 2 | $ python -mtimeit -s'r=[str(x)*99 for x in xrange(100,1000)]' 'z=[]; zap=z.append' 'for x in r: zap(x)' '"".join(z)' 1000 loops, best of 3: 438 usec per loop |
在平均循环时间内再刮十分之一毫秒。每个人(至少每个完全沉迷于大量性能的人)显然都知道提升(从内部循环中去掉一个重复的计算,否则会反复执行)是优化中的一个关键技术——python不代表您提升,因此在那些罕见的情况下,当每一微秒都很重要。
至于为什么
1 | l +="a" |
你在把字符串
1 | l = l + ["a"] |
。
您正在创建一个包含
我假设x()较慢,因为您首先构建数组,然后加入它。因此,您不仅要测量连接所需的时间,还要测量构建数组所需的时间。
在已经有一个数组并且希望从其元素中创建一个字符串的场景中,join应该比遍历数组并逐步构建字符串更快。
这个问题实际上是关于东西的价格。我们将在这里进行一些快速和宽松的游戏,在类似的情况下减去结果。您可以自己决定这是否是一个有效的方法。以下是一些基本测试用例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import timeit def append_to_list_with_join(): s=[] for i in xrange(100): s.append("abcdefg"[i%7]) return ''.join(s) def append_to_list_with_join_opt(): s=[] x = s.append for i in xrange(100): x("abcdefg"[i%7]) return ''.join(s) def plus_equals_string(): s='' for i in xrange(100): s+="abcdefg"[i%7] return s def plus_assign_string(): s='' for i in xrange(100): s=s+"abcdefg"[i%7] return s def list_comp_join(): return ''.join(["abcdefg"[i%7] for i in xrange(100)]) def list_comp(): return ["abcdefg"[i%7] for i in xrange(100)] def empty_loop(): for i in xrange(100): pass def loop_mod(): for i in xrange(100): a ="abcdefg"[i%7] def fast_list_join(): return"".join(["0"] * 100) for f in [append_to_list_with_join, append_to_list_with_join_opt, plus_equals_string,plus_assign_string,list_comp_join, list_comp, empty_loop,loop_mod, fast_list_join]: print f.func_name, timeit.timeit(f) |
。
这就是它们的成本:
1 2 3 4 5 6 7 8 9 | append_to_list_with_join 25.4540209021 append_to_list_with_join_opt 19.9999782794 plus_equals_string 16.7842428996 plus_assign_string 14.8312124167 list_comp_join 16.329590353 list_comp 14.6934344309 empty_loop 2.3819276612 loop_mod 10.1424356308 fast_list_join 2.58149394686 |
首先,在Python中,很多东西都有意想不到的成本。用join将u附加到u列表而用join opt将u附加到u列表表明即使在对象上查找方法也有不可忽略的成本。在这种情况下,查找s.append是四分之一的时间。
接下来,list-comp-join和list-comp显示join()非常快:它大约需要list-comp-join时间的1.7%或10%。
loop_mod显示,此测试的最大部分实际上是设置数据,而不管使用哪种字符串构造方法。通过推断,"string=string+","string+=",以及列表理解所用的时间为:
1 2 3 | plus_equals_string = 16.78 - 10.14 = 6.64 plus_assign_string = 14.83 - 10.14 = 4.69 list_comp = 14.69 - 10.14 = 4.55 |
。
因此,对于op的问题,join()很快,但是无论是使用list原语还是使用list理解,创建底层列表的时间都与使用string原语创建字符串的时间相当。如果您已经有了一个列表,那么使用join()将其转换为字符串——这将很快。
OP给出的计时表明使用concatenate运算符构造列表的速度很慢。相反,使用列表理解很快。如果必须构建一个列表,请使用列表理解。
最后,让我们来看三个op最接近的函数:x、p和q之间的区别是什么?让我们简化一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import timeit def x(): s=[] for i in range(100): s.append("c") def p(): s=[] for i in range(100): s +="c" def q(): s=[] for i in range(100): s = s + ["c"] for f in [x,p,q]: print f.func_name, timeit.timeit(f) |
。
结果如下:
1 2 3 | x 16.0757342064 p 87.1533697719 q 85.0999698984 |
拆卸步骤如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | >>> import dis >>> dis.dis(x) 2 0 BUILD_LIST 0 3 STORE_FAST 0 (s) 3 6 SETUP_LOOP 33 (to 42) 9 LOAD_GLOBAL 0 (range) 12 LOAD_CONST 1 (100) 15 CALL_FUNCTION 1 18 GET_ITER >> 19 FOR_ITER 19 (to 41) 22 STORE_FAST 1 (i) 4 25 LOAD_FAST 0 (s) 28 LOAD_ATTR 1 (append) 31 LOAD_CONST 2 ('c') 34 CALL_FUNCTION 1 37 POP_TOP 38 JUMP_ABSOLUTE 19 >> 41 POP_BLOCK >> 42 LOAD_CONST 0 (None) 45 RETURN_VALUE >>> dis.dis(p) 2 0 BUILD_LIST 0 3 STORE_FAST 0 (s) 3 6 SETUP_LOOP 30 (to 39) 9 LOAD_GLOBAL 0 (range) 12 LOAD_CONST 1 (100) 15 CALL_FUNCTION 1 18 GET_ITER >> 19 FOR_ITER 16 (to 38) 22 STORE_FAST 1 (i) 4 25 LOAD_FAST 0 (s) 28 LOAD_CONST 2 ('c') 31 INPLACE_ADD 32 STORE_FAST 0 (s) 35 JUMP_ABSOLUTE 19 >> 38 POP_BLOCK >> 39 LOAD_CONST 0 (None) 42 RETURN_VALUE >>> dis.dis(q) 2 0 BUILD_LIST 0 3 STORE_FAST 0 (s) 3 6 SETUP_LOOP 33 (to 42) 9 LOAD_GLOBAL 0 (range) 12 LOAD_CONST 1 (100) 15 CALL_FUNCTION 1 18 GET_ITER >> 19 FOR_ITER 19 (to 41) 22 STORE_FAST 1 (i) 4 25 LOAD_FAST 0 (s) 28 LOAD_CONST 2 ('c') 31 BUILD_LIST 1 34 BINARY_ADD 35 STORE_FAST 0 (s) 38 JUMP_ABSOLUTE 19 >> 41 POP_BLOCK >> 42 LOAD_CONST 0 (None) 45 RETURN_VALUE |
。
这些循环几乎是相同的。比较相当于调用_function+pop_top与inplace_add+store_fast与build_list+binary_add+store_fast。然而,我不能给出比这更低级的解释——我只是无法在网络上找到Python字节码的成本。然而,您可能会从DougHellmann在dis上发布的本周python模块中获得一些灵感。
这里已经有很多好的总结,但只是为了提供更多的证据。
源代码:我盯着Python源代码看了一个小时,计算了复杂度!
我的发现。
对于2个字符串。(假设n是两个字符串的长度)
1 2 3 | Concat (+) - O(n) Join - O(n+k) effectively O(n) Format - O(2n+k) effectively O(n) |
超过2个字符串。(假设n是所有字符串的长度)
1 2 3 | Concat (+) - O(n^2) Join - O(n+k) effectively O(n) Format - O(2n+k) effectively O(n) |
。
结果:
如果您有两个字符串,技术上来说连接(+)更好,尽管它与join和format完全相同。
如果有两个以上的字符串,concat会变得很糟糕,join和format实际上是相同的,尽管技术上join更好一些。
总结:
如果你不关心效率,就使用上面的任何一个。(不过既然你问了这个问题,我想你会关心的)
因此-
如果有两个字符串,请使用concat(不在循环中时!)
如果有两个以上的字符串(所有字符串)(或在循环中),请使用join
如果没有任何字符串,请使用格式,因为duh。
希望这有帮助!
我从专家们发布的答案中找到了答案。python字符串连接(和计时测量)取决于这些(据我所见):
- 连接数
- 字符串的平均长度
- 函数调用数
我已经构建了一个与这些相关的新代码。感谢Peter S Magnusson、SEPP2K、Hughdbrown、David Wolver和其他人指出了我之前错过的重要要点。另外,在这段代码中,我可能遗漏了一些东西。所以,我非常感谢任何指出我们的错误、建议、批评等的回复。毕竟,我是来学习的。这是我的新代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | from timeit import timeit noc = 100 tocat ="a" def f_call(): pass def loop_only(): for i in range(noc): pass def concat_method(): s = '' for i in range(noc): s = s + tocat def list_append(): s=[] for i in range(noc): s.append(tocat) ''.join(s) def list_append_opt(): s = [] zap = s.append for i in range(noc): zap(tocat) ''.join(s) def list_comp(): ''.join(tocat for i in range(noc)) def concat_method_buildup(): s='' def list_append_buildup(): s=[] def list_append_opt_buildup(): s=[] zap = s.append def function_time(f): return timeit(f,number=1000)*1000 f_callt = function_time(f_call) def measure(ftuple,n,tc): global noc,tocat noc = n tocat = tc loopt = function_time(loop_only) - f_callt buildup_time = function_time(ftuple[1]) -f_callt if ftuple[1] else 0 total_time = function_time(ftuple[0]) return total_time, total_time - f_callt - buildup_time - loopt*ftuple[2] functions ={'Concat Method\t\t':(concat_method,concat_method_buildup,True), 'List append\t\t\t':(list_append,list_append_buildup,True), 'Optimized list append':(list_append_opt,list_append_opt_buildup,True), 'List comp\t\t\t':(list_comp,0,False)} for i in range(5): print(" %d concatenation\t\t\t\t10'a'\t\t\t\t 100'a'\t\t\t1000'a'"%10**i) print('-'*80) for (f,ft) in functions.items(): print(f,"\t|",end="\t") for j in range(3): t = measure(ft,10**i,'a'*10**j) print("%.3f %.3f |" % t,end="\t") print() |
这就是我得到的。[时间列中显示两次(按比例缩放):第一次是总函数执行时间,第二次是实际时间(?)连接时间。我已经扣除了函数调用时间、函数构建时间(初始化时间)和迭代时间。在这里,我正在考虑一个没有循环就无法完成的情况(在里面说更多的语句)。]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 1 concatenation 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 2.310 2.168 | 2.298 2.156 | 2.304 2.162 Optimized list append | 1.069 0.439 | 1.098 0.456 | 1.071 0.413 Concat Method | 0.552 0.034 | 0.541 0.025 | 0.565 0.048 List append | 1.099 0.557 | 1.099 0.552 | 1.094 0.552 10 concatenations 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 3.366 3.224 | 3.473 3.331 | 4.058 3.916 Optimized list append | 2.778 2.003 | 2.956 2.186 | 3.417 2.639 Concat Method | 1.602 0.943 | 1.910 1.259 | 3.381 2.724 List append | 3.290 2.612 | 3.378 2.699 | 3.959 3.282 100 concatenations 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 15.900 15.758 | 17.086 16.944 | 20.260 20.118 Optimized list append | 15.178 12.585 | 16.203 13.527 | 19.336 16.703 Concat Method | 10.937 8.482 | 25.731 23.263 | 29.390 26.934 List append | 20.515 18.031 | 21.599 19.115 | 24.487 22.003 1000 concatenations 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 134.507 134.365 | 143.913 143.771 | 201.062 200.920 Optimized list append | 112.018 77.525 | 121.487 87.419 | 151.063 117.059 Concat Method | 214.329 180.093 | 290.380 256.515 | 324.572 290.720 List append | 167.625 133.619 | 176.241 142.267 | 205.259 171.313 10000 concatenations 1'a' 10'a' 100'a' ------------------- ---------------------- ------------------- ---------------- List comp | 1309.702 1309.560 | 1404.191 1404.049 | 2912.483 2912.341 Optimized list append | 1042.271 668.696 | 1134.404 761.036 | 2628.882 2255.804 Concat Method | 2310.204 1941.096 | 2923.805 2550.803 | STUCK STUCK List append | 1624.795 1251.589 | 1717.501 1345.137 | 3182.347 2809.233 |
号
综上所述,我为自己做了以下决定:
最后,我想更深入地学习Python。所以,在我的观察中会有错误是不寻常的。所以,请对此发表评论,并建议我是否走错了路线。感谢大家的参与。
您要测量两个不同的操作:字符串数组的创建和字符串的串联。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import timeit def x(): s = [] for i in range(100): s.append("abcdefg"[i%7]) return ''.join(s) def y(): s = '' for i in range(100): s +="abcdefgh"[i%7] # timeit.timeit(x) returns about 32s # timeit.timeit(y) returns about 23s |
从上面看来,'+'确实是比join更快的操作。但是考虑一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | src = [] def c(): global src s = [] for i in range(100): s.append("abcdefg"[i%7]) src = s def x2(): return ''.join(src) def y2(): s = '' for i in range(len(src)): s += src[i] return s # timeit.timeit(c) returns about 30s # timeit.timeit(x2) returns about 1.5s # timeit.timeit(y2) returns about 14s |
号
换句话说,通过计时x()与y(),源数组的构造会污染结果。如果你打破这一点,你会发现加入更快。
此外,您使用的是小数组,而您的计时数字恰好是一致的。如果显著增加数组的大小和每个字符串的长度,则差异会更明显:
1 2 3 4 5 6 7 8 9 | def c2(): global src s = [] for i in range(10000): s.append("abcdefghijklmnopqrstuvwxyz0123456789" src = s # timeit.timeit(x2, number=10000) returns about 1s # timeit.timeit(y2, number=10000) returns about 80s |
带字符串的+=和+之间有一个区别——如果没有其他对"x"的引用,x+=y可以直接附加到x,而不必复制要附加到的字符串——这与使用".join()获得的好处相同。
".join()优于+或+=的主要好处是join()应始终提供线性性能,而在许多情况下,+/+=将提供二次性能(即,当文本量翻倍时,所用时间将翻倍)。但这只对大量的文本有效,而不仅仅是100字节,而且我认为如果只有一个对要附加到的字符串的引用,它就不会被触发。
详细说明:
对于字符串连接,最好的情况是只查看最后一个字符串中的每个字符一次。".join()很自然地做到了这一点——它从一开始就拥有了所需的所有信息。
然而,a+=b有两种工作方式,它可以将"b"添加到现有的字符串中,在这种情况下,它只需要查看"b"中的字符,或者也可以查看"a"中的字符。
在C语言中,strcat()总是查看两个字符串中的所有字符,所以它总是工作得很糟糕。然而,在python中,字符串长度是存储的,因此只要不在其他地方引用字符串,就可以对其进行扩展——并且您只需复制"b"中的字符就可以获得良好的性能。如果它在其他地方被引用,python将首先复制"a",然后在末尾添加"b",这会导致性能下降。如果您以这种方式附加五个字符串,则所用的时间将是:
1 2 3 4 | ab = a+b # Time is a + b abc = ab+c # Time is (a+b) + c abcd = abc+d # Time is (a+b+c) + d abcde = abcd+e # Time is (a+b+c+d) + e |
。
如果a,b,c,d,e的大小大致相同,比如,n,是n*(n-1)/2-1操作,或者基本上是n平方。
要获得x+=y的不良行为,请尝试:
1 2 3 4 5 6 | def a(n=100): res ="" for k in xrange(n): v=res res +="foobar" return res |
。
尽管实际上没有使用v,但它足以触发+=的较慢路径,并导致人们担心的不良行为。
我相信在Python2.0之前,+=才被引入,所以在Python1.6和更早版本中不使用".join()"之类的东西就无法有效地附加。
除了其他人说的以外,100个1字符的字符串真的很小。(我有点惊讶于结果的分离。)这是一种适合您的处理器缓存的数据集。你不会看到微基准的渐进性能。
有趣的是:我做了一些测试,其中字符串的大小发生了变化,这就是我发现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | def x(): x ="a" * 100 s=[] for i in range(100): # Other codes here... s.append(x) return ''.join(s) def z(): x ="a" * 100 s='' for i in xrange(100): # Other codes here... s=s+x return s from timeit import timeit print"x:", timeit(x, number=1000000) print"z:", timeit(z, number=1000000) |
对于长度为1的字符串(
1 2 | x: 27.2318270206 z: 14.4046051502 |
号
对于长度为100的字符串:
1 2 | x: 30.0796670914 z: 21.5891489983 |
对于长度为1000的字符串,运行timeit 100000次,而不是1000000次
1 2 | x: 14.1769361496 z: 31.4864079952 |
。
如果我对
在第一次读取时,string.join算法(不考虑边缘情况)似乎是:
1 2 3 4 5 6 7 8 9 10 11 12 | def join(sep, sequence): size = 0 for string in sequence: size += len(string) + len(sep) result = malloc(size) for string in sequence: copy string into result copy sep into result return result |
因此,这将需要或多或少的
字符串连接比python 2.5慢得多,因为它仍然为每个字符串连接创建一个新的副本,而不是附加到原始的字符串连接,这导致join()成为一个流行的解决方案。
下面是一个老基准,展示了老问题:http://www.skymind.com/~ocrow/python_-string/