为什么float32的numpy操作明显快于float64?

k2fxgqgv  于 2023-10-19  发布在  其他
关注(0)|答案(1)|浏览(147)

做了一些优化,我注意到从float64切换到float32通过一些numpy操作大大提高了运行时间。5x在下面的例子中。我知道数据类型的处理方式不同,并且具有硬件依赖性等(参见问题herehere)。但是为什么我会看到下面的内容,有什么办法可以让64位的速度更快吗?

import numpy as np
import timeit

arr64 = np.random.random(size=10000).astype(np.float64)
arr32 = np.random.random(size=10000).astype(np.float32)

time64 = timeit.timeit(lambda :np.exp(arr64), number=10000)
time32 = timeit.timeit(lambda :np.exp(arr32), number=10000)

print(f'64bit time: {time64}')
print(f'32bit time: {time32}')

64bit time: 0.797928056679666
32bit time: 0.15939769614487886

注意我在Ubuntu上使用python 3.9.17和numpy 1.25.2

ldioqlga

ldioqlga1#

TL;DR:关于您在机器上实际使用的目标数学库,有几种解释。在您的情况下,主要原因可能是它们的实现方式不同:Numpy作为自己的简单精度数字实现,而它调用标准数学库实现双精度数字。前者使用SIMD指令,而后者不使用。Numpy通常不会考虑性能,而是考虑兼容性**(因此可以为所有x86-64 CPU提供相同的通用包)。

首先,一些数学库使用SIMD指令(例如。SSE、AVX、 neon )同时计算多个浮点项。Intel SVML就是这样一个库。AFAIK,它可以被链接来加速像这样的Numpy操作。这些指令在 * 固定长度 * SIMD寄存器上运行(例如,SSE/ neon 为128位,AVX为256位)。问题是,双精度数字是两倍大,所以寄存器中的项目少了两倍。这意味着双精度数字的工作量要大两倍,导致执行速度慢两倍。
上述观点仅适用于目标数学库使用SIMD指令的情况。标量指令不受此问题影响。然而,双精度数字的延迟有时会比单精度数字高得多(因为双精度需要更多的晶体管,因为需要计算更多的位,晶体管之间的依赖链更长)。在最近的主流x86-64体系结构上,这两种类型对于 * 标量 * ADD/穆尔/FMA操作(但不是像SQRT这样的高级操作)同样快速。你可以检查here
Linux上的默认数学库是glibc。它默认使用基于查找表的 scalar 实现。由于查找表太小1,无法包含每个浮点值的结果,因此使用n阶多项式调整结果。双精度数非常精确,因此表需要更大2,多项式需要具有显著更高的阶数,并且具有双精度。虽然较大的查找表由于可能的缓存未命中而通常较慢,但我不认为这是一个重大问题,因为要计算的项目数量很大。高阶多项式可能是减速的原因。实际上,高阶多项式是使用一系列融合乘加(FMA)操作来计算的,从而产生依赖链。这种依赖关系链很重要,因为FMA的延迟通常已经相当“大”(在主流x86-64处理器上约为4个周期)。话虽如此,我不希望这会导致5倍的减速(正如我们在您的机器上看到的那样)。也许只有2x-3x,但不会更多。这意味着这可能不是主要问题。
在这一点上,我感到惊讶和好奇,它可能是什么,所以我剖析了我的Linux机器(有一个i5- 9600 KF CPU)上发生了什么。以下是结果:

  • 双精度计算比单精度计算慢4倍,所以我可以大致重现你的问题;
  • 在双精度计算期间,两个主要的昂贵函数是__ieee754_exp_fmaexp@@GLIBC_2.29。这表明Numpy在我的机器上使用了glibc(正如预期的那样);
  • 在简单精度计算中,99%的时间都花在了Numpy上,所以在这种情况下,glibc甚至没有用于计算指数!

