为什么numba切片比numpy切片快得多?

trnvg8h3  于 2023-08-05  发布在  其他
关注(0)|答案(1)|浏览(100)
def test(x):
    k = x[1:2]
    l = x[0:3]
    m = x[0:1]
    
@njit
def test2(x):
    k = x[1:2]
    l = x[0:3]
    m = x[0:1]

x = np.arange(5)    

test2(x)

%timeit test(x)
%timeit test2(x)

776 ns ± 1.83 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
280 ns ± 2.53 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

字符串
它们之间差距随着切片的增加而变宽

def test(x):
    k = x[1:2]
    l = x[0:3]
    m = x[0:1]
    n = x[1:3]
    o = x[2:3]
    
@njit
def test2(x):
    k = x[1:2]
    l = x[0:3]
    m = x[0:1]
    n = x[1:3]
    o = x[2:3]
    
test2(x)

%timeit test(x)
%timeit test2(x)
1.18 µs ± 1.82 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
279 ns ± 0.562 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)


numpy函数似乎变得线性变慢,而numba函数并不关心你对它进行了多少次切片(这是我在两种情况下都期望发生的)
编辑:
在chrslg的回答之后我决定放一个return语句给这两个函数。两个都输入

return k,l,m,n,o


时间是:

1.23 µs ± 2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
1.61 µs ± 9.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)


所以numba函数现在变慢了,这似乎使它实际上只是一个死代码。然而,在看到用户Jared的评论后,我决定用他尝试的切片来测试相同的产品操作:

def test5(x):
    k = x[1:2]
    l = x[0:3]
    m = x[0:1]
    n = x[1:4]
    o = x[2:3]
    return (k*l*m*n*o)
    
@njit
def test6(x):
    k = x[1:2]
    l = x[0:3]
    m = x[0:1]
    n = x[1:4]
    o = x[2:3]
    return (k*l*m*n*o)
    
test6(x)

%timeit test5(x)
%timeit test6(x)
5.79 µs ± 202 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
787 ns ± 1.52 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)


现在numba函数比简单的return函数(!)并且速度差距再次扩大。老实说,我现在更困惑了。

j13ufse2

j13ufse21#

因为它可能什么都不做。
我得到了相同的计时,对于numba,对于这个函数

def test3(x):
    pass

字符串
请注意,test几乎什么也不做。这些只是切片,没有任何与之相关的操作。没有数据传输或任何东西。只是创建3个变量,和一些边界调整。
如果代码是一个包含5000000个元素的数组,并从中取出1000000个元素的切片,它不会慢。因此,我想,当你想在“更大”的情况下扩展一些东西时,你决定不增加数据大小(因为你可能知道数据大小在这里不相关),而是增加行数。
但是,test,即使几乎什么都不做,仍然在做这3个未使用的片。
其中as numba编译一些生成的C代码。而编译器,有了优化器,没有理由保留那些以后永远不会使用的切片变量。
我在这里完全是推测(我从来没有见过numba生成的代码)。但我想代码可以看起来像行

void test2(double *x, int xstride, int xn){
    double *k = x+1*xstride;
    int kstride=xstride;
    int kn=1;
    double *l=x;
    int lstride=xstride;
    int ln=3;
    double *m=x;
    int mstride=xstride;
    int mn=1;
    // And then it would be possible, from there to iterates those slices
    // for example k[:]=m[:] could generate this code
    // for(int i=0; i<kn; i++) k[i*stride] = m[i*stride]
}


(Here我在double *算法中使用stride的大小,而实际上步幅是以字节为单位的,但这并不重要,这只是伪代码)
我的观点是,如果之后有什么(就像我在注解中所说的),那么,这段代码,即使只是一些算术运算,仍然是“几乎没有,但不是没有”。
但之后什么都没有。所以,它只是一些局部变量的初始化,与代码显然没有副作用。编译器优化器很容易删除所有这些代码。并编译一个空函数,其效果和结果完全相同。
所以,再一次,这只是我的猜测。但是任何像样的代码生成器+编译器都应该为test2编译一个空函数。所以test2test3是一样的。
虽然解释器不这样做,但通常这种优化(首先,很难提前知道将要发生什么,其次,优化所花费的时间是在运行时,所以对于编译器来说,即使花费1小时的编译时间来节省1 ns的运行时间,也是值得的)

编辑:更多实验

jared和我都有这样的想法,那就是做一些事情,不管是什么,强迫切片存在,并比较numba在必须做一些事情时发生的事情,因此真正做切片,是自然的。问题是,一旦你开始做某件事,任何事情,切片本身的时间就变得可以忽略不计。因为切片不算什么。
但是,好吧,从统计学上讲,你可以去掉它,仍然以某种方式测量“切片”部分。
下面是一些计时数据

