python - 为什么 `arr.take(idx)` 比 `arr[idx]` 快

标签 python numpy indexing micro-optimization

似乎普遍认为使用 np.take 比数组索引要快得多。例如http://wesmckinney.com/blog/numpy-indexing-peculiarities/ , Fast numpy fancy indexing , 和 Fast(er) numpy fancy indexing and reduction? .也有人建议 np.ix_ 在某些情况下更好。

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

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

有很多因素可能会影响性能,所以我在下面展示了其中的一些,并包含了用于尝试使其可重现的代码。

编辑 我更新了图中的 y 轴以显示完整的值范围。它更清楚地表明差异比 1D 数据看起来要小。

一维索引

查看运行时间与行数的比较表明索引非常一致,略有上升趋势。随着行数的增加,take 始终较慢。 enter image description here

随着列数的增加,两者都变慢了,但是 take 的增加幅度更大(这仍然是针对一维索引)。 enter image description here

二维索引

与 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)

最佳答案

答案是非常低级别,并且与 C 编译器和 CPU 缓存优化有关。请参阅与 Sebastian Berg 和 Max Bolingbroke(均为 numpy 的贡献者)就此进行的积极讨论 numpy issue .

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

关于python - 为什么 `arr.take(idx)` 比 `arr[idx]` 快,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55126938/

相关文章:

Python在按下空格键时创建新行

python - 使用for循环python遍历两个目录

java - 使用 Solr 索引文档导致预期的 MIME 类型应用程序/八位字节流但得到 text/html

python - 保留 TFIDF 结果以使用 Scikit for Python 预测新内容

python - 如何获得由 scipy.cluster.hierarchy 制作的树状图的子树

Python:If 语句和 Scrapy XPath 选择器

javascript - 将列索引转换为对应的列字母

numpy - 如何在 tensorflow 中打印完整(未截断)的张量?

python - 当我说 data.dtypes == np.object 时,其中 data 是我正在处理的数据集。这个表达是什么意思?

python - 每第 n 行 Pandas iloc 复杂切片