【问题标题】:Weird Phenomenon Related to CPU Microarchitecture与 CPU 微架构相关的奇怪现象
【发布时间】: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 相似)。

我的问题:

  1. 为什么所有机器都有两个阶段(LOOP < 25 和LOOP > 100)?
  2. 为什么32 <= LOOP <= 41 时所有的非执行时间都有一个奇怪的峰值?
  3. 为什么在72 <= LOOP <= 94 时 Kaby Lake 机器(M1 和 M2)的 no-exec 时间和 exec 时间有不连续的间隔?
  4. 为什么 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 &lt; 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


【解决方案1】:

为什么所有机器都有两个阶段(LOOP 100)?

当最内层循环for(j = 0;j &lt; 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) 调用)和你的一样,在 ~24 LOOP 迭代时没有转换,但我不清楚为什么。我的经验是,TAGE 计数器通常无法处理超过 24 个周期的恒定迭代循环,但这里可能与间接分支预测器有一些交互。挺有意思的。

当 32

我在本地也经历过。在我的测试中,这也是由于分支错误预测造成的:当时间激增时,错误预测也随之增加。同样,这里的预测如何工作(如此好)尚不清楚,但显然在这些值下,算法会出现预测失败。

当 72

我经历过同样的事情:迭代 72 以 28M 循环/秒的速度运行,而迭代 73 仅以 20M 运行(随后的迭代也很慢)。同样,差异可以放在分支错误预测的脚下:从第 72 次迭代到第 73 次迭代,它们从 0.01% 增加到 1.35%。这几乎是每次执行外部循环时发生的一次错误预测,因此很可能是退出时的错误预测。

为什么 M4(服务器处理器)与 M3(桌面处理器)相比差异更大?

你的测试很长,所以会有很多变化来体验各种差异来源,例如中断、上下文切换、核心频率变化等等。

由于这是一个完全不同的硬件配置,也许是软件配置,所以看到不相等的差异也就不足为奇了。您可以减少外部迭代的数量以保持基准更短,并查看异常值的数量是否减少(但它们的大小会增加)。您还可以将计时移到外循环内,这样您就可以对较小的部分进行计时,并查看直方图以了解各种系统的结果如何在这个较小的间隔内分布。

如需更深入地了解差异来源以及如何诊断它们,请have a look at this answer。

【讨论】:

    猜你喜欢
    • 2018-09-04
    • 2015-01-11
    • 2018-10-24
    • 2018-12-20
    • 1970-01-01
    • 2016-08-12
    • 2017-06-09
    • 1970-01-01
    相关资源
    最近更新 更多