Характеристики работы Python

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

Есть несколько вещей, которые я хотел бы знать:

Насколько разумным является его оптимизатор?

Некоторые современные компиляторы были благословлены замечательно умными оптимизаторами, которые часто могут принимать простой код и заставлять его работать быстрее, чем любые попытки пользователя по настройке кода. В зависимости от того, насколько интеллектуальным является оптимизатор, для моего кода может быть намного глубже.

В то время как Python является "интерпретированным" языком, он, похоже, компилируется до некоторой формы байт-кода (.pyc). Насколько он умен, когда он это делает?

  • Будет ли он сбрасывать константы?
  • Будет ли он встроить небольшие функции или развернуть короткие циклы?
  • Будет ли он выполнять сложный анализ данных/потоков. Я не квалифицирован для правильного объяснения.

Насколько быстрыми являются следующие операции (сравнительно)

  • Функциональные вызовы
  • Создание экземпляра класса
  • Арифметика
  • "Тяжелые" математические операции, такие как sqrt()

Как обрабатываются числа внутри?

Как хранятся числа в Python. Сохраняются ли они как целые числа/поплавки внутри или перемещаются в виде строки?

NumPy

Сколько может быть разница в производительности в NumPy? Это приложение сильно использует векторы и смежную математику. Сколько можно сделать, используя это, чтобы ускорить эти операции.

Что-нибудь еще интересное

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

Некоторый фон...

Так как есть несколько человек, которые привносят "первый взгляд на ваши алгоритмы" (это довольно разумный совет, но на самом деле не помогает с моей целью задавать этот вопрос), я немного добавлю, и почему я спрашиваю об этом.

Предложенный проект для животных - это трассировщик лучей, написанный на Python. Это еще не очень далеко, и в настоящее время просто ударяют тесты против двух объектов (треугольника и сферы) внутри сцены. Выполнение расчетов затенения, затенения или освещения не выполняется. Алгоритм в основном:

for each x, y position in the image:
     create a ray
     hit test vs. sphere
     hit test vs. triangle
     colour the pixel based on the closest object, or black if no hit.

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

Хотя я понимаю, что луч-трассировщик, основанный на Python, не сможет достичь производительности на основе C, учитывая, что трассировки лучей реального времени, такие как Arauna может управлять 15-20 FPS на моем компьютере, предоставляя достаточно сложные сцены на 640x480, я ожидаю, что рендеринг очень простого изображения 500x500 в Python будет выполнен в течение секунды.

В настоящее время мой код занимает 38 секунд. Мне кажется, что это действительно не так долго.

Профилирование показывает большую часть времени, затрачиваемого в реальных процедурах тестирования для этих фигур. Это не особенно удивительно в лучей, и я ожидал. Количество вызовов для этих хит-тестов составляет 250 000 (точно 500 × 500), что указывает на то, что их вызывают точно так же часто, как и должны быть. Это довольно текстовая книга из 3%, где рекомендуется оптимизация.

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

Ответ 1

Компилятор Python преднамеренно грязный - это делает его быстрым и очень предсказуемым. Помимо некоторого постоянного сгибания, он в основном генерирует байт-код, который точно имитирует ваши источники. Кто-то еще предложил dis, и это действительно хороший способ взглянуть на байт-код, который вы получаете - например, как for i in [1, 2, 3]: фактически не выполняет постоянную фальцовку, а генерирует буквенный список "на лету", а for i in (1, 2, 3): (зацикливание на литеральном корте, а не в литеральном списке) -, способный к постоянной смене (причина: a list является изменчивым объектом, и для того, чтобы придерживаться "грязной" миссии миссии, компилятор не потрудился проверить, что этот конкретный список никогда не изменяется, поэтому его можно оптимизировать в кортеж).

Итак, есть место для большой ручной микро-оптимизации - в частности, подъем. I.e., переписать

for x in whatever():
    anobj.amethod(x)

а

f = anobj.amethod
for x in whatever():
    f(x)

