Почему numba быстрее, чем numpy здесь?

Я не могу понять, почему numba здесь издает numpy (более 3x). Я сделал некоторые фундаментальные ошибки в том, как я сравниваю здесь? Кажется, идеальная ситуация для numpy, нет? Обратите внимание, что в качестве проверки я также использовал вариант, сочетающий numba и numpy (не показано), который, как и ожидалось, был таким же, как работает numpy без numba.

(btw это следующий вопрос: Самый быстрый способ численного обработки 2d-массива: dataframe vs series vs array vs numba)

import numpy as np
from numba import jit
nobs = 10000 

def proc_numpy(x,y,z):

   x = x*2 - ( y * 55 )      # these 4 lines represent use cases
   y = x + y*2               # where the processing time is mostly
   z = x + y + 99            # a function of, say, 50 to 200 lines
   z = z * ( z - .88 )       # of fairly simple numerical operations

   return z

@jit
def proc_numba(xx,yy,zz):
   for j in range(nobs):     # as pointed out by Llopis, this for loop 
      x, y = xx[j], yy[j]    # is not needed here.  it is here by 
                             # accident because in the original benchmarks 
      x = x*2 - ( y * 55 )   # I was doing data creation inside the function 
      y = x + y*2            # instead of passing it in as an array
      z = x + y + 99         # in any case, this redundant code seems to 
      z = z * ( z - .88 )    # have something to do with the code running
                             # faster.  without the redundant code, the 
      zz[j] = z              # numba and numpy functions are exactly the same.
   return zz

x = np.random.randn(nobs)
y = np.random.randn(nobs)
z = np.zeros(nobs)
res_numpy = proc_numpy(x,y,z)

z = np.zeros(nobs)
res_numba = proc_numba(x,y,z)

результаты:

In [356]: np.all( res_numpy == res_numba )
Out[356]: True

In [357]: %timeit proc_numpy(x,y,z)
10000 loops, best of 3: 105 µs per loop

In [358]: %timeit proc_numba(x,y,z)
10000 loops, best of 3: 28.6 µs per loop

Я запускал это на 2012 macbook air (13.3), стандартном распределении анаконды. Я могу предоставить более подробную информацию о моей настройке, если это имеет значение.

Ответ 1

Я думаю, что этот вопрос подчеркивает (несколько) ограничения на вызов прекомпилированных функций с языка более высокого уровня. Предположим, что в С++ вы пишете что-то вроде:

for (int i = 0; i != N; ++i) a[i] = b[i] + c[i] + 2 * d[i];

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

В python, однако, рассмотрите, что происходит: когда вы используете numpy, каждый '' + '' использует перегрузку оператора в типах массивов np (которые представляют собой лишь тонкие обертки вокруг смежных блоков памяти, то есть массивы в низком уровне) и вызывает функцию fortran (или С++), которая добавляет супер быстрый. Но он просто делает одно дополнение и выплевывает временное.

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

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

Ответ 2

Когда вы запрашиваете numpy:

x = x*2 - ( y * 55 )

Это внутренне переведено на что-то вроде:

tmp1 = y * 55
tmp2 = x * 2
tmp3 = tmp2 - tmp1
x = tmp3

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

Ответ 3

Numba обычно быстрее, чем Numpy и даже Cython (по крайней мере, в Linux).

Здесь сюжет (украден из Numba vs. Cython: Take 2): Benchmark on Numpy, Cython and Numba

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

Обратите внимание, что это может быть другим на других платформах, см. это для Winpython (From Учебник WinPython Cython):

Benchmark on Numpy, Cython and Numba with Winpython

Ответ 4

Вместо того, чтобы загромождать исходный вопрос дальше, я добавлю еще несколько вещей в ответ на Jeff, Jaime, Veedrac:

def proc_numpy2(x,y,z):
   np.subtract( np.multiply(x,2), np.multiply(y,55),out=x)
   np.add( x, np.multiply(y,2),out=y)
   np.add(x,np.add(y,99),out=z) 
   np.multiply(z,np.subtract(z,.88),out=z)
   return z

def proc_numpy3(x,y,z):
   x *= 2
   x -= y*55
   y *= 2
   y += x
   z = x + y
   z += 99
   z *= (z-.88) 
   return z

Моя машина, кажется, работает чуть быстрее сегодня, чем вчера, поэтому здесь они сравниваются с proc_numpy (proc_numba имеет то же значение, что и раньше)

In [611]: %timeit proc_numpy(x,y,z)
10000 loops, best of 3: 103 µs per loop

In [612]: %timeit proc_numpy2(x,y,z)
10000 loops, best of 3: 92.5 µs per loop

In [613]: %timeit proc_numpy3(x,y,z)
10000 loops, best of 3: 85.1 µs per loop

Обратите внимание, что когда я писал proc_numpy2/3, я начал видеть некоторые побочные эффекты, поэтому сделал копии x, y, z и передал копии вместо повторного использования x, y, z. Кроме того, разные функции иногда имели небольшие различия в точности, поэтому некоторые из них не прошли тесты на равенство, но если вы их различаете, они действительно близки. Я предполагаю, что это связано с созданием или (не созданием) временных переменных. Например:.

In [458]: (res_numpy2 - res_numba)[:12]
Out[458]: 
array([ -7.27595761e-12,   0.00000000e+00,   0.00000000e+00,
         0.00000000e+00,   0.00000000e+00,   0.00000000e+00,
         0.00000000e+00,   0.00000000e+00,   0.00000000e+00,
         0.00000000e+00,  -7.27595761e-12,   0.00000000e+00])

Кроме того, он довольно незначительный (около 10 мкс), но с использованием float-литералов (55. вместо 55) также сэкономит немного времени на numpy, но не поможет numba.