为什么numpy的einsum比numpy的内置函数更快?

Dan*_*iel 69 python arrays performance numpy multidimensional-array

让我们从三个数组开始dtype=np.double.使用numpy 1.7.1在intel CPU上执行计时,编译icc并链接到intel mkl.带有numpy 1.6.1的AMD cpu与gccwithout 编译mkl也用于验证时序.请注意,时序与系统大小几乎呈线性关系,并不是由于numpy函数if语句中产生的小开销,这些差异将以微秒而非毫秒显示:

arr_1D=np.arange(500,dtype=np.double)
large_arr_1D=np.arange(100000,dtype=np.double)
arr_2D=np.arange(500**2,dtype=np.double).reshape(500,500)
arr_3D=np.arange(500**3,dtype=np.double).reshape(500,500,500)
Run Code Online (Sandbox Code Playgroud)

首先让我们看一下这个np.sum函数:

np.all(np.sum(arr_3D)==np.einsum('ijk->',arr_3D))
True

%timeit np.sum(arr_3D)
10 loops, best of 3: 142 ms per loop

%timeit np.einsum('ijk->', arr_3D)
10 loops, best of 3: 70.2 ms per loop
Run Code Online (Sandbox Code Playgroud)

鲍尔斯:

np.allclose(arr_3D*arr_3D*arr_3D,np.einsum('ijk,ijk,ijk->ijk',arr_3D,arr_3D,arr_3D))
True

%timeit arr_3D*arr_3D*arr_3D
1 loops, best of 3: 1.32 s per loop

%timeit np.einsum('ijk,ijk,ijk->ijk', arr_3D, arr_3D, arr_3D)
1 loops, best of 3: 694 ms per loop
Run Code Online (Sandbox Code Playgroud)

外产品:

np.all(np.outer(arr_1D,arr_1D)==np.einsum('i,k->ik',arr_1D,arr_1D))
True

%timeit np.outer(arr_1D, arr_1D)
1000 loops, best of 3: 411 us per loop

%timeit np.einsum('i,k->ik', arr_1D, arr_1D)
1000 loops, best of 3: 245 us per loop
Run Code Online (Sandbox Code Playgroud)

所有这些都是两倍快np.einsum.这些应该是苹果与苹果的比较,因为一切都是具体的dtype=np.double.我希望在这样的操作中加速:

np.allclose(np.sum(arr_2D*arr_3D),np.einsum('ij,oij->',arr_2D,arr_3D))
True

%timeit np.sum(arr_2D*arr_3D)
1 loops, best of 3: 813 ms per loop

%timeit np.einsum('ij,oij->', arr_2D, arr_3D)
10 loops, best of 3: 85.1 ms per loop
Run Code Online (Sandbox Code Playgroud)

Einsum似乎是至少两倍快np.inner,np.outer,np.kron和,np.sum不管axes选择.主要的例外是np.dot 它从BLAS库调用DGEMM.那么为什么np.einsum其他numpy函数更快更快呢?

完整性的DGEMM案例:

np.allclose(np.dot(arr_2D,arr_2D),np.einsum('ij,jk',arr_2D,arr_2D))
True

%timeit np.einsum('ij,jk',arr_2D,arr_2D)
10 loops, best of 3: 56.1 ms per loop

%timeit np.dot(arr_2D,arr_2D)
100 loops, best of 3: 5.17 ms per loop
Run Code Online (Sandbox Code Playgroud)

领先的理论来自@sebergs注释,np.einsum可以使用SSE2,但numpy的ufuncs直到numpy 1.8才会出现(参见更改日志).我相信这是正确的答案,但都没有能够证实它.通过改变输入数组的类型和观察速度差异以及不是每个人都能在时间上观察到相同趋势的事实,可以找到一些有限的证据.

Joe*_*ton 31

首先,在numpy列表上有很多关于此的讨论.例如,参见: http://numpy-discussion.10968.n7.nabble.com/poor-performance-of-sum-with-sub-machine-word-integer-types-td41.html HTTP:// numpy- discussion.10968.n7.nabble.com/odd-performance-of-sum-td3332.html

有些归结einsum为新的事实,并且可能试图更好地了解缓存对齐和其他内存访问问题,而许多较旧的numpy函数专注于通过高度优化的简单易用的实现.不过,我只是在猜测.


但是,你正在做的一些事情并不是一个"苹果对苹果"的比较.

除了@Jamie已经说过的,还sum为数组使用了更合适的累加器

例如,sum更仔细地检查输入的类型并使用适当的累加器.例如,请考虑以下事项:

