问题:
我正在试图弄清楚如何编写代码(C优先,只有在没有其他解决方案时才支持ASM)在50%的情况下使分支预测错过。< / p>
所以它必须是一段代码“对于与分支相关的编译器优化”是“imune”,而且所有HW分支预测都不应该优于50%(抛硬币)。即使是更大的挑战,也可以在多CPU架构上运行代码,并获得相同的50%未命中率。
我设法在x86平台上编写了一个代码 47%分支未命中率。我怀疑失踪可能有3%来自:
我编写了自己的随机数生成器,以避免调用rand,而rand的实现可能隐藏了可预测的分支。它可以在可用时使用 rdrand 。延迟对我来说无关紧要。
问题:
代码:
#include <stdio.h>
#include <time.h>
#define RDRAND
#define LCG_A 1103515245
#define LCG_C 22345
#define LCG_M 2147483648
#define ULL64 unsigned long long
ULL64 generated;
ULL64 rand_lcg(ULL64 seed)
{
#ifdef RDRAND
ULL64 result = 0;
asm volatile ("rdrand %0;" : "=r" (result));
return result;
#else
return (LCG_A * seed + LCG_C) % LCG_M;
#endif
}
ULL64 rand_rec1()
{
generated = rand_lcg(generated) % 1024;
if (generated < 512)
return generated;
else return rand_rec1();
}
ULL64 rand_rec2()
{
generated = rand_lcg(generated) % 1024;
if (!(generated >= 512))
return generated;
else return rand_rec2();
}
#define BROP(num, sum) \
num = rand_lcg(generated); \
asm volatile("": : :"memory"); \
if (num % 2) \
sum += rand_rec1(); \
else \
sum -= rand_rec2();
#define BROP5(num, sum) BROP(num, sum) BROP(num, sum) BROP(num, sum) BROP(num, sum) BROP(num, sum)
#define BROP25(num, sum) BROP5(num, sum) BROP5(num, sum) BROP5(num, sum) BROP5(num, sum) BROP5(num, sum)
#define BROP100(num, sum) BROP25(num, sum) BROP25(num, sum) BROP25(num, sum) BROP25(num, sum)
int main()
{
int i = 0;
int iterations = 500000;
ULL64 num = 0;
ULL64 sum = 0;
generated = rand_lcg(0) % 54321;
for (i = 0; i < iterations; i++)
{
BROP100(num, sum);
// ... repeat the line above 10 times
}
printf("Sum = %llu\n", sum);
}
更新v1:
根据usr的建议,我通过在脚本中更改命令行中的LCG_C参数来生成各种模式。 我能够达到49.67%的BP错过。这对我的目的来说已经足够了,我有了在各种架构上产生这种方法的方法。
答案 0 :(得分:8)
如果你知道分支预测器是如何工作的,你可以得到100%的错误预测。只需每次都对预测器进行预期的预测,然后进行相反的操作。问题是我们不知道它是如何实施的。
我已经读过,典型的预测变量可以预测0,1,0,1
等模式。但我确信模式可以有多长时间。我的建议是尝试给定长度的每个模式(例如4)并查看哪一个最接近目标百分比。你应该能够同时针对50%和100%并且非常接近。需要对每个平台进行一次或在运行时进行此分析。
我怀疑分支总数的3%是在你说的系统代码中。内核不会在纯粹的CPU绑定用户代码上花费3%的开销。将调度优先级提高到最大值。
您可以通过生成一次随机数据并多次迭代相同数据来将RNG从游戏中移除。分支预测器不太可能检测到这一点(虽然它显然可以)。
我会通过填充bool[1 << 20]
来实现这一点,就像我描述的那样使用零模式。然后,您可以多次运行以下循环:
int sum0 = 0, sum1 = 0;
for (...) {
//unroll this a lot
if (array[i]) sum0++;
else sum1++;
}
//print both sums here to make sure the computation is not being optimized out
您需要检查反汇编以确保编译器没有做任何聪明的事情。
我不明白为什么你现在需要的复杂设置是必要的。 RNG可以解决问题,我不明白为什么需要这个简单的循环。如果编译器正在使用技巧,您可能需要将变量标记为volatile
,这使得编译器(更好:大多数编译器)将它们视为外部函数调用。
由于RNG现在已不再重要,因为它几乎从不被调用,您甚至可以调用操作系统的加密RNG来获取与任何人类无法区分的数字与真正的随机数。
答案 1 :(得分:3)
使用字节填充数组,并编写一个循环,根据字节的值检查每个字节和分支。
现在仔细检查处理器的架构及其分支预测。填充数组的初始字节,以便在检查它们之后,处理器处于可预测的已知状态。从该已知状态,您可以确定是否预测下一个分支。设置下一个字节,以便预测错误。再次,找出是否预测下一个分支,并设置下一个字节,以便预测错误,依此类推。
如果你也禁用中断(这可能会改变分支预测),你可以接近100%错误预测的分支。
作为一个简单的例子,在具有强/弱预测的旧PowerPC处理器上,在三个分支之后,它将始终处于“强烈采取”状态并且一个分支未被采用将其改变为“弱吸收”。如果你现在有一系列交替的未采取/采取的分支,那么预测总是错误的,并且在弱的不采取和弱采取之间切换。
这当然只适用于那个特定的处理器。大多数现代处理器都会看到该序列几乎100%可预测。例如,他们可能会使用两个独立的预测变量;一个用于案例“最后一个分支被采取”,一个用于案例“最后一个分支未被采取”。但是对于这样的处理器,不同的字节序列将给出相同的100%误预测率。
答案 2 :(得分:0)
避免编译器优化的最简单方法是在另一个转换单元中使用void f(void) { }
和void g(void) { }
虚函数,并禁用链接时优化。这将迫使if (*++p) f(); else g();
成为一个真正不可预测的分支,假设p
指向一组随机布尔值(这可以避开rand()
内的分支预测问题 - 只需在测量之前执行此操作)
如果for(;;)
循环给您带来问题,请输入goto
。
请注意&#34;循环展开技巧&#34;在评论中有点误导。您实际上创建了数千个分支。每个分支都将被单独预测,除了它们可能都不会被预测,因为CPU根本无法容纳数千个不同的预测。这可能会或可能不会对您的真正目标带来好处。