Почему numpy.any настолько медленный на больших массивах?

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

Рассмотрим этот крайний случай:

first = np.zeros(1E3,dtype=np.bool)
last = np.zeros(1E3,dtype=np.bool)

first[0] = True
last[-1] = True

# test 1
%timeit np.any(first)
>>> 100000 loops, best of 3: 6.36 us per loop

# test 2
%timeit np.any(last)
>>> 100000 loops, best of 3: 6.95 us per loop

По крайней мере, np.any, кажется, делает что-то неопределенное здесь - если ненулевое значение является первым в массиве, не должно быть необходимости учитывать любые другие перед возвратом True, поэтому я ожидал бы, что тест 1 будет немного быстрее, чем тест 2.

Однако, что происходит, когда мы делаем массивы намного большими?

first = np.zeros(1E9,dtype=np.bool)
last = np.zeros(1E9,dtype=np.bool)

first[0] = True
last[-1] = True

# test 3
%timeit np.any(first)
>>> 10 loops, best of 3: 21.6 ms per loop

# test 4
%timeit np.any(last)
>>> 1 loops, best of 3: 739 ms per loop

Как и ожидалось, тест 4 довольно медленный, чем тест 3. Однако в тесте 3 np.any должен по-прежнему только проверять значение одного элемента в first, чтобы знать, что он содержит хотя бы одно ненулевое значение. Зачем, то, тест 3 настолько медленнее, чем тест 1?

Изменить 1:

Я использую версию для разработки Numpy (1.8.0.dev-e11cd9b), но я получаю точные результаты синхронизации с помощью Numpy 1.7.1. Я запускаю 64-разрядный Linux, Python 2.7.4. Моя система в основном работает на холостом ходу (я запускаю сеанс IPython, браузер и текстовый редактор), и я определенно не попаду на своп. Я также реплицировал результат на другой машине, работающей с Numpy 1.7.1.

Изменить 2:

Использование Numpy 1.6.2 Я получаю время ~ 1.85us для обоих тестов 1 и 3, так как jorgeca говорит, что, по-видимому, была некоторая регрессия производительности между Numpy 1.6.2 и 1.7.1 1.7.0 в этом отношении.

Изменить 3:

Следуя примеру J.F. Sebastian и jorgeca, я провел еще несколько тестов, используя np.all в массиве нулей, который должен быть эквивалентен вызову np.any в массиве, где первый элемент - один.

Тест script:

import timeit
import numpy as np
print 'Numpy v%s' %np.version.full_version
stmt = "np.all(x)"
for ii in xrange(10):
    setup = "import numpy as np; x = np.zeros(%d,dtype=np.bool)" %(10**ii)
    timer = timeit.Timer(stmt,setup)
    n,r = 1,3
    t = np.min(timer.repeat(r,n))
    while t < 0.2:
        n *= 10
        t = np.min(timer.repeat(r,n))
    t /= n
    if t < 1E-3:
        timestr = "%1.3f us" %(t*1E6)
    elif t < 1:
        timestr = "%1.3f ms" %(t*1E3)
    else:
        timestr = "%1.3f s" %t
    print "Array size: 1E%i, %i loops, best of %i: %s/loop" %(ii,n,r,timestr)

Результаты:

Numpy v1.6.2
Array size: 1E0, 1000000 loops, best of 3: 1.738 us/loop
Array size: 1E1, 1000000 loops, best of 3: 1.845 us/loop
Array size: 1E2, 1000000 loops, best of 3: 1.862 us/loop
Array size: 1E3, 1000000 loops, best of 3: 1.858 us/loop
Array size: 1E4, 1000000 loops, best of 3: 1.864 us/loop
Array size: 1E5, 1000000 loops, best of 3: 1.882 us/loop
Array size: 1E6, 1000000 loops, best of 3: 1.866 us/loop
Array size: 1E7, 1000000 loops, best of 3: 1.853 us/loop
Array size: 1E8, 1000000 loops, best of 3: 1.860 us/loop
Array size: 1E9, 1000000 loops, best of 3: 1.854 us/loop