чтобы сохранить повторяющиеся запросы (компилятор не проверяет, может ли запуск anobj.amethod реально изменить привязки anobj и c, чтобы в следующий раз нужен свежий поиск - это просто грязь вещь, т.е. отсутствие подъема, которая гарантирует правильность, но определенно не гарантирует высокую скорость; -).

Модуль timeit (лучше всего используется в командной строке IMHO) позволяет очень просто измерить общие эффекты компиляции + интерпретацию байт-кода (просто убедитесь, что фрагмент, который вы измеряете, не имеет побочных эффектов, которые повлияли бы на время, так как timeit делает, повторяя его снова и снова в цикле;-). Например:

$ python -mtimeit 'for x in (1, 2, 3): pass'
1000000 loops, best of 3: 0.219 usec per loop
$ python -mtimeit 'for x in [1, 2, 3]: pass'
1000000 loops, best of 3: 0.512 usec per loop

вы можете увидеть затраты на повторное построение списка - и убедитесь, что это действительно то, что мы наблюдаем, попробовав незначительную настройку:

$ python -mtimeit -s'Xs=[1,2,3]' 'for x in Xs: pass'
1000000 loops, best of 3: 0.236 usec per loop
$ python -mtimeit -s'Xs=(1,2,3)' 'for x in Xs: pass'
1000000 loops, best of 3: 0.213 usec per loop

перемещение итеративной конструкции в настройку -s (которая запускается только один раз и не синхронизируется) показывает, что правильная петля немного быстрее по кортежам (может быть, 10%), но большая проблема с первой парой (список медленнее чем кортеж более чем на 100%) в основном со строительством.

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

Насколько быстрыми являются следующие операции (Сравнительно)

* Function calls
* Class instantiation
* Arithmetic
* 'Heavier' math operations such as sqrt()
$ python -mtimeit -s'def f(): pass' 'f()'
10000000 loops, best of 3: 0.192 usec per loop
$ python -mtimeit -s'class o: pass' 'o()'
1000000 loops, best of 3: 0.315 usec per loop
$ python -mtimeit -s'class n(object): pass' 'n()'
10000000 loops, best of 3: 0.18 usec per loop

поэтому мы видим: создание экземпляра класса нового стиля и вызов функции (как пустой) имеют одинаковую скорость, причем экземпляры, возможно, имеют крошечный запас скорости, возможно, 5%; создание экземпляра класса старого стиля является самым медленным (примерно на 50%). Небольшие различия, такие как 5% или менее, конечно, могут быть шумом, поэтому рекомендуется повторять каждую попытку несколько раз; но различия, подобные 50%, безусловно, намного превосходят уровень шума.

$ python -mtimeit -s'from math import sqrt' 'sqrt(1.2)'
1000000 loops, best of 3: 0.22 usec per loop
$ python -mtimeit '1.2**0.5'
10000000 loops, best of 3: 0.0363 usec per loop
$ python -mtimeit '1.2*0.5'
10000000 loops, best of 3: 0.0407 usec per loop

и здесь мы видим: вызов sqrt медленнее, чем выполнение одного и того же вычисления оператором (с использованием оператора ** raise-to-power) примерно за счет вызова пустой функции; все арифметические операторы примерно одинаковы с точностью до шума (крошечная разница в 3 или 4 наносекунды определенно является шумом;-). Проверка того, может ли помешать постоянное складывание:

$ python -mtimeit '1.2*0.5'
10000000 loops, best of 3: 0.0407 usec per loop
$ python -mtimeit -s'a=1.2; b=0.5' 'a*b'
10000000 loops, best of 3: 0.0965 usec per loop
$ python -mtimeit -s'a=1.2; b=0.5' 'a*0.5'
10000000 loops, best of 3: 0.0957 usec per loop
$ python -mtimeit -s'a=1.2; b=0.5' '1.2*b'
10000000 loops, best of 3: 0.0932 usec per loop

... мы видим, что это действительно так: если один или оба числа просматриваются как переменные (которые блокируют постоянную складку), мы платим "реалистичную" стоимость. Переменный поиск имеет свои собственные затраты:

