x86 - 与 CPU 微架构相关的奇怪现象
问题描述
我正在测试通过指针测量一个函数调用的成本,这是我的代码。但是,我发现了一些非常奇怪的事情并寻求您的帮助。
代码在VS2017的Release模式下编译,使用默认配置。
有 4 个测试平台,它们的所有操作系统都是 Win10。以下是一些详细信息:
- M1:CPU:i7-7700,微架构:Kaby Lake
- M2:CPU:i7-7700,微架构:Kaby Lake
- M3:CPU:i7-4790,微架构:Haswell
- M4:CPU:E5-2698 v3,微架构:Haswell
在下图中,图例采用 形式machine parameter_order alias
。machine
上面列出了。parameter_order
描述了LOOP
在单次运行期间传递给程序的顺序。alias
指示时间是哪个部分。no-exec
也就是没有函数调用部分。第 98-108 行。exec
表示调用函数部分,又名。第 115-125 行。per-exec
是函数调用的成本。所有时间单位都是毫秒。per-exec
指左 y 轴,而其他指右 y 轴。
比较图 1-图 4,您可以看到该图可能与 CPU 的微架构有关(M1 和 M2 相似,M3 和 M4 相似)。
我的问题:
- 为什么所有机器都有两个阶段(
LOOP < 25
和LOOP > 100
)? - 为什么所有非执行时间在 时都有一个奇怪的峰值
32 <= LOOP <= 41
? - 为什么 Kaby Lake 机器(M1 和 M2)的 no-exec 时间和 exec 时间在 时具有不连续的间隔
72 <= LOOP <= 94
? - 为什么 M4(服务器处理器)与 M3(桌面处理器)相比差异更大?
这是我的测试结果:
为方便起见,我还在这里粘贴代码:
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <algorithm>
#include <windows.h>
using namespace std;
const int PMAX = 11000000, ITER = 60000, RULE = 10000;
//const int LOOP = 10;
int func1(int a, int b, int c, int d, int e)
{
return 0;
}
int func2(int a, int b, int c, int d, int e)
{
return 0;
}
int func3(int a, int b, int c, int d, int e)
{
return 0;
}
int func4(int a, int b, int c, int d, int e)
{
return 0;
}
int func5(int a, int b, int c, int d, int e)
{
return 0;
}
int func6(int a, int b, int c, int d, int e)
{
return 0;
}
int (*init[6])(int, int, int, int, int) = {
func1,
func2,
func3,
func4,
func5,
func6
};
int (*pool[PMAX])(int, int, int, int, int);
LARGE_INTEGER freq;
void getTime(LARGE_INTEGER *res)
{
QueryPerformanceCounter(res);
}
double delta(LARGE_INTEGER begin_time, LARGE_INTEGER end_time)
{
return (end_time.QuadPart - begin_time.QuadPart) * 1000.0 / freq.QuadPart;
}
int main()
{
char path[100], tmp[100];
FILE *fin, *fout;
int cnt = 0;
int i, j, t, r;
int ans;
int LOOP;
LARGE_INTEGER begin_time, end_time;
double d1, d2, res;
for(i = 0;i < PMAX;i += 1)
pool[i] = init[i % 6];
QueryPerformanceFrequency(&freq);
printf("file path:");
scanf("%s", path);
fin = fopen(path, "r");
start:
if (fscanf(fin, "%d", &LOOP) == EOF)
goto end;
ans = 0;
getTime(&begin_time);
for(r = 0;r < RULE;r += 1)
{
for(t = 0;t < ITER;t += 1)
{
//ans ^= (pool[t])(0, 0, 0, 0, 0);
ans ^= pool[0](0, 0, 0, 0, 0);
ans = 0;
for(j = 0;j < LOOP;j += 1)
ans ^= j;
}
}
getTime(&end_time);
printf("%.10f\n", d1 = delta(begin_time, end_time));
printf("ans:%d\n", ans);
ans = 0;
getTime(&begin_time);
for(r = 0;r < RULE;r += 1)
{
for(t = 0;t < ITER;t += 1)
{
ans ^= (pool[t])(0, 0, 0, 0, 0);
ans ^= pool[0](0, 0, 0, 0, 0);
ans = 0;
for(j = 0;j < LOOP;j += 1)
ans ^= j;
}
}
getTime(&end_time);
printf("%.10f\n", d2 = delta(begin_time, end_time));
printf("ans:%d\n", ans);
printf("%.10f\n", res = (d2 - d1) / (1.0 * RULE * ITER));
sprintf(tmp, "%d.txt", cnt++);
fout = fopen(tmp, "w");
fprintf(fout, "%d,%.10f,%.10f,%.10f\n", LOOP, d1, d2, res);
fclose(fout);
goto start;
end:
fclose(fin);
system("pause");
exit(0);
}
解决方案
为什么所有机器都有两个阶段(LOOP < 25 和 LOOP > 100)?
当最里面的循环for(j = 0;j < LOOP;j += 1)
停止正确预测其退出时,会出现第一个不连续性。在我的机器上,它发生在LOOP
24 次迭代时。
perf stat -I3000
您可以通过将基准输出与性能统计信息交错来清楚地看到这一点:
BenchWithFixture/RandomTarget/21 727779 ns 727224 ns 3851 78.6834M items/s
45.003283831 2998.636997 task-clock (msec)
45.003283831 118 context-switches # 0.039 K/sec
45.003283831 0 cpu-migrations # 0.000 K/sec
45.003283831 0 page-faults # 0.000 K/sec
45.003283831 7,777,209,518 cycles # 2.595 GHz
45.003283831 26,846,680,371 instructions # 3.45 insn per cycle
45.003283831 6,711,087,432 branches # 2238.882 M/sec
45.003283831 1,962,643 branch-misses # 0.03% of all branches
BenchWithFixture/RandomTarget/22 751421 ns 750758 ns 3731 76.2169M items/s
48.003487573 2998.943341 task-clock (msec)
48.003487573 111 context-switches # 0.037 K/sec
48.003487573 0 cpu-migrations # 0.000 K/sec
48.003487573 0 page-faults # 0.000 K/sec
48.003487573 7,778,285,186 cycles # 2.595 GHz
48.003487573 26,956,175,646 instructions # 3.47 insn per cycle
48.003487573 6,738,461,171 branches # 2247.947 M/sec
48.003487573 1,973,024 branch-misses # 0.03% of all branches
BenchWithFixture/RandomTarget/23 774490 ns 773955 ns 3620 73.9325M items/s
51.003697814 2999.024360 task-clock (msec)
51.003697814 105 context-switches # 0.035 K/sec
51.003697814 0 cpu-migrations # 0.000 K/sec
51.003697814 0 page-faults # 0.000 K/sec
51.003697814 7,778,570,598 cycles # 2.595 GHz
51.003697814 21,547,027,451 instructions # 2.77 insn per cycle
51.003697814 5,386,175,806 branches # 1796.776 M/sec
51.003697814 72,207,066 branch-misses # 1.12% of all branches
BenchWithFixture/RandomTarget/24 1138919 ns 1138088 ns 2461 50.2777M items/s
57.004129981 2999.003582 task-clock (msec)
57.004129981 108 context-switches # 0.036 K/sec
57.004129981 0 cpu-migrations # 0.000 K/sec
57.004129981 0 page-faults # 0.000 K/sec
57.004129981 7,778,509,575 cycles # 2.595 GHz
57.004129981 19,061,717,197 instructions # 2.45 insn per cycle
57.004129981 4,765,017,648 branches # 1589.492 M/sec
57.004129981 103,398,285 branch-misses # 1.65% of all branches
BenchWithFixture/RandomTarget/25 1171572 ns 1170748 ns 2391 48.8751M items/s
60.004325775 2998.547350 task-clock (msec)
60.004325775 111 context-switches # 0.037 K/sec
60.004325775 0 cpu-migrations # 0.000 K/sec
60.004325775 0 page-faults # 0.000 K/sec
60.004325775 7,777,298,382 cycles # 2.594 GHz
60.004325775 17,008,954,992 instructions # 2.19 insn per cycle
60.004325775 4,251,656,734 branches # 1418.230 M/sec
60.004325775 131,311,948 branch-misses # 2.13% of all branches
过渡前,分支误判率约为 0.03%,然后在基准放缓时跃升至 2.13% 左右,或增加两个数量级。错误预测率实际上比您预期的要低一些:有 25 个分支(外循环还有更多),您可以预期1 / 25 == 4%
错误预测,但我们没有看到,不知道为什么。
在我的机器上,第一个循环(只有pool[0](0,0,0,0,0)
调用),就像你的一样,在大约 24LOOP
次迭代时没有转换,但为什么我不清楚。我的经验是,TAGE 计数器通常无法处理超过 24 个周期的恒定迭代循环,但这里可能与间接分支预测器有一些交互。这很有趣。
当 32 <= LOOP <= 41 时,为什么所有非执行时间都有一个奇怪的峰值?
我在当地也经历过。在我的测试中,这也是由于分支错误预测造成的:当时间激增时,错误预测也随之增加。同样,这里的预测如何工作(如此好)尚不清楚,但显然在这些值下,算法会出现预测失败。
当 72 <= LOOP <= 94 时,为什么 Kaby Lake 机器(M1 和 M2)的 no-exec 时间和 exec 时间具有不连续的间隔?
我经历了同样的事情:迭代 72 以 28M 循环/秒的速度运行,而迭代 73 仅以 20M 运行(随后的迭代也很慢)。同样,差异可以放在分支错误预测的脚下:从第 72 次迭代到第 73 次迭代,它们从 0.01% 增加到 1.35%。这几乎恰好是每次执行外部循环的一次错误预测,因此很可能是退出时的错误预测。
为什么 M4(服务器处理器)与 M3(桌面处理器)相比差异更大?
你的测试很长,所以会有很多变化来体验各种差异来源,比如中断、上下文切换、核心频率变化等等。
由于这是一种完全不同的硬件,也许是软件配置,因此看到不相等的差异也就不足为奇了。您可以减少外部迭代的数量以保持基准更短,并查看异常值的数量是否减少(但它们的大小会增加)。您还可以在外循环内移动计时,因此您正在计时一个较小的部分,并查看直方图以了解各种系统的结果如何分布在这个较小的时间间隔内。
要更深入地了解方差来源以及如何诊断它们,请查看此答案。
推荐阅读
- c# - EF Core 2、.NET CORE 2:如何使用 IQueryable 查询同一列的多个条件
? - json - 为什么我的 Gson 对象一直返回 null?
- matplotlib - 为 matplotlib 颜色图设置颜色限制
- javascript - 具有锚链接偏移量的到达路由器导航()?
- .htaccess - 允许用户使用 htaccess 访问管理部分
- windows - Redis 问题为 Windows 编译 ReJSON 模块
- r - R:如果字符串包含点,则 parse_number 失败
- python - 根据条件合并行(熊猫)
- css - 如何在 simple_form 中自定义提示?
- python - 禁止向 Python 子类添加新方法