【发布时间】:2018-11-02 15:52:50
【问题描述】:
我正在测试通过指针测量一个函数调用的成本,这是我的code。但是,我发现了一些非常奇怪的东西并寻求您的帮助。
代码在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时所有的非执行时间都有一个奇怪的峰值? - 为什么在
72 <= LOOP <= 94时 Kaby Lake 机器(M1 和 M2)的 no-exec 时间和 exec 时间有不连续的间隔? - 为什么 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);
}
【问题讨论】:
-
你说你想通过指针来衡量一个函数调用的成本,但我不明白你是怎么做到的。
no function call part和function call part是什么意思?两者似乎都包含函数调用。这部分(d2 - d1) / (1.0 * RULE * ITER)计算什么?左y轴怎么可能是负数?LOOP有什么意义? M1和M2有什么区别? -
LOOP < 25是现代 Intel CPU 可以正确预测短循环的循环退出分支的地方。服用 20 次然后不服用一次是比服用 24 次但不服用一次更容易的模式。在我使用简单的嵌套循环进行的测试中,我发现内部迭代(外部迭代数百万)是完美分支预测的截止点。 -
您的散文似乎与代码不一致。您说“no-exec 表示没有函数调用部分,也就是第 98-108 行。exec 表示调用函数部分,也就是第 115-125 行。”,但据我所知,这两个时序循环要么涉及一个函数调用(第一个循环)到固定位置
pool[0],或者两个函数调用一个到pool[0],另一个调用pool[t]for t = 1, 2, 3, ... 等等。你说“per-exec”是函数调用的值,所以它是其他两个测试中哪一个的 LOOP 标准化值? -
顺便说一句,我对 OP 试图做的事情的印象是测量由其他一些可以与调用交错运行的工作参数化的(可能是随机的)间接调用的时间。我认为使用 xor 的关联和交换属性,编译器实际上可以将整个 xor 循环移出所有循环并计算一次,如果它知道 LOOP 没有转义函数。在我的测试中它并没有逃脱,但在 OP 中它确实是由于 scanf 造成的。不过编译器没那么聪明。
-
@BeeOnRope:哦,对,不是常数。但是there is a closed-form solution for
xor(1..n)。它不像n*(n-1)/2那样简单,但只有大约 9 条指令长 (godbolt.org/g/Lg8Ff6),如果编译器花时间寻找这种非常罕见的模式,那么它是值得使用的。 (他们没有而且可能不应该这样做!)
标签: x86 cpu performance-testing intel cpu-architecture