Numpy v1.7.0
Array size: 1E0, 100000 loops, best of 3: 5.881 us/loop
Array size: 1E1, 100000 loops, best of 3: 5.831 us/loop
Array size: 1E2, 100000 loops, best of 3: 5.924 us/loop
Array size: 1E3, 100000 loops, best of 3: 5.864 us/loop
Array size: 1E4, 100000 loops, best of 3: 5.997 us/loop
Array size: 1E5, 100000 loops, best of 3: 6.979 us/loop
Array size: 1E6, 100000 loops, best of 3: 17.196 us/loop
Array size: 1E7, 10000 loops, best of 3: 116.162 us/loop
Array size: 1E8, 1000 loops, best of 3: 1.112 ms/loop
Array size: 1E9, 100 loops, best of 3: 11.061 ms/loop

Numpy v1.7.1
Array size: 1E0, 100000 loops, best of 3: 6.216 us/loop
Array size: 1E1, 100000 loops, best of 3: 6.257 us/loop
Array size: 1E2, 100000 loops, best of 3: 6.318 us/loop
Array size: 1E3, 100000 loops, best of 3: 6.247 us/loop
Array size: 1E4, 100000 loops, best of 3: 6.492 us/loop
Array size: 1E5, 100000 loops, best of 3: 7.406 us/loop
Array size: 1E6, 100000 loops, best of 3: 17.426 us/loop
Array size: 1E7, 10000 loops, best of 3: 115.946 us/loop
Array size: 1E8, 1000 loops, best of 3: 1.102 ms/loop
Array size: 1E9, 100 loops, best of 3: 10.987 ms/loop

Numpy v1.8.0.dev-e11cd9b
Array size: 1E0, 100000 loops, best of 3: 6.357 us/loop
Array size: 1E1, 100000 loops, best of 3: 6.399 us/loop
Array size: 1E2, 100000 loops, best of 3: 6.425 us/loop
Array size: 1E3, 100000 loops, best of 3: 6.397 us/loop
Array size: 1E4, 100000 loops, best of 3: 6.596 us/loop
Array size: 1E5, 100000 loops, best of 3: 7.569 us/loop
Array size: 1E6, 100000 loops, best of 3: 17.445 us/loop
Array size: 1E7, 10000 loops, best of 3: 115.109 us/loop
Array size: 1E8, 1000 loops, best of 3: 1.094 ms/loop
Array size: 1E9, 100 loops, best of 3: 10.840 ms/loop

Изменить 4:

После комментария seberg я попробовал тот же тест с массивом np.float32 вместо np.bool. В этом случае Numpy 1.6.2 также показывает замедление по мере увеличения размеров массива:

Numpy v1.6.2
Array size: 1E0, 100000 loops, best of 3: 3.503 us/loop
Array size: 1E1, 100000 loops, best of 3: 3.597 us/loop
Array size: 1E2, 100000 loops, best of 3: 3.742 us/loop
Array size: 1E3, 100000 loops, best of 3: 4.745 us/loop
Array size: 1E4, 100000 loops, best of 3: 14.533 us/loop
Array size: 1E5, 10000 loops, best of 3: 112.463 us/loop
Array size: 1E6, 1000 loops, best of 3: 1.101 ms/loop
Array size: 1E7, 100 loops, best of 3: 11.724 ms/loop
Array size: 1E8, 10 loops, best of 3: 116.924 ms/loop
Array size: 1E9, 1 loops, best of 3: 1.168 s/loop

Numpy v1.7.1
Array size: 1E0, 100000 loops, best of 3: 6.548 us/loop
Array size: 1E1, 100000 loops, best of 3: 6.546 us/loop
Array size: 1E2, 100000 loops, best of 3: 6.804 us/loop
Array size: 1E3, 100000 loops, best of 3: 7.784 us/loop
Array size: 1E4, 100000 loops, best of 3: 17.946 us/loop
Array size: 1E5, 10000 loops, best of 3: 117.235 us/loop
Array size: 1E6, 1000 loops, best of 3: 1.096 ms/loop
Array size: 1E7, 100 loops, best of 3: 12.328 ms/loop
Array size: 1E8, 10 loops, best of 3: 118.431 ms/loop
Array size: 1E9, 1 loops, best of 3: 1.172 s/loop

