Чем понимание списков "создает список" и "добавляет элементы" отличается от простого l oop? - PullRequest
2 голосов
/ 12 июля 2020

Пока я читал и искал, что именно вызывает разницу в производительности между циклами и пониманием списка (на основе нижеприведенных простых тестовых примеров, понимание списка быстрее), я обнаружил следующие сообщения здесь, в SO:

Простые тестовые примеры:

def f1():
    t = []
    for i in range(10000):
        t.append(i)

def f2():
    t = [i for i in range(10000)]

Почему понимание списка намного быстрее, чем добавление к списку?

Являются ли списки понимания и функциональные функции быстрее, чем «циклы for»?

Как я понимаю из приведенных выше сообщений, основные отличия заключаются в следующем:

  1. For l oop строит список, но список понимание не
  2. Для l oop загружает метод добавления на каждой итерации, но понимание списка не

Затем я использовал дизассемблер, чтобы увидеть детали, и я увидел следующие шаги для следующий блок кода:

Код:

def f2():
    t = [i for i in range(10000)]
    
dis.dis(f2)

Результат дизассемблера:

0 BUILD_LIST              
2 LOAD_FAST                
4 FOR_ITER                 
6 STORE_FAST               
8 LOAD_FAST                
10 LIST_APPEND        
12 JUMP_ABSOLUTE            
14 RETURN_VALUE

На основе Python делать c; 0 BUILD_LIST создает список, а 10 LIST_APPEND использует метод добавления в отличие от вышеупомянутых связанных сообщений:

LIST_APPEND(i)
    Calls list.append(TOS[-i], TOS). Used to implement list comprehensions.

BUILD_LIST(count)
    Works as BUILD_TUPLE, but creates a list.

Я не мог понять, что мне здесь не хватает. Отличается ли способ понимания списка и добавления от l oop, потому что в любом случае LIST_APPEND содержит метод append, а BUILD_LIST создает список? а может причина разницы в производительности в другом? Может кто-нибудь прояснить это для меня?

Спасибо!

1 Ответ

2 голосов
/ 12 июля 2020

Я пробовал другой подход:

from collections import Counter


def f1():
    t = []
    for i in range(10000):
        t.append(i)

def f2():
    t = [i for i in range(10000)]

    
f1i = Counter(i.opname for i in dis.get_instructions(f1))
f2i = Counter(i.opname for i in dis.get_instructions(f2))

print(f"Only in regular append: {f1i - f2i}")
print(f"Only in list comprehension: {f2i - f1i}")

Результаты: (Python 3.7.6):

Only in regular append: Counter({'LOAD_FAST': 2, 'BUILD_LIST': 1, 'STORE_FAST': 1, 'SETUP_LOOP': 1, 'FOR_ITER': 1, 'LOAD_METHOD': 1, 'CALL_METHOD': 1, 'POP_TOP': 1, 'JUMP_ABSOLUTE': 1, 'POP_BLOCK': 1})
Only in list comprehension: Counter({'LOAD_CONST': 2, 'MAKE_FUNCTION': 1, 'CALL_FUNCTION': 1})

Вы можете видеть, что "обычное" добавление использует LOAD_METHOD (для list.append), LOAD_FAST, CALL_METHOD и POP_TOP на каждой итерации:

dis.dis(f1)
  5           0 BUILD_LIST               0
              2 STORE_FAST               0 (t)

  6           4 SETUP_LOOP              26 (to 32)
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               1 (10000)
             10 CALL_FUNCTION            1
             12 GET_ITER
        >>   14 FOR_ITER                14 (to 30)
             16 STORE_FAST               1 (i)

  7          18 LOAD_FAST                0 (t)
             20 LOAD_METHOD              1 (append)
             22 LOAD_FAST                1 (i)
             24 CALL_METHOD              1
             26 POP_TOP
             28 JUMP_ABSOLUTE           14
        >>   30 POP_BLOCK
        >>   32 LOAD_CONST               0 (None)
             34 RETURN_VALUE

Также рекомендуется иметь в виду, что коды операций меняются с от одной версии к другой.

...