In [1]: x = 255 * np.ones(100, dtype=np.uint8)

In [2]: x
Out[2]:
array([255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
       255, 255, 255, 255, 255, 255, 255, 255, 255], dtype=uint8)
Run Code Online (Sandbox Code Playgroud)

请注意,这sum是正确的:

In [3]: x.sum()
Out[3]: 25500
Run Code Online (Sandbox Code Playgroud)

虽然einsum会给出错误的结果:

In [4]: np.einsum('i->', x)
Out[4]: 156
Run Code Online (Sandbox Code Playgroud)

但如果我们使用较少的限制dtype,我们仍然会得到您期望的结果:

In [5]: y = 255 * np.ones(100)

In [6]: np.einsum('i->', y)
Out[6]: 25500.0
Run Code Online (Sandbox Code Playgroud)

  • 我认同.毕竟,这首先是你在做什么.因此,我提出的观点可能毕竟不相关! (2认同)

Dan*_*iel 22

现在numpy 1.8已经发布,根据文档,所有ufunc都应该使用SSE2,我想仔细检查Seberg关于SSE2的评论是否有效.

为了执行测试,创建了一个新的python 2.7安装 - icc使用运行Ubuntu的AMD opteron核心上的标准选项编译了1.7和1.8 .

这是1.8升级之前和之后的测试运行:

import numpy as np
import timeit

arr_1D=np.arange(5000,dtype=np.double)
arr_2D=np.arange(500**2,dtype=np.double).reshape(500,500)
arr_3D=np.arange(500**3,dtype=np.double).reshape(500,500,500)

