Список понимания, карты и производительности numpy.vectorize - PullRequest
11 голосов
/ 24 апреля 2010

У меня есть функция foo (i), которая принимает целое число и занимает значительное количество времени для выполнения. Будет ли существенная разница в производительности между любым из следующих способов инициализации a :

a = [foo(i) for i in xrange(100)]

a = map(foo, range(100))

vfoo = numpy.vectorize(foo)
a = vfoo(range(100))

(Мне все равно, является ли вывод списком или массивом с пустым фрагментом.)

Есть ли лучший способ?

Ответы [ 4 ]

18 голосов
/ 24 апреля 2010
  • Почему вы оптимизируете это? Вы написали работающий, протестированный код, затем проверили свой алгоритм , профилированный ваш код и обнаружили, что оптимизация этого даст эффект? Вы делаете это в глубоком внутреннем цикле, где вы обнаружили, что проводите свое время? Если нет, не беспокойтесь.

  • Вы узнаете, какой из них работает быстрее всего по времени. Чтобы рассчитать это полезным способом, вам нужно будет специализировать его для фактического использования. Например, вы можете получить заметные различия в производительности между вызовом функции в понимании списка и встроенным выражением; неясно, действительно ли вы хотели получить первое или уменьшили его до того, чтобы сделать ваши дела похожими.

  • Вы говорите, что это не имеет значения, в конечном итоге вы получаете массив numpy или list, но если вы выполняете такую ​​микрооптимизацию, это имеет значение , так как они будут работать по-другому, когда вы будете использовать их позже. Поместить это может быть сложно, так что, надеюсь, окажется, что вся проблема спорна как преждевременная.

  • Обычно лучше просто использовать правильный инструмент для работы для ясности, читаемости и так далее. Редко мне было бы трудно выбирать между этими вещами.

    • Если бы мне понадобились массивы numpy, я бы использовал их. Я бы использовал их для хранения больших однородных массивов или многомерных данных. Я часто их использую, но редко, где, я думаю, я хотел бы использовать список.
      • Если бы я использовал их, я приложил бы все усилия, чтобы написать свои функции уже векторизованными, поэтому мне не пришлось использовать numpy.vectorize. Например, times_five ниже может использоваться для массива numpy без декорации.
    • Если у меня не было причины использовать numpy, то есть, если я не решал числовые математические задачи, не использовал специальные функции numpy, не сохранял многомерные массивы или что-то еще ...
      • Если бы у меня уже была уже существующая функция , я бы использовал map. Вот для чего это.
      • Если бы у меня была операция, которая помещалась в маленькое выражение, и мне не нужна была функция, я бы использовал понимание списка.
      • Если бы я просто хотел выполнить операцию для всех случаев, но на самом деле не нуждался в сохранении результата, я использовал бы цикл for.
      • Во многих случаях я бы фактически использовал map и перечислял ленивые эквиваленты пониманий: itertools.imap и выражения генератора. Это может уменьшить использование памяти в n случаях в некоторых случаях и может иногда избегать выполнения ненужных операций.

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

Рассмотрим следующие случаи (timeme.py опубликован ниже)

python -m timeit "from timeme import x, times_five; from numpy import vectorize" "vectorize(times_five)(x)"
1000 loops, best of 3: 924 usec per loop

python -m timeit "from timeme import x, times_five" "[times_five(item) for item in x]"
1000 loops, best of 3: 510 usec per loop

python -m timeit "from timeme import x, times_five" "map(times_five, x)"
1000 loops, best of 3: 484 usec per loop

Наивный наблюдатель пришел бы к выводу, что карта является наиболее эффективной из этих опций, но ответ по-прежнему "зависит". Подумайте о возможности использования преимуществ инструментов, которые вы используете: списки позволяют избежать определения простых функций; Numpy позволяет вам векторизовать вещи в C, если вы делаете правильные вещи.

python -m timeit "from timeme import x, times_five" "[item + item + item + item + item for item in x]"
1000 loops, best of 3: 285 usec per loop

python -m timeit "import numpy; x = numpy.arange(1000)" "x + x + x + x + x"
10000 loops, best of 3: 39.5 usec per loop

Но это еще не все, это еще не все. Рассмотрим силу изменения алгоритма. Это может быть еще более драматично.

