Python 为什么'arr.take(idx)`比'arr[idx]快`

Python 为什么'arr.take(idx)`比'arr[idx]快`,python,numpy,indexing,micro-optimization,Python,Numpy,Indexing,Micro Optimization,似乎大家都认为使用np.take比数组索引要快得多。例如,和。还有人建议,在某些情况下,np.ix更好 我已经做了一些分析,在大多数情况下似乎都是这样,尽管随着阵列变大,差异会减小。 性能受数组大小、索引长度(对于行)和所采用的列数的影响。行数的影响似乎最大,数组中的列数也有影响,即使索引是1D。改变索引的大小似乎对两种方法之间的关系影响不大 因此,问题有两个方面: 1.为什么方法之间的性能有如此大的差异? 2.什么时候使用一种方法而不是另一种方法有意义?是否有一些数组类型、顺序或形状可以更好地

似乎大家都认为使用
np.take
比数组索引要快得多。例如,和。还有人建议,在某些情况下,
np.ix
更好

我已经做了一些分析,在大多数情况下似乎都是这样,尽管随着阵列变大,差异会减小。
性能受数组大小、索引长度(对于行)和所采用的列数的影响。行数的影响似乎最大,数组中的列数也有影响,即使索引是1D。改变索引的大小似乎对两种方法之间的关系影响不大

因此,问题有两个方面: 1.为什么方法之间的性能有如此大的差异? 2.什么时候使用一种方法而不是另一种方法有意义?是否有一些数组类型、顺序或形状可以更好地使用

有很多事情可能会影响性能,所以我在下面展示了其中的一些,并包括了用于尝试使其可复制的代码

编辑我已更新了绘图上的y轴,以显示完整的值范围。这更清楚地表明,该差异小于1D数据的差异

一维索引 通过比较运行时间和行数,可以看出索引是非常一致的,有轻微的上升趋势<代码>获取始终随着行数的增加而变慢。

随着列数的增加,两者都会变慢,但
take
的增加幅度更大(这仍然适用于1D索引)。

二维索引 对于二维数据,结果类似。还显示了使用
ix
,总体性能似乎最差。

数字代码 答案是非常低级的,与C编译器和CPU缓存优化有关。请参阅与塞巴斯蒂安·伯格和马克斯·博林布鲁克(都是numpy的撰稿人)就此进行的积极讨论


花式索引试图“聪明”地了解内存的读写方式(C顺序与F顺序),而
.take
将始终保持C顺序。这意味着对于F-有序数组,奇特的索引通常要快得多,而对于大型数组,在任何情况下都应该更快。现在,numpy决定什么是“智能”方式,而不考虑阵列的大小,也不考虑它运行的特定硬件。因此,对于较小的阵列,由于在CPU缓存中更好地使用读取,选择“错误”的内存顺序实际上可能会获得更好的性能。

take
可能更快,但从我在SO问题(以及
numpy
函数)中看到的情况来看,它的使用频率不如“普通”索引。感谢链接,我得花点时间读一遍。从那里的一个链接来看,
take
这个想法似乎更适合熊猫优化,这让我感觉更好,因为忽略它并不总是正确的民间智慧。
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)