这是我的测试代码:
#include <chrono>
#include <iostream>
#include <cstdlib>
using namespace std;
using ll = long long;
int main()
{
__int128_t a, b;
ll x, y;
a = rand() + 10000000;
b = rand() % 50000;
auto t0 = chrono::steady_clock::now();
for (int i = 0; i < 100000000; i++)
{
a += b;
a /= b;
b *= a;
b -= a;
a %= b;
}
cout << chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now() - t0).count() << ' '
<< (ll)a % 100000 << '\n';
x = rand() + 10000000;
y = rand() % 50000;
t0 = chrono::steady_clock::now();
for (int i = 0; i < 100000000; i++)
{
x += y;
x /= y;
y *= x;
y -= x;
x %= y;
}
cout << chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now() - t0).count() << ' '
<< (ll)x % 100000 << '\n';
return 0;
}
字符串
测试结果如下:
$ g++ main.cpp -o main -O2
$ ./main
2432 1
2627 1
型
在x64 GNU/Linux上使用GCC 10.1.0,无论是使用-O2优化还是未优化,__int128_t
总是比long long
快一点。int
和double
都比long long
快得多; long long
已成为最慢的类型。
这是怎么发生的?
2条答案
按热度按时间nszi6y051#
性能差异来自GCC/Clang在此特定情况下的128位除法/模数**效率。
实际上,在我的系统以及godbolt、
sizeof(long long) = 8
和sizeof(__int128_t) = 16
上都是如此,因此前者的操作是由本机指令执行的,而后者不是(因为我们专注于64位平台)。加法,乘法和减法在__int128_t
上较慢。但是,16字节类型(x86 GCC/Clang上的__divti3
和__modti3
)的除法/取模内置函数比原生idiv
指令(至少在Intel处理器上相当慢)快得惊人。如果我们深入研究GCC/Clang内置函数的实现(此处仅用于
__int128_t
),我们可以看到__modti3
使用条件语句(在调用__udivmodti4
时)。英特尔处理器可以更快地执行代码,因为:div
指令 * 仍然在大多数可能的路径中使用 *(特别是在这种情况下);div
/idiv
指令的执行时间占总执行时间的大部分,因为它们的延迟非常高**。div
/idiv
指令不能并行执行,因为循环依赖性。但是,div
的 * 延迟低于idiv
*,使得前者更快。请注意,两种实现的性能可能因架构而异(由于CPU端口的数量、分支预测能力和
idiv
指令的延迟/吞吐量)。实际上,例如,latency of a 64-bitidiv
instruction在Skylake上需要41-95个周期,而在AMD Ryzen处理器上需要8-41个周期。div
的延迟分别约为6-这意味着基准性能测试结果在Ryzen处理器上应该会有很大的不同(在128位的情况下,由于额外的指令/分支成本,可能会看到相反的效果)。0ejtzxu12#
TL:DR:
__int128
除法帮助函数在内部执行无符号div reg64
(在值为正且上半部分为0
的一些分支之后)。在Ice Lake之前的Intel CPU上,64-位div
比GCC为有符号long long
内联的有符号idiv reg64
快。快得足以弥补帮助程序的所有额外开销函数,以及其他操作的扩展精度。你可能不会在AMD CPU上看到这种效果:
long long
会像预期的那样更快,因为idiv r64
在性能上与div r64
足够相似。即使在较旧的Intel CPU上,
unsigned long long
也比unsigned __int128
快。例如,在我的3.9GHz的i7- 6700 k(Skylake)上(在perf stat
下运行以确保测试期间的CPU频率):div
与idiv
。此外,从这样一个非常具体的微基准测试中得出任何“一般”的结论是一个坏主意。不过,深入研究为什么扩展精度
__int128
类型在这个除法基准测试中能够更快,因为正数小到足以容纳32位整数。(有趣的事实:如果数字适合32位,clang与一些调优选项使代码使用32位除法,例如如果dividend|divisor
的高半部分为无符号全零。这在旧的Intel上是值得的,但不应该在AMD上,或英特尔自冰湖。)你的基准测试非常重视除法,每次迭代都要做两次(
/
和%
),尽管它比其他操作要昂贵得多,而且在大多数代码中使用得很少(例如,对整个数组求和,然后除以一次以获得平均值。您的基准测试也没有配置级别的并行性:每一步都有一个对前一步的数据依赖性,这可以防止自动向量化或任何显示较窄类型的优点的东西,或者更宽CPU管道的好处,比如桤木Lake的每周期6 uops与Skylake的(或Core 2的)4 uops。最近的AMD中的类似宽度,如6 uops / clock in Zen 3 and 4,以及Zen 1和2中的5指令或6 uops的限制,以较少者为准。
(It也不小心避免预热效果,比如第一个定时区域在CPU达到最大turbo. Idiomatic way of performance evaluation?之前会变慢。但这比你的定时区域的几秒钟要快得多,所以这不是问题。)
128-位整数除法(特别是有符号的)对于GCC来说太复杂了,所以gcc发出一个对助手函数
__divti3
或__modti3
的调用。(TI = tetra-integer,GCC的内部名称,表示大小为int
的4倍的整数。)这些函数在GCC-internals manual中有文档记录。你可以在the Godbolt compiler-explorer上看到编译器生成的asm。即128位加法与add/decode,乘法与一个
mul
低半部分的全乘,以及叉积的2x非扩展imul
。是的,这些比int64_t
的单指令等效慢。但是Godbolt没有向你展示libgcc帮助函数的asm,它甚至在“编译到二进制”和反汇编模式下也没有反汇编它们(而不是通常的编译器asm文本输出),因为它动态链接libgcc_s而不是
libgcc.a
。扩展精度的有符号除法是通过在必要时取反和对64位块进行无符号除法来完成的,然后在必要时修复结果的符号。
当两个输入都是小的和正的时候,不需要实际的求反(只需要测试和分支)。**对于小的数字也有快速的路径(高半除数= 0,商将适合64位),这里就是这种情况。**最终的结果是通过
__divti3
的执行路径看起来像这样:这是在我的Arch GNU/Linux系统上使用gcc-libs 10.1.0-2使用
g++ -g -O3 int128-bench.cpp -o int128-bench.O3
编译后,使用gdb手动单步执行对__divti3
的调用。字符串
Intel CPUs (since IvyBridge) have zero-latency
mov
(除了Ice Lake),所以所有这些开销不会显著恶化关键路径延迟(这是您的瓶颈)。或者至少不足以弥补idiv
和div
之间的差异。分支由分支预测和推测执行处理,只有当实际输入寄存器值相同时才检查预测。分支每次都以相同的方式进行,因此分支预测学习起来很简单。由于除法非常慢,因此有足够的时间让无序的exec赶上。
64-位操作数大小的整数除法在Intel CPU上非常慢,即使数字实际上很小,适合32位整数,并且用于有符号整数除法的额外微码甚至更昂贵。
例如,在我的Skylake(i7- 6700 k)上,https://uops.info/显示(table search result)
*
idiv r64
是前端的56 uops,延迟从41到95周期(从除数到商,我认为这是相关的情况)。*
div r64
是前端的33 uop,延迟从35到87周期。(对于相同的延迟路径)。延迟最好的情况发生在小的股息或小的股息或什么,我永远不记得。
类似于GCC在软件中对128位除法进行64位除法的分支,我认为CPU微码在内部进行64位除法,操作范围更窄,可能是32位,只有10个uop用于有符号或无符号,延迟更低。(Ice Lake改进了除法器,因此64位除法并不比32位慢多少。)
这就是为什么你发现
long long
比int
慢得多的原因。在很多情况下,如果涉及内存带宽或SIMD,它的速度差不多,或者是一半。(每128位向量宽度只有2个元素,而不是4个)。**AMD CPU更有效地处理64位操作数大小,性能仅取决于实际值,因此对于具有相同数字的div r32与div r64,性能大致相同。
顺便说一句,实际值往往是
a=1814246614 / b=1814246613
= 1,然后是a=1 % b=1814246612
(每次迭代b
减1)。只有商=1的除法测试看起来很愚蠢。(第一次迭代可能不同,但我们在第二次及以后进入这种状态。)除了除法之外,整数运算的性能在现代CPU上并不依赖于数据。(当然,除非有 * 编译时 * 常量允许发出不同的asm。就像除以一个常量,当在编译时计算一个乘法逆时,成本要低得多。)
回复:
double
:请参阅浮点除法与浮点乘法,了解除法与乘法。FP除法通常很难避免,而且它的性能在更多情况下都是相关的,所以它处理得更好。相关:
div r64
更改为div r32
,吞吐量提高了约3倍。