Производительность замены элементов в списке по сравнению с пониманием списка

Как указал пользователь 2357112, исходный сценарий теста производительности не работал, как я ожидал. «после первого выполнения s1 в вашем списке нет 1, поэтому никакие дальнейшие выполнения s1 и s2 фактически не используют ветвь x == 1». Модифицированная версия:

import timeit
import random
random.seed(0)
a = [ random.randrange(10) for _ in range(10000)]
change_from = 1
change_to = 6
setup = "from __main__ import a, change_from, change_to"

# s1 is replaced for a simple for loop, which is faster than the original
s1 = """\
for i,x in enumerate(a):
    if x == change_from:
        a[i] = change_to
change_from, change_to = change_to, change_from
"""

s2 = """\
a = [change_to if x==change_from else x for x in a]
change_from, change_to = change_to, change_from
"""
print(timeit.timeit(stmt=s1,number=10000, setup=setup))
print(timeit.timeit(stmt=s2, number=10000, setup=setup))

Этот скрипт заменяет все вхождения от 1 до 6, а при следующем запуске все вхождения от 6 до 1. И так далее. Результат:

7.841739330212443
5.5166219217914065

Почему понимание списка происходит быстрее?

И как разобраться в таком вопросе? Комментарий бортрайдера выглядит интересно, спасибо.

Используется следующая версия Python: Python 3.6.0 (v3.6.0:41df79263a11, 23 декабря 2016 г., 08:06:12) [MSC v.1900 64 бит (AMD64)] на win32

Поскольку подробного ответа я не получил, я попытался разобраться.

Если я представляю простой цикл for с помощью этой функции:

def func1(a):
    for i,x in enumerate(a):
        if x == 1:
            a[i] = 6
    return(a)

и разобрав его, я получил следующее:

func1:
  7           0 SETUP_LOOP              48 (to 51)
              3 LOAD_GLOBAL              0 (enumerate)
              6 LOAD_FAST                0 (a)
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 GET_ITER
        >>   13 FOR_ITER                34 (to 50)
             16 UNPACK_SEQUENCE          2
             19 STORE_FAST               1 (i)
             22 STORE_FAST               2 (x)

  8          25 LOAD_FAST                2 (x)
             28 LOAD_CONST               1 (1)
             31 COMPARE_OP               2 (==)
             34 POP_JUMP_IF_FALSE       13

  9          37 LOAD_CONST               2 (6)
             40 LOAD_FAST                0 (a)
             43 LOAD_FAST                1 (i)
             46 STORE_SUBSCR
             47 JUMP_ABSOLUTE           13
        >>   50 POP_BLOCK

 10     >>   51 LOAD_FAST                0 (a)
             54 RETURN_VALUE

Это просто. Он перебирает a и, если находит значение 1, заменяет его на 6 с помощью STORE_SUBSCR.

Если я представляю вариант понимания с этой функцией:

def func2(a): a = [6 if x==1 else x вместо x в a] return(a)

и разобрав его, я получил следующее:

func2:
  7           0 LOAD_CONST               1 (<code object <listcomp> at 0x00000000035731E0, file "<file_path>", line 7>)
              3 LOAD_CONST               2 ('func2.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_FAST                0 (a)
             12 GET_ITER
             13 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             16 STORE_FAST               0 (a)

  8          19 LOAD_FAST                0 (a)
             22 RETURN_VALUE

Это короче предыдущего. Однако он начинается с загрузки объекта кода. У func2 есть следующие константы кода:

>>> func2.__code__.co_consts
(None, <code object <listcomp> at 0x00000000035731E0, file "<file_path>", line 7>, 'func2.<locals>.<listcomp>')

и объект кода listcomp выглядит так:

>>> dis.dis(func2.__code__.co_consts[1].co_code)
          0 BUILD_LIST               0
          3 LOAD_FAST                0 (0)
    >>    6 FOR_ITER                30 (to 39)
          9 STORE_FAST               1 (1)
         12 LOAD_FAST                1 (1)
         15 LOAD_CONST               0 (0)
         18 COMPARE_OP               2 (==)
         21 POP_JUMP_IF_FALSE       30
         24 LOAD_CONST               1 (1)
         27 JUMP_FORWARD             3 (to 33)
    >>   30 LOAD_FAST                1 (1)
    >>   33 LIST_APPEND              2
         36 JUMP_ABSOLUTE            6
    >>   39 RETURN_VALUE

Таким образом, две реализации выполняют аналогичные шаги. Основное отличие состоит в том, что в версии для понимания FOR_ITER заменяется на CALL_FUNCTION.

Из этого я должен понять, почему понимание списка происходит быстрее, но я этого не делаю. Итак, мой первоначальный вопрос все еще актуален:

Почему понимание списка происходит быстрее?


person Ptheven    schedule 17.01.2017    source источник
comment
Понимание списков в целом быстрее, чем цикл for. Этот порядок величины кажется тем, что вы ожидаете.   -  person juanpa.arrivillaga    schedule 17.01.2017
comment
Я думаю, что замена на месте будет быстрее для больших списков, чем создание нового списка с пониманием списка. - Что, вы думали, что дополнительные ассигнования будут дорогими? Это CPython, очень динамичный и полностью без JIT. Почти все дороже аллокации.   -  person user2357112 supports Monica    schedule 17.01.2017
comment
@ user2357112 Поскольку вы, кажется, знаете ответ, не могли бы вы предложить его? Мне интересно узнать причину этого, и я уверен, что другие тоже.   -  person Tagc    schedule 17.01.2017
comment
Кроме того, обратите внимание, что после первого выполнения s1 в вашем списке нет единиц, поэтому никакие дальнейшие выполнения s1 и s2 фактически не берут ветвь x==1.   -  person user2357112 supports Monica    schedule 17.01.2017
comment
Я этого не знал, я попытаюсь придумать какое-нибудь реальное измерение. Однако изначально я учил, что понимание списка создает совершенно новый список и копирует значения из исходного списка. И замена всего нескольких значений кажется немного менее трудоемкой. Ясно, что этот упрощенный взгляд на понимание ложен. Буду копать глубже в теме. Спасибо   -  person Ptheven    schedule 17.01.2017
comment
how should one figure out this kind of question? stackoverflow.com/questions/41051553/   -  person boardrider    schedule 18.01.2017