Python 用numpy中的标量乘以小矩阵的最有效方法

Python 用numpy中的标量乘以小矩阵的最有效方法,python,performance,numpy,matrix-multiplication,scalar,Python,Performance,Numpy,Matrix Multiplication,Scalar,我有一个程序,其主要性能瓶颈涉及将矩阵相乘,矩阵的一个维度为1,另一个维度较大,例如1000: large_dimension = 1000 a = np.random.random((1,)) b = np.random.random((1, large_dimension)) c = np.matmul(a, b) 换句话说,将矩阵b与标量a[0]相乘 我正在寻找最有效的计算方法,因为这个操作重复了数百万次 我测试了两种简单方法的性能,它们实际上是等效的: %timeit np.mat

我有一个程序,其主要性能瓶颈涉及将矩阵相乘,矩阵的一个维度为1,另一个维度较大,例如1000:

large_dimension = 1000

a = np.random.random((1,))
b = np.random.random((1, large_dimension))

c = np.matmul(a, b)
换句话说,将矩阵
b
与标量
a[0]
相乘

我正在寻找最有效的计算方法,因为这个操作重复了数百万次

我测试了两种简单方法的性能,它们实际上是等效的:

%timeit np.matmul(a, b)
>> 1.55 µs ± 45.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%timeit a[0] * b
>> 1.77 µs ± 34.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
有没有更有效的计算方法

  • 注意:我不能将这些计算移动到GPU,因为程序使用多处理,并且许多这样的计算是并行完成的

在这种情况下,使用元素乘法可能会更快,但您看到的时间主要是Numpy的开销(从CPython解释器调用C函数、包装/取消包装类型、进行检查、执行操作、数组分配等)

因为这个操作重复了数百万次

这就是问题所在。事实上,CPython解释器在处理低延迟的事情时非常糟糕。当您处理Numpy类型时尤其如此,因为调用C代码并检查琐碎的操作要比在纯Python中执行慢得多,而纯Python也比编译的原生C/C++代码慢得多。如果您确实需要这样做,并且无法使用Numpy对代码进行矢量化(因为有一个循环在时间步长上迭代),那么您就不再使用CPython,或者至少不再使用纯Python代码。相反,您可以使用NumbaCython来减轻执行C调用、包装类型等的影响。如果这还不够,那么您将需要编写本机C/C++代码(或任何类似的语言),除非您找到一个专门为您这样做的Python包。请注意,只有当Numba在本机类型或Numpy数组(包含本机类型)上工作时,它才是快速的。如果您使用许多纯Python类型,并且不想重写代码,那么您可以尝试使用PyPyJIT


以下是Numba中的一个简单示例,它避免了(昂贵的)创建/分配一个新阵列(以及许多Numpy内部检查和调用),该阵列是专门为解决您的特定情况而编写的:

@nb.njit('void(float64[::1],float64[:,::1],float64[:,::1]))
def fastMul(a、b、out):
val=a[0]
对于范围内的i(b.形状[1]):
out[0,i]=b[0,i]*val
res=np.empty(b.shape,dtype=b.dtype)
%timeit fastMul(a、b、res)
#每个回路397纳秒±0.587纳秒(7次运行的平均值±标准偏差,每个1000000个回路)
在撰写本文时,此解决方案比所有其他解决方案都要快。由于大部分时间都花在调用Numba和执行一些内部检查上,因此直接对包含迭代循环的函数使用Numba应该会产生更快的代码

import numpy as np
import numba

def matmult_numpy(matrix, c):
    return np.matmul(c, matrix)

@numba.jit(nopython=True)
def matmult_numba(matrix, c):
    return c*matrix

if __name__ == "__main__":
    large_dimension = 1000
    a = np.random.random((1, large_dimension))
    c = np.random.random((1,))
使用Numba大约有3倍的加速。Numba cognoscenti可以通过显式地将参数“c”转换为标量来做得更好

检查:检查结果

%timeit matmult\u numpy(a,c)
2.32µs每个回路±50纳秒(7次运行的平均±标准偏差,每个100000个回路)

%timeit matmult\u numba(a,c)
763纳秒每个回路±6.67纳秒(7次运行的平均±标准偏差,每个1000000个回路)

使用“仅浮动”

%timeit float(a[0]) * B
3.48 µs ± 26.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
要避免内存分配,请使用“缓冲区”

要避免不必要的属性获取,请使用“别名”

我也不建议使用numpy标量, 因为如果你避开它,计算速度会更快

a_float = float(a[0])

%timeit mul(a_float, B, buffer)
1.94 µs ± 5.74 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
此外,如果可能的话,可以初始化一次循环外缓冲区(当然,如果您有类似于循环:)

所以

“最佳迭代时间”=(1.91-0.02)/1000=>1.89(µs)


“加速比”=5.43/1.89=2.87

你能把你的a值打包成对角矩阵a,把你的b值打包成2d矩阵b,然后只做AB吗?如果你想让它更快,我想你必须重做任何让你达到这一点的东西。时间有什么不同吗?尝试第三次测试,使用
d=np.random.random(),
%timeit d*b
我发现这比
%timeit a[0]快33%*b
numpy
提供了
*
元素级乘法,并使用
广播处理维度混合效率
@
被添加以提供批处理的
,即
产品之和
。在可能的情况下,它将任务传递给fast BLAS(或相关)代码。它在这里工作是因为
求和
维度的大小为1。相对速度可能因您的
numpy
环境而异-您安装了哪些BLAS或相关库。谢谢。你能详细说明编辑“小”矩阵的问题标题吗?你认为什么是大矩阵?@ NurL没有阈值,但我认为1x1矩阵很小。对于可以在CPU缓存中拟合的向量也是如此。1000x1000大小的矩阵对我来说开始相当大了。然而,这有点主观。我只是补充了这一点,以强调矩阵足够小,因此管理矩阵对象的开销远远高于其实际计算。如果您不同意,请随意更改标题;)。非常好的贡献,看起来您确实传递了数据类型以获得更快的速度。但是,假设阵列创建
res
包含在计时中,则实际速度较慢。我只得到1.9美元用于numpy,0.73美元用于我发布的NUBA代码,0.56美元用于你的,1.17美元用于你的数组创建。@Mike谢谢。我对结果并不感到惊讶,因为在这个粒度上几乎所有事情都很重要,尤其是参数的数量。新面值
buffer = np.empty_like(B)

%timeit np.multiply(float(a[0]), B, buffer)
2.96 µs ± 37.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
mul = np.multiply

%timeit mul(float(a[0]), B, buffer)
2.73 µs ± 12.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
a_float = float(a[0])

%timeit mul(a_float, B, buffer)
1.94 µs ± 5.74 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
rng = range(1000)

%%timeit
for i in rng:
    pass
24.4 µs ± 1.21 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

%%timeit
for i in rng:
    mul(a_float, B, buffer)
1.91 ms ± 2.21 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)