Почему `arr.take (idx)` быстрее чем `arr [idx]` - PullRequest
10 голосов
/ 12 марта 2019

Существует мнение, что использование np.take значительно быстрее индексации массивов. Например, http://wesmckinney.com/blog/numpy-indexing-peculiarities/, Быстрое индексирование фигурных чисел и Быстрое (er) индексирование фантазийных фигур и сокращение? . Есть также предположения, что np.ix_ лучше в некоторых случаях.

Я провел некоторое профилирование, и в большинстве случаев это действительно так, хотя разница уменьшается по мере увеличения массивов.
На производительность влияет размер массива, длина индекса (для строк) и количество занятых столбцов. Количество строк, кажется, оказывает наибольшее влияние, количество столбцов в массиве также оказывает влияние, даже если индекс равен 1D. Изменение размера индекса, похоже, не сильно влияет на методы.

Итак, вопрос в два раза: 1. Почему такая большая разница в производительности между методами? 2. Когда имеет смысл использовать один метод над другим? Существуют ли какие-либо типы массивов, упорядочения или формы, для которых всегда лучше работать?

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

Редактировать Я обновил ось Y на графиках, чтобы показать полный диапазон значений. Это делает более ясным, что разница меньше, чем она появилась для 1D данных.

1D индекс

Анализ времени выполнения в сравнении с количеством строк показывает, что индексация довольно последовательная, с небольшим восходящим трендом. take постоянно медленнее с увеличением числа строк. enter image description here

По мере увеличения числа столбцов оба становятся медленнее, но take имеет большее увеличение (и это все еще для одномерного индекса). enter image description here

2D индекс

С 2D данными результаты аналогичны. Использование ix_ также показано, и, похоже, имеет худшую производительность в целом. enter image description here

код для цифр

from pylab import *
import timeit


def get_test(M, T, C):
    """
    Returns an array and random sorted index into rows
    M : number of rows
    T : rows to take
    C : number of columns
    """
    arr = randn(M, C)
    idx = sort(randint(0, M, T))
    return arr, idx


def draw_time(call, N=10, V='M', T=1000, M=5000, C=300, **kwargs):
    """
    call : function to do indexing, accepts (arr, idx)
    N : number of times to run timeit
    V : string indicating to evaluate number of rows (M) or rows taken (T), or columns created(C)
    ** kwargs : passed to plot
    """
    pts = {
        'M': [10, 20, 50, 100, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, ],
        'T': [10, 50, 100, 500, 1000, 5000, 10000, 50000],
        'C': [5, 10, 20, 50, 100, 200, 500, 1000],
    }
    res = []

    kw = dict(T=T, M=M, C=C) ## Default values
    for v in pts[V]:
        kw[V] = v
        try:
            arr, idx = get_test(**kw)
        except CallerError:
            res.append(None)
        else:
            res.append(timeit.timeit(lambda :call(arr, idx), number=N))

    plot(pts[V], res, marker='x', **kwargs)
    xscale('log')
    ylabel('runtime [s]')

    if V == 'M':
        xlabel('size of array [rows]')
    elif V == 'T':
        xlabel('number of rows taken')
    elif V == 'C':
        xlabel('number of columns created')

funcs1D = {
    'fancy':lambda arr, idx: arr[idx],
    'take':lambda arr, idx: arr.take(idx, axis=0),
}

cidx = r_[1, 3, 7, 15, 29]
funcs2D = {
    'fancy2D':lambda arr, idx: arr[idx.reshape(-1, 1), cidx],
    'take2D':lambda arr, idx: arr.take(idx.reshape(-1, 1)*arr.shape[1] + cidx),
    'ix_':lambda arr, idx: arr[ix_(idx, cidx)],
}

def test(funcs, N=100, **kwargs):
    for descr, f in funcs.items():
        draw_time(f, label="{}".format(descr), N=100, **kwargs)
    legend()

figure()
title('1D index, 30 columns in data')
test(funcs1D, V='M')
ylim(0, 0.25)
# savefig('perf_1D_arraysize', C=30)

figure()
title('1D index, 5000 rows in data')
test(funcs1D, V='C', M=5000)
ylim(0, 0.07)
# savefig('perf_1D_numbercolumns')

figure()
title('2D index, 300 columns in data')
test(funcs2D, V='M')
ylim(0, 0.01)
# savefig('perf_2D_arraysize')

figure()
title('2D index, 30 columns in data')
test(funcs2D, V='M')
ylim(0, 0.01)
# savefig('perf_2D_arraysize_C30', C=30)

1 Ответ

3 голосов
/ 05 апреля 2019

Ответ очень низкий уровень, и он связан с оптимизацией компилятора C и кэша ЦП.Пожалуйста, посмотрите активное обсуждение с Себастьяном Бергом и Максом Болингброком (оба авторами numpy) этой проблемы numpy .

Необычная индексация пытается быть "умной" о том, как читается и пишется память (C-порядок против F-порядка), в то время как .take всегда будет держать C-порядок.Это означает, что сложное индексирование обычно будет намного быстрее для F-упорядоченных массивов и всегда должно быть быстрее в любом случае для огромных массивов.Теперь numpy решает, что такое «умный» способ без учета размера массива или конкретного оборудования, на котором он работает.Поэтому для меньших массивов выбор «неправильного» порядка памяти может на самом деле повысить производительность благодаря лучшему использованию операций чтения в кэш-памяти CPU.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...