Производительность Julia по сравнению с Python + Numba LLVM/JIT-скомпилированным кодом

Показатели производительности для Julia I, которые я видел до сих пор, например, http://julialang.org/, сравнивают Julia с чистым Python или Python + NumPy, В отличие от NumPy, SciPy использует библиотеки BLAS и LAPACK, где мы получаем оптимальную многопоточную реализацию SIMD. Если предположить, что производительность Julia и Python одинакова при вызове функций BLAS и LAPACK (под капотом), как производительность Julia сравнивается с CPython при использовании Numba или NumbaPro для кода, который не вызывает функции BLAS или LAPACK?

Одна вещь, которую я замечаю, это то, что Julia использует LLVM v3.3, а Numba использует llvmlite, который построен на LLVM v3.5. Неужели Julia LLVM предотвращает оптимальную реализацию SIMD на новых архитектурах, таких как Intel Haswell (инструкции AVX2)?

Меня интересуют сравнения производительности как для кода спагетти, так и для небольших циклы DSP для обработки очень больших векторов. Последнее более эффективно обрабатывается процессором, чем GPU для меня из-за накладных расходов на перемещение данных в и из памяти устройства графического процессора. Меня интересует только производительность на одном процессоре Intel Core i7, поэтому производительность кластера для меня не важна. Особый интерес для меня представляет легкость и успех при создании параллельных реализаций функций DSP.

Вторая часть этого вопроса - сравнение Numba с NumbaPro (игнорирование MKL BLAS). Нужна ли NumbaPro target="parallel", учитывая новый аргумент nogil для декоратора @jit в Numba?

Ответ 1

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

Одна вещь, которую я замечаю, это то, что Julia использует LLVM v3.3, а Numba использует llvmlite, который построен на LLVM v3.5. Неужели Julia LLVM предотвращает оптимальную реализацию SIMD на новых архитектурах, таких как Intel Haswell (инструкции AVX2)?

[ 2017/01+: Информация ниже не относится к текущим выпускам Julia]

Julia отключает avx2 с LLVM 3.3, потому что на Haswell есть некоторые глубокие ошибки.

Julia построена с LLVM 3.3 для текущих выпусков и ночных часов, но вы можете построить с 3.5, 3.6 и обычно svn trunk (если мы еще не обновили некоторые изменения API в данный день, пожалуйста, зарегистрируйте проблему). Для этого установите LLVM_VER=svn (например) в Make.user, а затем перейдите к инструкциям сборки.

Ответ 2

См. здесь (раздел 4) для некоторых рецензируемых тестов, которые я лично работал. Сравнение было между Julia и PyPy.

Ответ 3

(Сравнение несравнимого всегда является двухсторонним мечом.

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


Вступление: (numba материал и [us] результаты немного ниже страницы)

При всем уважении официальный сайт представляет собой табличный набор тестов производительности, в котором указаны две категории фактов. Первый, связанный с тем, как был выполнен тест производительности (julia, используя LLVM скомпилированный код-исполнение v/s python, оставаясь выполненным с использованием GIL-интерпретированного кода). Во-вторых, насколько дольше другие языки используют для выполнения одной и той же "контрольной задачи", используя выполнение С-компилированного кода как относительную единицу времени = 1.0

Заголовок главы, выше таблицы с результатами, говорит (cit.:)

Высокопроизводительный компилятор JIT
Компилятор Just-in-time (JIT) Julias LLVM в сочетании с дизайном языков позволяет ему приближаться и часто соответствовать производительности C.

введите описание изображения здесь
Я подумал немного более строго, чтобы сравнить яблоки с яблоками и взял только одну из "контрольных задач", названных pi-sum.

Это было второе худшее время для интерпретируемого python, представленного , который выполнялся в 21,99 раза медленнее, чем LLVM/JIT-скомпилированный julia-код или скомпилированная альтернатива.

Итак, начался небольшой эксперимент.

@numba.jit( JulSUM, nogil = True )

Давайте начнем сравнивать яблоки с яблоками. Если сообщается, что код julia работает в 22 раза быстрее, пусть сначала измеряет простой интерпретируемый код кода на Python.

>>> def JulSUM():
...     sum = 0.
...     j   = 0
...     while j < 500:
...           j   += 1
...           sum  = 0.
...           k    = 0
...           while k < 10000:
...                 k   += 1
...                 sum += 1. / ( k * k )
...     return sum
...
>>> from zmq import Stopwatch
>>> aClk = Stopwatch()
>>> aClk.start();_=JulSUM();aClk.stop()
1271963L
1270088L
1279277L
1277371L
1279390L
1274231L

Итак, ядро ​​ pi-sum работает около 1.27x.xxx [us] ~ около 1,27 ~ 1,28 [s]

Учитывая строка таблицы для pi-sum в презентации языка на веб-сайт LLVM/JIT-powered julia выполнение кода должно выполняться примерно в 22 раза быстрее, то есть в ~ 57,92 [мс]

>>> 1274231 / 22
57919

Итак, пусть конвертирует апельсины в яблоки, используя numba.jit (v24.0)

>>> import numba
>>> JIT_JulSUM = numba.jit( JulSUM )
>>> aClk.start();_=JIT_JulSUM();aClk.stop()
1175206L
>>> aClk.start();_=JIT_JulSUM();aClk.stop()
35512L
37193L
37312L
35756L
34710L

Итак, после того, как JIT-компилятор выполнил задание, python numba-LLVM демонстрирует контрольные времена где-то около 34,7 ~ 37,3 [мс]

Можем ли мы пойти дальше?

Конечно, мы еще не выполнили большую настройку numba, в то время как пример кода настолько тривиален, что не ожидаются неожиданные достижения в будущем.

Во-первых, удалите здесь ненужный GIL-степпинг:

>>> JIT_NOGIL_JulSUM = numba.jit( JulSUM, nogil = True )
>>> aClk.start();_=JIT_NOGIL_JulSUM();aClk.stop()
85795L
>>> aClk.start();_=JIT_NOGIL_JulSUM();aClk.stop()
35526L
35509L
34720L
35906L
35506L

nogil=True
не приносит исполнение намного дальше, но все же бреет несколько [мс] больше, вождение всех результатов в ~ 35.9 [мс]

>>> JIT_NOGIL_NOPYTHON_JulSUM = numba.jit( JulSUM, nogil = True, nopython = True )
>>> aClk.start();_=JIT_NOGIL_NOPYTHON_JulSUM();aClk.stop()
84429L
>>> aClk.start();_=JIT_NOGIL_NOPYTHON_JulSUM();aClk.stop()
35779L
35753L
35515L
35758L
35585L
35859L

nopython=True
делает только окончательное прикосновение к полировке, чтобы получить все результаты последовательно в ~ 35,86 [мс] (против ~ 57,92 [мс ] для LLVM/JIT-julia)


Эпилог по обработке DSP:

Для вопроса OP о дополнительных преимуществах для ускоренной обработки DSP, можно попробовать и протестировать numba + Intel Python (через Anaconda), где Intel открыла новый горизонт в двоичных файлах, оптимизированный для внутренних процессов IA64-процессора, таким образом, выполнение кода может пользоваться дополнительными трюками, связанными с процессором, на основе знаний Intel об ILP4, векторизации и прогнозировании ветвлений, детали их собственных CPU-s экспонат во время выполнения. Стоит проверить, чтобы сравнить это (плюс один может пользоваться своим неразрушающим инструментом анализа кода, интегрированным в VisualStudio, где в реальном времени можно анализировать горячие точки с кодом для обработки кода in-vitro - вещь, которая будет просто инженером DSP, не он/она?