事实证明Numpy显然有自己的exp实现,用于简单精度的数字!这意味着在这种情况下甚至不需要从另一个库调用外部函数(昂贵)。还有更多事实证明这个简单精度的Numpy实现实际上使用了SIMD指令,而glibc的一个没有。我通过分析处理器的性能计数器发现了这一点。在Numpy中,负责计算的主函数名为npy_exp(文件npy_math_internal.h.src调用它)。

单指令多数据流实现在简单精度上可以快得多。然而,由于上述两个因素,它们对于双精度来说没有那么快:每个SIMD寄存器的项目更少,需要更多的FMA指令来达到所要求的更高精度。只有当处理器提供宽SIMD寄存器和低延迟FMA配置时,双精度SIMD实施才值得3。这在十年前的大多数处理器上并不是最初的情况4:128位SSE是x86-64 CPU上的主要标准SIMD指令集(256位AVX指令集相当新),没有广泛接受的FMA指令集(因此延迟是穆尔+ADD之一),浮点运算的延迟略高。这可能就是为什么Numpy没有实现它,更不用说它需要一些时间来实现和维护它。如今,最新CPU支持的512位AVX-512指令集足够宽5,双精度实现可以使用SIMD指令。事实上,英特尔开发人员added such implementation directly in Numpy(见here)!这意味着,如果你在支持AVX-512的CPU上运行(例如,AMD Zen 4或Intel IceLake),那么效果应该明显不那么明显(仍然慢大约两倍)。如果你想要更快的双精度计算,我建议你尝试一个SIMD数学库,比如SVML。

**更新:**我在IceLake服务器上运行代码(使用AVX-512),如果Numpy构建正确,结果与预期的非常接近。话虽如此,我发现无论是标准Ubuntu包还是PIP似乎都无法在Numpy中启用AVX-512。事实上,生成的包非常低效,所以我从头开始重建Numpy以正确完成这项工作。双精度版本仅慢1.76倍。以下是结果:

Intel CoffeeLake (i5-9600KF) -- from standard debian packages:
    64bit time: 0.39326330599578796
    32bit time: 0.08715593699889723    (x4.51 faster)

Intel IceLake (Xeon 8375C) -- from standard Ubuntu packages:
    64bit time: 1.4964690230001452
    32bit time: 0.5068110490001345     (x2.95 faster)

Intel IceLake (Xeon 8375C) -- from PIP packages:
    64bit time: 0.9384758739997778
    32bit time: 0.550410964999628      (x1.85 faster)

Intel IceLake (Xeon 8375C) -- manual Numpy install enabling AVX-512:
    64bit time: 0.09678016599991679
    32bit time: 0.054961627000011504   (x1.76 faster)

请注意,尽管IceLake处理器的频率较低(IceLake Xeon的Turbo频率约为3.5 GHz,CoffeeLake的Turbo频率约为4.5 GHz),但IceLake的结果比CoffeeLake的结果更快(预期)。我建议你自己重新构建Numpy,以确保目标包有效地使用你的机器

脚注:

0:glibc有一个用于简单精度数字的SIMD实现,但看起来只有在提供-ffast-math的情况下才会被GCC调用,因此它可能不符合IEEE-754。
1:查找表不能太大,因为它将需要太多的存储器空间,并且还将导致昂贵的缓存未命中。
2:在最新版本的glibc中,查找表实际上是双精度数的4倍大(2**5=32 VS 2**7=128项)。
3:SIMD指令的延迟可以通过并发计算更多项来减轻,但这需要更多的SIMD寄存器,并且可用的数量有限(特别是在旧处理器上),更不用说许多人没有意识到这个延迟问题。
4:这是>15年前,因为人们保持他们的机器几年,Numpy的目标是在平均机器上实现良好的性能。
5:AVX-512还提供比SSE(x4)和AVX(x2)多得多的寄存器,因此现在实际上可以更容易地减轻延迟。

相关问题