Почему это должно произойти? Как и в булевом случае, np.all должен по-прежнему только проверять первый элемент перед возвратом, поэтому время должно быть постоянным w.r.t. размер массива.

Ответ 1

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

Где найти обработку сокращения в исходных файлах Numpy

np.all(x) совпадает с x.all(). all() действительно вызывает np.core.umath.logical_and.reduce(x).

Если вы хотите выкопать источник numpy, я попытаюсь найти вас, если вы используете размер буфера/куска. Папка со всем кодом, который мы будем смотреть, это numpy/core/src/umath/.

PyUFunc_Reduce() в ufunc_object.c - это функция C, которая обрабатывает сокращение. В PyUFunc_Reduce() размер куска или буфера определяется путем поиска значения для сокращения в каком-либо глобальном словаре через функцию PyUFunc_GetPyValues ​​() (ufunc_object.c). На моей машине и компиляции из ветки разработки размер блока равен 8192. PyUFunc_ReduceWrapper() в reduce.c вызывается для установки итератора (с шагом, равным размеру блока), и он вызывает переданную в цикле функцию, которая is reduce_loop() в ufunc_object.c.

reduce_loop() в основном просто использует итератор и вызывает другую функцию innerloop() для каждого фрагмента. Функция innerloop находится в loops.c.src. Для логического массива и нашего случая из всех /logical _and подходящей функцией innerloop является BOOL_logical_and. Вы можете найти нужную функцию, выполнив поиск BOOLEAN LOOPS, а затем это вторая функция ниже (ее трудно найти из-за используемого здесь шаблона программирования). Там вы обнаружите, что короткое замыкание на самом деле выполняется для каждого блока.

Как изменить размер буфера, используемый в ufunctions (и, следовательно, в любом/все)

Вы можете получить размер блока/буфера с помощью np.getbuffersize(). Для меня это возвращает 8192 без ручной настройки, которая соответствует найденному мной, выписав размер буфера в коде. Вы можете использовать np.setbuffersize(), чтобы изменить размер куска.

Результаты с использованием большего размера буфера

Я изменил ваш тестовый код на следующее:

import timeit
import numpy as np
print 'Numpy v%s' %np.version.full_version
stmt = "np.all(x)"
for ii in xrange(9):
    setup = "import numpy as np; x = np.zeros(%d,dtype=np.bool); np.setbufsize(%d)" %(10**ii, max(8192, min(10**ii, 10**7)))
    timer = timeit.Timer(stmt,setup)
    n,r = 1,3
    t = np.min(timer.repeat(r,n))
    while t < 0.2:
        n *= 10
        t = np.min(timer.repeat(r,n))
    t /= n
    if t < 1E-3:
        timestr = "%1.3f us" %(t*1E6)
    elif t < 1:
        timestr = "%1.3f ms" %(t*1E3)
    else:
        timestr = "%1.3f s" %t
    print "Array size: 1E%i, %i loops, best of %i: %s/loop" %(ii,n,r,timestr)

Numpy не любит, чтобы размер буфера был слишком маленьким или слишком большим, поэтому я не делал его меньше 8192 или больше, чем 1E7, потому что Numpy не любил размер буфера 1E8. В противном случае я устанавливал размер буфера на размер обрабатываемого массива. Я только поднялся до 1E8, потому что на данный момент у моей машины всего 4 ГБ памяти. Вот результаты:

Numpy v1.8.0.dev-2a5c2c8
Array size: 1E0, 100000 loops, best of 3: 5.351 us/loop
Array size: 1E1, 100000 loops, best of 3: 5.390 us/loop
Array size: 1E2, 100000 loops, best of 3: 5.366 us/loop
Array size: 1E3, 100000 loops, best of 3: 5.360 us/loop
Array size: 1E4, 100000 loops, best of 3: 5.433 us/loop
Array size: 1E5, 100000 loops, best of 3: 5.400 us/loop
Array size: 1E6, 100000 loops, best of 3: 5.397 us/loop
Array size: 1E7, 100000 loops, best of 3: 5.381 us/loop
Array size: 1E8, 100000 loops, best of 3: 6.126 us/loop

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