$ python -mtimeit -s'a=1.2; b=0.5' 'a'
10000000 loops, best of 3: 0.039 usec per loop

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

$ python -mtimeit -s'a=1.2; b=0.5' '1.2'
10000000 loops, best of 3: 0.0225 usec per loop

как вы видите, хотя и меньше, чем переменный поиск, он вполне сопоставим - примерно наполовину.

Если и когда (вооруженный тщательным профилированием и измерением) вы решаете, какое ядро ​​ваших вычислений отчаянно нуждается в оптимизации, я рекомендую попробовать cython - это слияние C/Python, которое пытается быть таким же опрятным, как Python, и так же быстро, как C, и, хотя он не может получить 100%, он, несомненно, делает хороший кулак (в частности, он делает двоичный код довольно немного быстрее, чем вы можете получить со своим предшественником, pyrex, а также быть немного богаче, чем он). За последние несколько процентов производительности вы, вероятно, все еще хотите перейти на C (или сборку/машинный код в некоторых исключительных случаях), но это было бы действительно очень редко.

Ответ 2

S.Lott прав: большие эффекты - это структуры данных и алгоритмы. Кроме того, если вы делаете много операций ввода-вывода, то как вы справитесь с этим, это будет иметь большое значение.

Но если вам интересно о внутренних компонентах компилятора: он будет складывать константы, но не будет встроенных функций или разворачивать петли. Функции инкрустации - сложная проблема в динамическом языке.

Вы можете увидеть, что делает компилятор, разобрав некоторый скомпилированный код. Поместите некоторый пример кода в файл my_file.py, затем используйте:

python -m dis my_file.py

Этот источник:

def foo():
    return "BAR!"

for i in [1,2,3]:
    print i, foo()

дает:

  1           0 LOAD_CONST               0 (<code object foo at 01A0B380, file "\foo\bar.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (foo)

  4           9 SETUP_LOOP              35 (to 47)
             12 LOAD_CONST               1 (1)
             15 LOAD_CONST               2 (2)
             18 LOAD_CONST               3 (3)
             21 BUILD_LIST               3
             24 GET_ITER
        >>   25 FOR_ITER                18 (to 46)
             28 STORE_NAME               1 (i)

  5          31 LOAD_NAME                1 (i)
             34 PRINT_ITEM
             35 LOAD_NAME                0 (foo)
             38 CALL_FUNCTION            0
             41 PRINT_ITEM
             42 PRINT_NEWLINE
             43 JUMP_ABSOLUTE           25
        >>   46 POP_BLOCK
        >>   47 LOAD_CONST               4 (None)
             50 RETURN_VALUE

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

Ответ 3

Скорость вашего кода может быть автоматически улучшена с помощью модуля Psyco.

Что касается Numpy, он, как правило, ускоряет работу с существенным фактором. Я считаю это обязательным при манипулировании численными массивами.

Вы также можете ускорить критическую часть вашего кода с помощью Cython или Pyrex, которые позволяют создавать более быстрые модули расширения без необходимости писать полноценный модуль расширения в C (что было бы более громоздким).

Ответ 4

Вот что интересно.

  • Структура данных

  • Алгоритм

Это принесет значительные улучшения.

Ваш список хорош для - в лучшем случае - нескольких однозначных улучшений производительности.

Вам нужно принципиально переосмыслить свои структуры данных, если вы хотите увидеть реальные улучшения скорости.

Ответ 5

Если вы уже знаете, что ваш алгоритм работает как можно быстрее, и вы знаете, что C будет намного быстрее, тогда вы можете реализовать ядро ​​своего кода на C как Расширение C на Python. Вы можете прагматично решить, какая часть кода находится в C и находится в Python, используя весь язык в полном объеме.

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

Ответ 6

Я автор Арауна. Я ничего не знаю о Python, но я знаю, что Arauna чрезвычайно оптимизирован как на высоком уровне (структуры данных и алгоритмы), так и на низкоуровневом (кешированный код, SIMD, многопоточность). Это тяжелая задача, чтобы пойти...