Python [<выражение генераторa>] как минимум в 3 раза быстрее, чем список (<выражение генераторa>)?

Похоже, что использование [] вокруг выражения генератора (test1) ведет себя значительно лучше, чем помещение его в список() (test2). Замедление не существует, когда я просто передаю список в список() для мелкой копии (test3). Почему это?

Доказательства:

from timeit import Timer

t1 = Timer("test1()", "from __main__ import test1")
t2 = Timer("test2()", "from __main__ import test2")
t3 = Timer("test3()", "from __main__ import test3")

x = [34534534, 23423523, 77645645, 345346]

def test1():
    [e for e in x]

print t1.timeit()
#0.552290201187


def test2():
    list(e for e in x)

print t2.timeit()
#2.38739395142

def test3():
    list(x)

print t3.timeit()
#0.515818119049

Машина: 64-разрядная AMD, Ubuntu 8.04, Python 2.7 (r27: 82500)

Ответы

Ответ 1

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

>python -mtimeit "x=[34534534, 23423523, 77645645, 345346]" "[e for e in x]"
1000000 loops, best of 3: 0.638 usec per loop

>python -mtimeit "x=[34534534, 23423523, 77645645, 345346]" "list(e for e in x)"
1000000 loops, best of 3: 1.72 usec per loop

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

>>> import dis
>>> x=[34534534, 23423523, 77645645, 345346]
>>> dis.dis(lambda: [e for e in x])
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x0000000001F8B330, file "<stdin>", line 1>)
              3 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (x)
              9 GET_ITER
             10 CALL_FUNCTION            1
             13 RETURN_VALUE
>>> dis.dis(lambda: list(e for e in x))
  1           0 LOAD_GLOBAL              0 (list)
              3 LOAD_CONST               0 (<code object <genexpr> at 0x0000000001F8B9B0, file "<stdin>", line 1>)
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              1 (x)
             12 GET_ITER
             13 CALL_FUNCTION            1
             16 CALL_FUNCTION            1
             19 RETURN_VALUE

Обратите внимание, что первый метод создает список напрямую, тогда как второй метод создает объект genexpr и передает его глобальному list. Вероятно, это связано с накладными расходами.

Отметим также, что разность примерно равна микросекунде, т.е. совершенно тривиальной.


Другие интересные данные

Это сохраняется для нетривиальных списков

>python -mtimeit "x=range(100000)" "[e for e in x]"
100 loops, best of 3: 8.51 msec per loop

>python -mtimeit "x=range(100000)" "list(e for e in x)"
100 loops, best of 3: 11.8 msec per loop

и для менее тривиальных функций отображения:

>python -mtimeit "x=range(100000)" "[2*e for e in x]"
100 loops, best of 3: 12.8 msec per loop

>python -mtimeit "x=range(100000)" "list(2*e for e in x)"
100 loops, best of 3: 16.8 msec per loop

и (хотя и менее сильно), если мы фильтруем список:

>python -mtimeit "x=range(100000)" "[e for e in x if e%2]"
100 loops, best of 3: 14 msec per loop

>python -mtimeit "x=range(100000)" "list(e for e in x if e%2)"
100 loops, best of 3: 16.5 msec per loop

Ответ 2

list(e for e in x) не является пониманием списка, а создается genexpr object (e for e in x) и передается функции list factory. Предположительно, создание объектов и вызовы методов создают накладные расходы.

Ответ 3

В python list имя должно быть просмотрено в модуле, а затем в встроенных. Хотя вы не можете изменить то, что понимает список, означает, что вызов списка должен быть стандартным вызовом функции lookup + function, поскольку его можно переопределить, чтобы быть чем-то другим.

Глядя на код vm, сгенерированный для понимания, можно видеть, что он встроен, а вызов списка - обычный вызов.

>>> import dis
>>> def foo():
...     [x for x in xrange(4)]
... 
>>> dis.dis(foo)
  2           0 BUILD_LIST               0
              3 DUP_TOP             
              4 STORE_FAST               0 (_[1])
              7 LOAD_GLOBAL              0 (xrange)
             10 LOAD_CONST               1 (4)
             13 CALL_FUNCTION            1
             16 GET_ITER            
        >>   17 FOR_ITER                13 (to 33)
             20 STORE_FAST               1 (x)
             23 LOAD_FAST                0 (_[1])
             26 LOAD_FAST                1 (x)
             29 LIST_APPEND         
             30 JUMP_ABSOLUTE           17
        >>   33 DELETE_FAST              0 (_[1])
             36 POP_TOP             
             37 LOAD_CONST               0 (None)
             40 RETURN_VALUE        

>>> def bar():
...     list(x for x in xrange(4))
... 
>>> dis.dis(bar)
  2           0 LOAD_GLOBAL              0 (list)
              3 LOAD_CONST               1 (<code object <genexpr> at 0x7fd1230cf468, file "<stdin>", line 2>)
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              1 (xrange)
             12 LOAD_CONST               2 (4)
             15 CALL_FUNCTION            1
             18 GET_ITER            
             19 CALL_FUNCTION            1
             22 CALL_FUNCTION            1
             25 POP_TOP             
             26 LOAD_CONST               0 (None)
             29 RETURN_VALUE  

Ответ 4

Ваш тест2 примерно эквивалентен:

def test2():
    def local():
        for i in x:
            yield i
    return list(local())

Накладные расходы вызова объясняют увеличенное время обработки.