当前位置: 首页 > 面试题库 >

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

许亦
2023-03-14
问题内容

通常,我从numpy的einsum函数获得了良好的性能(我喜欢它的语法)。@Ophion对这个问题的回答表明-在测试的情况下-
einsum始终优于“内置”功能(有时会稍微好一些,有时会很多)。但是我刚遇到einsum慢得多的情况。考虑以下等效功能:

(M, K) = (1000000, 20)
C = np.random.rand(K, K)
X = np.random.rand(M, K)

def func_dot(C, X):
    Y = X.dot(C)
    return np.sum(Y * X, axis=1)

def func_einsum(C, X):
    return np.einsum('ik,km,im->i', X, C, X)

def func_einsum2(C, X):
    # Like func_einsum but break it into two steps.
    A = np.einsum('ik,km', X, C)
    return np.einsum('ik,ik->i', A, X)

我期望func_einsum跑得最快,但这不是我遇到的。在具有超线程,numpy版本1.9.0.dev-7ae0206和带有OpenBLAS的多线程的四核CPU上运行,我得到以下结果:

In [2]: %time y1 = func_dot(C, X)
CPU times: user 320 ms, sys: 312 ms, total: 632 ms
Wall time: 209 ms
In [3]: %time y2 = func_einsum(C, X)
CPU times: user 844 ms, sys: 0 ns, total: 844 ms
Wall time: 842 ms
In [4]: %time y3 = func_einsum2(C, X)
CPU times: user 292 ms, sys: 44 ms, total: 336 ms
Wall time: 334 ms

当我增加到K200时,差异更加极端:

In [2]: %time y1= func_dot(C, X)
CPU times: user 4.5 s, sys: 1.02 s, total: 5.52 s
Wall time: 2.3 s
In [3]: %time y2= func_einsum(C, X)
CPU times: user 1min 16s, sys: 44 ms, total: 1min 16s
Wall time: 1min 16s
In [4]: %time y3 = func_einsum2(C, X)
CPU times: user 15.3 s, sys: 312 ms, total: 15.6 s
Wall time: 15.6 s

有人可以解释为什么einsum这么慢吗?

如果重要的话,这是我的numpy配置:

In [6]: np.show_config()
lapack_info:
    libraries = ['openblas']
    library_dirs = ['/usr/local/lib']
    language = f77
atlas_threads_info:
    libraries = ['openblas']
    library_dirs = ['/usr/local/lib']
    define_macros = [('ATLAS_WITHOUT_LAPACK', None)]
    language = c
    include_dirs = ['/usr/local/include']
blas_opt_info:
    libraries = ['openblas']
    library_dirs = ['/usr/local/lib']
    define_macros = [('ATLAS_INFO', '"\\"None\\""')]
    language = c
    include_dirs = ['/usr/local/include']
atlas_blas_threads_info:
    libraries = ['openblas']
    library_dirs = ['/usr/local/lib']
    define_macros = [('ATLAS_INFO', '"\\"None\\""')]
    language = c
    include_dirs = ['/usr/local/include']
lapack_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    define_macros = [('ATLAS_WITHOUT_LAPACK', None)]
    language = f77
    include_dirs = ['/usr/local/include']
lapack_mkl_info:
  NOT AVAILABLE
blas_mkl_info:
  NOT AVAILABLE
mkl_info:
  NOT AVAILABLE

问题答案:

您可以两全其美:

def func_dot_einsum(C, X):
    Y = X.dot(C)
    return np.einsum('ij,ij->i', Y, X)

在我的系统上:

In [7]: %timeit func_dot(C, X)
10 loops, best of 3: 31.1 ms per loop

In [8]: %timeit func_einsum(C, X)
10 loops, best of 3: 105 ms per loop

In [9]: %timeit func_einsum2(C, X)
10 loops, best of 3: 43.5 ms per loop

In [10]: %timeit func_dot_einsum(C, X)
10 loops, best of 3: 21 ms per loop

如果可用,请np.dot使用BLAS,MKL或您拥有的任何库。因此,np.dot几乎可以肯定,对的调用是多线程的。np.einsum有自己的循环,因此除了使用SIMD来加快原始C实现的速度外,不使用任何优化。

然后是一个多输入的einsum调用,运行速度要慢得多…
einsum的numpy源非常复杂,我还不完全了解。因此,请注意以下内容充其量只是推测性的,但这是我认为正在发生的事情…

当您运行时np.einsum('ij,ij->i', a, b),这样做的好处np.sum(a*b, axis=1)是避免了必须实例化具有所有产品的中间阵列,并在其上循环两次。因此,从低层次来看,情况是这样的:

