Почему variable1 += variable2 намного быстрее, чем variable1 = variable1 + variable2?


я унаследовал некоторый код Python, который используется для создания огромных таблиц (до 19 столбцов шириной 5000 строк). Это заняло девять секунд для таблицы, которая будет нарисована на экране. Я заметил, что каждая строка была добавлена с помощью этого кода:

sTable = sTable + 'n' + GetRow()

здесь sTable - это строка.

Я изменил это:

sTable += 'n' + GetRow()

и я заметил, что таблица теперь появилась в шесть секунд.

а потом я изменил его к:

sTable += 'n%s' % GetRow()

на основе эти советы по производительности Python (еще шесть секунд).

так как это было вызвано около 5000 раз, он подчеркнул проблему производительности. Но почему была такая большая разница? И почему компилятор не обнаружил проблему в первой версии и не оптимизировал ее?

1 52

1 ответ:

речь идет не об использовании inplace += и + бинарные добавить. Ты не рассказал нам всю историю. Ваша оригинальная версия объединила 3 строки, а не только две:

sTable = sTable + '\n' + sRow  # simplified, sRow is a function call

Python пытается помочь и оптимизирует конкатенацию строк; как при использовании strobj += otherstrobj и strobj = strobj + otherstringobj, но он не может применить эту оптимизацию, когда задействовано более 2 строк.

строки в Python являются неизменяемыми обычно, а если нет других ссылок в левой строке объекта и это отскок в любом случае, то Python Читы и изменяет строку. Это позволяет избежать необходимости создавать новую строку каждый раз, когда вы объединяете, и это может привести к большому улучшению скорости.

это реализовано в цикле оценки байт-кода. Как при использовании BINARY_ADD на две строки и при использовании INPLACE_ADD на две строки, Python делегирует конкатенацию специальному помощнику функция string_concatenate(). Чтобы оптимизировать конкатенацию путем изменения строки, сначала необходимо убедиться, что строка не имеет других ссылок на нее; если только стек и исходная переменная ссылаются на нее, то это можно сделать,и the далее операция будет заменять исходную ссылку на переменную.

так что если есть только 2 ссылки на строку, а следующий оператор является одним из STORE_FAST (поставить локальный переменная),STORE_DEREF (установите переменную, на которую ссылаются закрытые функции) или STORE_NAME (установите глобальную переменную), и затронутая переменная в настоящее время ссылается на ту же строку, затем эта целевая переменная очищается, чтобы уменьшить количество ссылок только на 1, стек.

и именно поэтому ваш исходный код не может использовать эту оптимизацию полностью. Первая часть вашего выражения -sTable + '\n' и далее операция другой BINARY_ADD:

>>> import dis
>>> dis.dis(compile(r"sTable = sTable + '\n' + sRow", '<stdin>', 'exec'))
  1           0 LOAD_NAME                0 (sTable)
              3 LOAD_CONST               0 ('\n')
              6 BINARY_ADD          
              7 LOAD_NAME                1 (sRow)
             10 BINARY_ADD          
             11 STORE_NAME               0 (sTable)
             14 LOAD_CONST               1 (None)
             17 RETURN_VALUE        

первый BINARY_ADD после LOAD_NAME к sRow переменная, а не операция хранения. Это первый BINARY_ADD всегда должен приводить к новому строковому объекту, все больше как sTable растет и занимает все больше и больше времени, чтобы создать этот новый объект String.

вы изменили этот код:

sTable += '\n%s' % sRow

, который удалена вторая конкатенация. Теперь байт-код это:

>>> dis.dis(compile(r"sTable += '\n%s' % sRow", '<stdin>', 'exec'))
  1           0 LOAD_NAME                0 (sTable)
              3 LOAD_CONST               0 ('\n%s')
              6 LOAD_NAME                1 (sRow)
              9 BINARY_MODULO       
             10 INPLACE_ADD         
             11 STORE_NAME               0 (sTable)
             14 LOAD_CONST               1 (None)
             17 RETURN_VALUE        

и все что нам осталось-это INPLACE_ADD за ним следует магазин. Сейчас sTable может быть изменен на месте, не приводя к еще большему новому объекту строки.

вы бы получили такую же разницу в скорости с:

sTable = sTable + ('\n%s' % sRow)

здесь.

испытание временем показывает разницу:

>>> import random
>>> from timeit import timeit
>>> testlist = [''.join([chr(random.randint(48, 127)) for _ in range(random.randrange(10, 30))]) for _ in range(1000)]
>>> def str_threevalue_concat(lst):
...     res = ''
...     for elem in lst:
...         res = res + '\n' + elem
... 
>>> def str_twovalue_concat(lst):
...     res = ''
...     for elem in lst:
...         res = res + ('\n%s' % elem)
... 
>>> timeit('f(l)', 'from __main__ import testlist as l, str_threevalue_concat as f', number=10000)
6.196403980255127
>>> timeit('f(l)', 'from __main__ import testlist as l, str_twovalue_concat as f', number=10000)
2.3599119186401367
str.join():
table_rows = []
for something in something_else:
    table_rows += ['\n', GetRow()]
sTable = ''.join(table_rows)

это еще быстрее:

>>> def str_join_concat(lst):
...     res = ''.join(['\n%s' % elem for elem in lst])
... 
>>> timeit('f(l)', 'from __main__ import testlist as l, str_join_concat as f', number=10000)
1.7978830337524414

но вы не можете победить, используя только '\n'.join(lst):

>>> timeit('f(l)', 'from __main__ import testlist as l, nl_join_concat as f', number=10000)
0.23735499382019043