今天文章被人纠了错,就跑去人家主页上逛。结果看到有篇文章说字符串相加速度的,看看结论很奇怪。就做了一下实验。原文可以看这里。我们只讨论python部分的行为。首先是论证我观点的测试,无关部分就跳过了,大家应当可以自行补上。

def f():
	s = ''
	for i in range(3):
		s += '123'
		print id(s)
	return s

f()
f()

输出:

138190216
138276992
138276992
138190216
138276992
138276992

至少在几十的规模,这个结论还是成立的。说明对象确实被缓存了,这导致了字符串相加的多次测试中,后续次数都没有实际的执行字符串分配动作。召dis来问之。

14           0 LOAD_CONST               1 (u'')
			 3 STORE_FAST               0 (s)

15           6 SETUP_LOOP              46 (to 55)
			 9 LOAD_GLOBAL              0 (range)
			12 LOAD_CONST               2 (3)
			15 CALL_FUNCTION            1
			18 GET_ITER            
	   >>   19 FOR_ITER                32 (to 54)
			22 STORE_FAST               1 (i)

16          25 LOAD_FAST                0 (s)
			28 LOAD_CONST               3 (u'123')
			31 INPLACE_ADD         
			32 STORE_FAST               0 (s)

17          35 LOAD_GLOBAL              1 (print)
			38 LOAD_GLOBAL              2 (id)
			41 LOAD_FAST                0 (s)
			44 CALL_FUNCTION            1
			47 CALL_FUNCTION            1
			50 POP_TOP             
			51 JUMP_ABSOLUTE           19
	   >>   54 POP_BLOCK           

18     >>   55 LOAD_FAST                0 (s)
			58 RETURN_VALUE        

我们看到s是local变量,这个符合我们的预期。但是后续确实发生了add,而string的+算法,我们可以参考Objects/stringobject.c:1015这里,string_concat函数的内容。这里没有加速过程,即使有,也只有发生在len(a) == 0 or len(b) == 0的情况下。对于123的求和无法说明原因。

这里我只能做一个假定,我们发现的id相等,其实可能是由于内存重分配的结果。一个对象被回收后,是存放在对象池中的,再分配的时候,可能按照规则被重新分配。当然这只能是一个推测,实际证明必须在python源码中修改并且重编译,我就不找这个麻烦了。

另一个问题,实验的时候,只是把固定字符串更换为随机字符串而已,长度也没有发生变化。预期的结果应当是+=的速度变慢,结果+=速度不变。问题是join的速度加快算是怎么回事?关于join,我们可以参考Objects/stringobject.c:1574,这里说明了join的工作流程,也没有任何加速!

唯一的解释,就是append的工作速度慢到超乎正常人类的想像,实验证明了这点。

def g():
	a = []
	for i in range(1000):
		a.append('1234567890')
		# s = ''.join(a)
print Timer('g()', 'from __main__ import g').timeit(10000)

结果是0.91,append动作比join慢10倍。