Подтвердить что ты не робот

Почему переменная1 + = переменная2 намного быстрее, чем переменная1 = переменная1 + переменная2?

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

sTable = sTable + '\n' + GetRow()

где sTable - строка.

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

sTable += '\n' + GetRow()

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

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

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

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

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

4b9b3361

Ответ 1

Речь идет не об использовании inplace += versus + binary add. Вы не рассказали нам всю историю. Ваша исходная версия объединила 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(). Чтобы иметь возможность оптимизировать конкатенацию, изменяя строку, сначала нужно убедиться, что в строке нет других ссылок на нее; если только стек и исходная переменная ссылаются на это, это может быть сделано, и операция next заменит исходную ссылку на переменную.

Итак, если на строку есть всего 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 растет, и для создания этого нового строкового объекта требуется больше времени.

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

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