Использование line_profiler с совмещенными функциями numba

Можно ли использовать line_profiler с Numba?

призвание %lprun на функции украшены @numba.jit возвращает пустой профиль:

Timer unit: 1e-06 s

Total time: 0 s
File: <ipython-input-29-486f0a3cdf73>
Function: conv at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           @numba.jit
     2                                           def conv(f, w):
     3                                               f_full = np.zeros(np.int(f.size + (2 * w.size) - 2), dtype=np.float64)
     4                                               for i in range(0, f_full.size):
     5                                                   if i >= w.size - 1 and i < w.size + f.size - 1:
     6                                                       f_full[i] = f[i - w.size + 1]
     7                                               w = w[::-1]
     8                                               g = np.zeros(f_full.size-w.size + 1, dtype=np.float64)
     9                                               for i in range(0, f_full.size - w.size):
    10                                                   g[i] = np.sum(np.multiply(f_full[i:i+w.size], w))
    11                                               return g

Есть обходной путь для кода Cython, но ничего не могу найти для Numba.

1 ответ

Решение

TL;DR: профилирование строки в функции numba может быть (технически) невозможно, но даже если было возможно профилировать функцию numba в виде линии, результаты могут быть неточными.

Проблема с профилировщиками и скомпилированными / оптимизированными языками

Сложно использовать профилировщики с "скомпилированными" языками (даже в некоторых случаях с некомпилированными языками, в зависимости от того, что разрешено делать во время выполнения), потому что компиляторам разрешено переписывать ваш код. Просто назвать несколько примеров: постоянное свертывание, вызовы встроенных функций, циклы развертывания (чтобы воспользоваться инструкциями SIMD), подъем и, как правило, переупорядочение / перестановка выражений (даже для нескольких строк). Как правило, компилятору разрешено делать что угодно, если результат и побочные эффекты "как будто", функция не была "оптимизирована".

Схема:

+---------------+       +-------------+      +----------+
|  Source file  |   ->  |  Optimizer  |  ->  |  Result  |
+---------------+       +-------------+      +----------+

Это проблема, потому что профилировщик должен вставлять операторы в код, например, профилировщик функции может вставлять оператор в начале и в начале каждой функции, что может работать, даже если код оптимизирован и функция встроена - просто потому, что "заявления профилировщика" также встроены. Однако что, если компилятор решит не включать функцию из-за дополнительных операторов профилировщика? Тогда то, что вы профилируете, может фактически отличаться от того, как будет работать "настоящая программа".

Например, если у вас есть (я использую Python здесь, хотя он не скомпилирован, просто предположим, что я написал такую ​​программу на C или около того):

 def give_me_ten():
     return 10

 def main():
     n = give_me_ten()
     ...

Тогда оптимизатор может переписать его так:

 def main():
     n = 10  # <-- inline the function

Однако, если вы вставите операторы профилирования:

 def give_me_ten():
     profile_start('give_me_ten')
     n = 10
     profile_end('give_me_ten')
     return n

 def main():
     profile_start('main')
     n = give_me_ten()
     ...
     profile_end('main')

Оптимизатор может просто выдавать тот же код, потому что он не встроен в функцию.

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

Например, если у вас была эта программа:

 def main():
     n = 1
     for _ in range(1000):
         n += 1
     ...

Оптимизатор может (не уверен, что любой компилятор сделает это) переписать его так:

 def main():
     n = 1001  # all statements are compile-time constants and no side-effects visible

Однако, если у вас есть операторы профилирования строк, то:

 def main():
     profile_start('main', line=1)
     n = 1
     profile_end('main', line=1)
     profile_start('main', line=2)
     for _ in range(1000):
         profile_end('main', line=2)
         profile_start('main', line=3)
         n += 1
         profile_end('main', line=3)
         profile_start('main', line=2)
     ...

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

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

В зависимости от языка, компилятора и профилировщика возможно смягчение этих эффектов. Но вряд ли Python-ориентированный профилировщик (например, line-profiler) предназначен для компиляторов C/C++.

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

Как это относится к Numba и Cython?

  • Cython переводит ваш код Python в код C (или C++), а затем использует компилятор C (или C++) для его компиляции. Схема:

    +-------------+    +--------+    +----------+    +-----------+    +--------+
    | Source file | -> | Cython | -> | C source | -> | Optimizer | -> | Result |
    +-------------+    +--------+    +----------+    +-----------+    +--------+
    
  • Numba переводит ваш код Python в зависимости от типов аргументов и использует LLVM для компиляции кода. Схема:

    +-------------+    +-------+    +------------------+    +--------+
    | Source file | -> | Numba | -> | LLVM / Optimizer | -> | Result |
    +-------------+    +-------+    +------------------+    +--------+
    

У обоих есть компилятор, который может делать обширные оптимизации. Многие оптимизации не будут возможны, если вы вставите операторы профилирования в ваш код перед его компиляцией. Таким образом, даже если бы можно было выполнить линейный профиль кода, результаты могут быть неточными (точными в том смысле, что настоящая программа будет работать таким образом).

Line-profiler был написан для чистого Python, поэтому я не обязательно доверял бы выводу для Cython/Numba, если бы он работал. Это может дать некоторые подсказки, но в целом это может быть слишком неточным.

Особенно Numba может быть очень хитрым, потому что переводчик numba должен будет поддерживать операторы профилирования (в противном случае вы получите функцию numba в объектном режиме, которая будет давать совершенно неточные результаты), а ваша функция с привязкой больше не является единственной функцией. Это на самом деле диспетчер, который делегирует "скрытую" функцию в зависимости от типа аргументов. Поэтому, когда вы называете тот же "диспетчер" с int или float он может выполнять совершенно другую функцию. Интересный факт: процесс профилирования с помощью профилировщика функций уже налагает значительные накладные расходы, потому что разработчики numba хотели выполнить эту работу (см. CProfile добавляет значительные накладные расходы при вызове функций jumb numba).

Хорошо, как их профилировать?

Вы, вероятно, должны профилировать с помощью профилировщика, который может работать с компилятором в переведенном коде. Они могут (вероятно) давать более точные результаты, чем профилировщик, написанный для кода Python. Это будет сложнее, потому что эти профилировщики будут возвращать результаты для переведенного кода, которые нужно будет снова вручную перенести в исходный код. Также это может быть даже невозможно - обычно Cython/Numba управляет переводом, компиляцией и выполнением результата, поэтому вам нужно проверить, предоставляют ли они хуки для дополнительного профилировщика. У меня нет опыта там.

И как общее правило: если у вас есть оптимизаторы, то всегда рассматривайте профилирование как "руководство", а не как "факт". И всегда используйте профилировщики, предназначенные для компилятора / оптимизатора, иначе вы потеряете много надежности и / или точности.

Другие вопросы по тегам