Учет списка, карта и numpy.vectorize performance

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

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

a = map(foo, range(100))

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

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

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

Ответ 1

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

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

поскольку listcomps vs. map() вызывает go... один делает 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 не создает весь список в памяти... это более дружественный к памяти, ленивый итеративный способ перебора элементов. лучшая часть состоит в том, что его синтаксис почти идентичен синтаксису listcomps:

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

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

Ответ 2

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

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

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

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

    • Если мне нужны массивы numpy, я бы использовал их. Я бы использовал их для хранения больших однородных массивов или многомерных данных. Я использую их много, но редко, где я думаю, что хочу использовать список.
      • Если бы я использовал их, я сделал бы все возможное, чтобы написать мои уже векторизованные функции, поэтому мне не пришлось использовать numpy.vectorize. Например, times_five ниже может использоваться в массиве numpy без декорации.
    • Если у меня не было причины использовать numpy, то есть, если бы я не решал числовые математические задачи или использовал специальные функции numpy или сохранял многомерные массивы или что-то еще...
      • Если бы у меня была уже существующая функция, я бы использовал map. Это для чего.
      • Если у меня была операция, которая помещалась внутри небольшого выражения, и мне не нужна функция, я бы использовал понимание списка.
      • Если я просто хотел выполнить операцию для всех случаев, но на самом деле не нужно было сохранять результат, я бы использовал простой цикл.
      • Во многих случаях я фактически использовал 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

Наивный obsever заключил бы, что карта является наилучшим из этих вариантов, но ответ по-прежнему "это зависит". Подумайте о возможности использования преимуществ используемых вами инструментов: понимание списков позволяет избежать определения простых функций; 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 настолько велико, если вы можете использовать его правильно, но у него есть свои ограничения: ни один из этих примеров 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

Ответ 3

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

Ответ 4

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

Без psyco:

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

С psyco:

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

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

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