空函数

在我的计算机上,一个空函数在纯python中花费130 ns。和540 ns与numba。
这并不奇怪。什么都不做,但是在跨越“python/C边界”的同时这样做可能会花费一点,只是为了那个“python/C”。不多tho

时间vs切片数

下一个实验是你做的那个实验(因为,顺便说一句,你的帖子包含了我的答案的证明:你已经看到,在纯Python中,时间是O(n),n是切片的数量,而在numba中,时间是O(1)。仅这一点就证明根本没有切片发生。如果切片完成,在numba中,就像在任何其他非量子计算机中一样:D,成本必须是O(n)。当然,如果t=100+0.000001*n,那么很难区分O(n)和O(1)。因此,我开始评估“空”的情况
在纯Python中,只切片,随着切片数量的增加,显然在O(n)中,确实:
x1c 0d1x的数据
线性回归表明,这大约是138+n×274,单位为ns。
这与“空”时间一致
另一方面,对于numba来说,我们得到



所以不需要用线性回归来证明
1.它确实是O(1)
1.时序与540 ns的“空”情况一致
请注意,这意味着,对于n=2或更多的切片,在我的计算机上,numba变得有竞争力。以前,它不是。但是,好吧,在“无所事事”的竞争中……

使用slice

当然,如果我们后来添加代码来强制使用切片,情况就会改变。编译器不能只是删除切片。
但我们得小心点

  1. O(n)的加法运算
    1.为了区分操作的时序和可能可以忽略的切片的时序
    然后我做的是计算一些加法slice1[1]+slice2[2]+slice3[3]+...
    但是不管切片的数量是多少,我在这个加法中有1000个术语。因此,对于n=2(2个切片),该加法是slice1[1]+slice2[2]+slice1[3]+slice2[4]+...,具有1000项。
    这应该有助于消除由于加法而导致的O(n)部分。然后,有了足够大的数据,我们可以从周围的变化中提取一些值,即使这些变化在加法时间本身之前(因此甚至在该加法时间的噪声之前)是相当微不足道的。但有了足够的测量,噪音就变得足够低,可以开始看到东西)
    在python中



线性回归得到199000 + 279× nns

我们从中学到的是,我的实验设置是好的。279与之前的274足够接近,可以说,实际上,加法部分,尽管它很大(200000 ns),但时间复杂度为O(1),因为与切片相比,O(n)部分保持不变。所以我们只有和之前一样的时间+一个很大的加法部分的常数。

使用numba

这一切都只是为了证明实验设置的合理性。现在有趣的部分来了,实验本身



线性回归得到1147 + 1.3×n
因此,这里的时间复杂度为O(n)。
结论
切伦巴确实要花点钱。时间复杂度O(n)但是如果不使用它,编译器只会删除它,我们得到的是O(1)操作。
证明原因确实是,在您的版本中,numba代码只是什么都不做
2.不管是什么,你对切片所做的强制使用它的操作的成本,并防止编译器只是删除它,要大得多,这在没有统计预防措施的情况下,掩盖了O(n)部分。因此,“当我们使用变量时,情况是一样的”。
3.不管怎么说,numba在大多数时候都比numpy快。
我的意思是,numpy是一个很好的方式来拥有“编译语言速度”,而不使用编译语言。但它并没有击败真实的的编译。因此,在numba中使用一个简单的算法来击败numpy中非常聪明的向量化是非常经典的。(古典的,而且对像我这样的人来说非常令人失望,他以成为一个知道谁在numpy中矢量化事物的人为生。有时候,我觉得用numba,最天真的嵌套for循环更好)。
它停止是如此,tho,当

  1. Numpy使用多个核心(你也可以用numba来做。但不只是用天真的算法)
    1.你正在做的是一个非常聪明的算法存在的操作。Numpy算法有几十年的优化。不能用3个嵌套循环来击败它。只是有些任务太简单了,无法真正优化。
    所以,我还是更喜欢numpy而不是numba。我更喜欢使用numpy背后数十年的优化,重新发明numba中的轮子。另外,有时最好不要依赖编译器。
    但是,好吧,这是经典的numba击败numpy。
    只是,不符合你的案件比例。因为在你的例子中,你在比较(我想我现在已经证明了,而且你已经证明了自己,通过看到numpy case是O(n)而numba case是O(1)),“用numpy做切片vs用numba什么都不做”

相关问题