for i in range(I):
    out[i] = 0
    for j in range(J):
        out[i] += a[i, j] * b[i, j]

现在说,您正在执行以下操作:

np.einsum('ij,jk,ik->i', a, b, c)

您可以执行与

np.sum(a[:, :, None] * b[None, :, :] * c[:, None, :], axis=(1, 2))

我认为einsum要做的是运行最后的代码,而不必实例化巨大的中间数组,这肯定会使事情变得更快:

In [29]: a, b, c = np.random.rand(3, 100, 100)

In [30]: %timeit np.einsum('ij,jk,ik->i', a, b, c)
100 loops, best of 3: 2.41 ms per loop

In [31]: %timeit np.sum(a[:, :, None] * b[None, :, :] * c[:, None, :], axis=(1, 2))
100 loops, best of 3: 12.3 ms per loop

但是,如果仔细看,摆脱中间存储可能是一件可怕的事情。这是我认为einsum正在做的底层工作:

for i in range(I):
    out[i] = 0
    for j in range(J):
        for k in range(K):
            out[i] += a[i, j] * b[j, k] * c[i, k]

但是您正在重复大量操作!如果您改为:

for i in range(I):
    out[i] = 0
    for j in range(J):
        temp = 0
        for k in range(K):
            temp += b[j, k] * c[i, k]
        out[i] += a[i, j] * temp

您将I * J * (K-1)减少乘法运算(和I * J额外的加法运算),并节省大量时间。我的猜测是,einsum不够聪明,无法在此级别优化事物。在源代码中有一个提示,它仅优化具有1个或2个操作数的操作,而不是3个。在任何情况下,为通用输入自动执行此操作似乎都不是一件简单的事情…



 类似资料:
  • 问题内容: 我正在努力确切地了解其工作原理。我看了一下文档和一些示例,但看起来似乎并不固定. 这是我们上课的例子: 对于两个数组A和B 我认为可以,但是我不确定(它正在正确处理其中之一的移调吗?)。谁能告诉我这里的实际情况(以及使用时的一般情况)? 问题答案: einsum是做什么的? 假设我们有两个多维数组,A和B。现在假设我们要… 乘 A用B一种特殊的方式来创造新的产品阵列; 然后也许 沿特定

  • 问题内容: 我不知道为什么numba在这里击败numpy(超过3倍)。我在这里进行基准测试时是否犯了一些根本性的错误?对于numpy来说似乎是完美的情况,不是吗?请注意,作为检查,我还运行了一个结合了numba和numpy的变体(未显示),正如预期的那样,它与不带numba的numpy运行相同。 (顺便说一下,这是一个后续问题:数字处理二维数组的最快方法:dataframe vs series v

  • 问题内容: 如果我有,这给了我,我怎么能回到原来的阵列? 问题答案: 简短而甜美,没有缓慢的Python循环。我们对除第一个元素()和除最后一个()以外的所有元素进行查看,并逐元素相减。该副本可确保我们减去原始元素值,而不是我们正在计算的值。(在NumPy 1.13及更高版本上 ,您可以跳过通话。)

  • 问题内容: 当我遇到性能问题时,我只是更改了一个正在编写的程序,以将数据存储为numpy数组,而两者之间的区别令人难以置信。最初耗时30分钟,而现在耗时2.5秒! 我想知道它是如何做到的。我认为这是因为它消除了对循环的需要,但除此之外,我感到很困惑。 问题答案: 块状阵列是均质类型的密集堆积阵列。相比之下,Python列表是指向对象的指针数组,即使它们都属于同一类型。因此,您可以获得引用局部性的好

  • 问题内容: 我有一个由基本数学函数(abs,cosh,sinh,exp,…)组合定义的函数。 我不知道是否有差别(速度)来使用,例如, 而不是? 问题答案: 计时结果如下: 比它还处理Numpy数组要慢:它包含提供这种灵活性的其他代码。 但是,Numpy在数组 上的 速度很快: (PS:在python 2.7中比慢于慢,后者快约30%,但仍然比NumPy慢得多。) 因此,对于1000个元素而言,花

  • 问题内容: 例如,尝试理解以下结果: 这里发生了什么?在[1]的情况下,它将1与x的每个元素进行比较,并将结果汇​​总到一个数组中。对于[[1]],同样的事情。仅通过对repl进行试验,就很容易弄清楚特定阵列形状会发生什么。但是,双方可以具有任意形状的基本规则是什么? 问题答案: NumPy会在比较之前尝试将两个数组广播为兼容形状。如果广播失败,则当前返回False。将来, 如果广播或元素比较等失