Почему variable1 += variable2 намного быстрее, чем variable1 = variable1 + variable2?
я унаследовал некоторый код Python, который используется для создания огромных таблиц (до 19 столбцов шириной 5000 строк). Это заняло девять секунд для таблицы, которая будет нарисована на экране. Я заметил, что каждая строка была добавлена с помощью этого кода:
sTable = sTable + 'n' + GetRow()
здесь sTable
- это строка.
Я изменил это:
sTable += 'n' + GetRow()
и я заметил, что таблица теперь появилась в шесть секунд.
а потом я изменил его к:
sTable += 'n%s' % GetRow()
на основе эти советы по производительности Python (еще шесть секунд).
так как это было вызвано около 5000 раз, он подчеркнул проблему производительности. Но почему была такая большая разница? И почему компилятор не обнаружил проблему в первой версии и не оптимизировал ее?
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)
здесь.
испытание временем показывает разницу:
str.join():>>> 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
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