python -m timeit "from timeme import x, times_five" "[5 * item for item in x]"
10000 loops, best of 3: 147 usec per loop

python -m timeit "import numpy; x = numpy.arange(1000)" "5 * x"
100000 loops, best of 3: 16.6 usec per loop

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

python -m timeit "from timeme import square, x" "map(square, x)"
10 loops, best of 3: 41.8 msec per loop

python -m timeit "from timeme import good_square, x" "map(good_square, x)"
1000 loops, best of 3: 370 usec per loop

И даже сейчас все это может иметь мало отношения к вашей реальной проблеме. Похоже, что numpy настолько хорош, если вы можете использовать его правильно, но у него есть свои ограничения: ни один из этих примеров не использовал реальные объекты Python в массивах. Это усложняет то, что должно быть сделано; очень даже А что, если нам удастся использовать типы данных C? Они менее надежны, чем объекты Python. Они не обнуляются. Переполнение целых чисел. Вы должны сделать дополнительную работу, чтобы получить их. Они статически напечатаны. Иногда эти вещи оказываются проблемами, даже неожиданными.

Итак, вы идете: окончательный ответ. «Это зависит».


# timeme.py

x = xrange(1000)

def times_five(a):
    return a + a + a + a + a

def square(a):
    if a == 0:
        return 0

    value = a
    for i in xrange(a - 1):
        value += a
    return value

def good_square(a):
    return a ** 2
9 голосов
/ 24 апреля 2010

Первый комментарий, который у меня есть, заключается в том, что вы должны использовать xrange() или range() во всех ваших примерах. если вы их смешаете, то сравниваете яблоки и апельсины.

во-вторых, @ Gabe считает, что если у вас много структур данных и они велики, то в целом выигрывает numpy ... просто имейте в виду, что большую часть времени C работает быстрее, чем Python, но опять же, большая часть время PyPy быстрее, чем CPython. :-)

что касается списков вызовов против map() вызовов ... один выполняет 101 вызов функции, а другой - 102. Вы не увидите значительной разницы во времени, как показано ниже при использовании timeit модуль, предложенный @Mark:

  • Понимание списка

    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.216 usec per loop
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.21 usec per loop
    $ python -m timeit "def foo(x):pass; [foo(i) for i in range(100)]"
    1000000 loops, best of 3: 0.212 usec per loop

  • map() вызов функции

    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.216 usec per loop
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.214 usec per loop
    $ python -m timeit "def foo(x):pass; map(foo, range(100))"
    1000000 loops, best of 3: 0.215 usec per loop

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

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

a = (foo(i) for i in range(100))

в соответствии с принципами дополнительной итерации, измените все вызовы range() на xrange() для оставшейся части выпусков 2.x, затем переключите их обратно на range() при портировании на Python 3 после замены xrange() и переименован в range(). :-)

7 голосов
/ 24 апреля 2010

Если выполнение самой функции занимает значительное время, не имеет значения, как вы отобразите ее вывод в массив. Однако, как только вы начнете получать массивы из миллионов чисел, numpy может сэкономить вам значительное количество памяти.

3 голосов
/ 24 апреля 2010

Понимание списка самое быстрое, потом карта, потом тупица на моей машине. Код numpy на самом деле немного медленнее, чем два других, но разница гораздо меньше, если вы используете numpy.arange вместо range (или xrange), как я делал в приведенные ниже времена. Кроме того, если вы используете psyco, понимание списка ускоряется, в то время как два других замедлились для меня. Я также использовал массивы чисел большего размера, чем в вашем коде, и моя функция foo просто вычисляла квадратный корень. Вот некоторые типичные времена.

Без психо:

list comprehension: 47.5581952455 ms
map: 51.9082732582 ms
numpy.vectorize: 57.9601876775 ms

С психо:

list comprehension: 30.4318844993 ms
map: 96.4504427239 ms
numpy.vectorize: 99.5858691538 ms

Я использовал Python 2.6.4 и модуль timeit.

Основываясь на этих результатах, я бы сказал, что, вероятно, не имеет значения, какой вы выберете для инициализации. Я бы, вероятно, выбрал бы numpy one или список, основанный на скорости, но в конечном счете вы должны позволить тому, что вы делаете с массивом, впоследствии руководить вашим выбором.

...