print 'Summation test:'
print timeit.timeit('np.sum(arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print timeit.timeit('np.einsum("ijk->", arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print '----------------------\n'


print 'Power test:'
print timeit.timeit('arr_3D*arr_3D*arr_3D',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print timeit.timeit('np.einsum("ijk,ijk,ijk->ijk", arr_3D, arr_3D, arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print '----------------------\n'


print 'Outer test:'
print timeit.timeit('np.outer(arr_1D, arr_1D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print timeit.timeit('np.einsum("i,k->ik", arr_1D, arr_1D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print '----------------------\n'


print 'Einsum test:'
print timeit.timeit('np.sum(arr_2D*arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print timeit.timeit('np.einsum("ij,oij->", arr_2D, arr_3D)',
                      'import numpy as np; from __main__ import arr_1D, arr_2D, arr_3D',
                      number=5)/5
print '----------------------\n'
Run Code Online (Sandbox Code Playgroud)

Numpy 1.7.1:

Summation test:
0.172988510132
0.0934836149216
----------------------

Power test:
1.93524689674
0.839519000053
----------------------

Outer test:
0.130380821228
0.121401786804
----------------------

Einsum test:
0.979052495956
0.126066613197
Run Code Online (Sandbox Code Playgroud)

Numpy 1.8:

Summation test:
0.116551589966
0.0920487880707
----------------------

Power test:
1.23683619499
0.815982818604
----------------------

Outer test:
0.131808176041
0.127472200394
----------------------

Einsum test:
0.781750011444
0.129271841049
Run Code Online (Sandbox Code Playgroud)

我认为这是相当确定的,SSE在时序差异中起着重要作用,应该注意的是,重复这些测试的时间只有~0.003s.其余的差异应该包含在这个问题的其他答案中.

  • 很棒的跟进!这是我需要更频繁地开始使用`einsum`的另一个原因.顺便说一句,在这种情况下,我认为你应该把你自己的答案标记为正确. (4认同)

Jai*_*ime 19

我认为这些时间解释了发生了什么:

a = np.arange(1000, dtype=np.double)
%timeit np.einsum('i->', a)
100000 loops, best of 3: 3.32 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 6.84 us per loop

a = np.arange(10000, dtype=np.double)
%timeit np.einsum('i->', a)
100000 loops, best of 3: 12.6 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 16.5 us per loop

a = np.arange(100000, dtype=np.double)
%timeit np.einsum('i->', a)
10000 loops, best of 3: 103 us per loop
%timeit np.sum(a)
10000 loops, best of 3: 109 us per loop
Run Code Online (Sandbox Code Playgroud)

所以,你打电话的时候基本上有一个几乎不变的3US开销np.sumnp.einsum,所以他们基本上跑得快,而是一个需要一点时间来走了.为什么会这样?我的钱如下:

a = np.arange(1000, dtype=object)
%timeit np.einsum('i->', a)
Traceback (most recent call last):
...
TypeError: invalid data type for einsum
%timeit np.sum(a)
10000 loops, best of 3: 20.3 us per loop
Run Code Online (Sandbox Code Playgroud)

不知道究竟是什么回事,但似乎np.einsum是跳过一些检查,以提取类型特定的功能做乘法和加法,并与直接进入*,并+为唯一标准的C类型.


多维案例没有区别:

n = 10; a = np.arange(n**3, dtype=np.double).reshape(n, n, n)
%timeit np.einsum('ijk->', a)
100000 loops, best of 3: 3.79 us per loop
%timeit np.sum(a)
100000 loops, best of 3: 7.33 us per loop

n = 100; a = np.arange(n**3, dtype=np.double).reshape(n, n, n)
%timeit np.einsum('ijk->', a)
1000 loops, best of 3: 1.2 ms per loop
%timeit np.sum(a)
1000 loops, best of 3: 1.23 ms per loop
Run Code Online (Sandbox Code Playgroud)

所以一个大致恒定的开销,而不是一旦他们开始运行就更快.

  • 你可能应该看到双精度(SSE)的2倍加速.因为sum是天真的(可能不是1.8+不确定),而einsum专门用于使用SIMD指令,大多数ufunc都没有. (8认同)
  • @seberg 你成功了,两个处理器都有 SSE2,所以人们期望单精度的速度是 4 倍,事实确实如此。如果你能写下来我会接受。 (2认同)

Nic*_*mer 9

numpy 1.21.2 的更新:Numpy 的本机函数在几乎所有情况下都比 einsum 更快。只有 einsum 的外部变体和 sum23 测试速度比非 einsum 版本更快。

如果您可以使用 numpy 的本机函数,请执行此操作。

(使用perfplot创建的图像,这是我的一个项目。)

在此输入图像描述

在此输入图像描述

在此输入图像描述

在此输入图像描述

在此输入图像描述

在此输入图像描述


重现绘图的代码:

import numpy
import perfplot


def setup1(n):
    return numpy.arange(n, dtype=numpy.double)


def setup2(n):
    return numpy.arange(n ** 2, dtype=numpy.double).reshape(n, n)


def setup3(n):
    return numpy.arange(n ** 3, dtype=numpy.double).reshape(n, n, n)


def setup23(n):
    return (
        numpy.arange(n ** 2, dtype=numpy.double).reshape(n, n),
        numpy.arange(n ** 3, dtype=numpy.double).reshape(n, n, n),
    )


def numpy_sum(a):
    return numpy.sum(a)


def einsum_sum(a):
    return numpy.einsum("ijk->", a)


perfplot.save(
    "sum.png",
    setup=setup3,
    kernels=[numpy_sum, einsum_sum],
    n_range=[2 ** k for k in range(10)],
)


def numpy_power(a):
    return a * a * a


def einsum_power(a):
    return numpy.einsum("ijk,ijk,ijk->ijk", a, a, a)


perfplot.save(
    "power.png",
    setup=setup3,
    kernels=[numpy_power, einsum_power],
    n_range=[2 ** k for k in range(9)],
)


def numpy_outer(a):
    return numpy.outer(a, a)


def einsum_outer(a):
    return numpy.einsum("i,k->ik", a, a)


perfplot.save(
    "outer.png",
    setup=setup1,
    kernels=[numpy_outer, einsum_outer],
    n_range=[2 ** k for k in range(13)],
)


def dgemm_numpy(a):
    return numpy.dot(a, a)


def dgemm_einsum(a):
    return numpy.einsum("ij,jk", a, a)


def dgemm_einsum_optimize(a):
    return numpy.einsum("ij,jk", a, a, optimize=True)


perfplot.save(
    "dgemm.png",
    setup=setup2,
    kernels=[dgemm_numpy, dgemm_einsum],
    n_range=[2 ** k for k in range(13)],
)


def dot_numpy(a):
    return numpy.dot(a, a)


def dot_einsum(a):
    return numpy.einsum("i,i->", a, a)


perfplot.save(
    "dot.png",
    setup=setup1,
    kernels=[dot_numpy, dot_einsum],
    n_range=[2 ** k for k in range(20)],
)


def sum23_numpy(data):
    a, b = data
    return numpy.sum(a * b)


def sum23_einsum(data):
    a, b = data
    return numpy.einsum("ij,oij->", a, b)


perfplot.save(
    "sum23.png",
    setup=setup23,
    kernels=[sum23_numpy, sum23_einsum],
    n_range=[2 ** k for k in range(10)],
)
Run Code Online (Sandbox Code Playgroud)