首页 > 解决方案 > 与 CPU 微架构相关的奇怪现象

问题描述

我正在测试通过指针测量一个函数调用的成本,这是我的代码。但是,我发现了一些非常奇怪的事情并寻求您的帮助。

代码在VS2017的Release模式下编译,使用默认配置。

有 4 个测试平台,它们的所有操作系统都是 Win10。以下是一些详细信息:

在下图中,图例采用 形式machine parameter_order aliasmachine上面列出了。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 < 25LOOP > 100)?
  2. 为什么所有非执行时间在 时都有一个奇怪的峰值32 <= LOOP <= 41
  3. 为什么 Kaby Lake 机器(M1 和 M2)的 no-exec 时间和 exec 时间在 时具有不连续的间隔72 <= LOOP <= 94
  4. 为什么 M4(服务器处理器)与 M3(桌面处理器)相比差异更大?

这是我的测试结果:

图。1

图 2

图 3

图 4

为方便起见,我还在这里粘贴代码:

#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);
}

标签: x86cpuperformance-testingintelcpu-architecture

解决方案


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

当最里面的循环for(j = 0;j < LOOP;j += 1)停止正确预测其退出时,会出现第一个不连续性。在我的机器上,它发生在LOOP24 次迭代时。

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(桌面处理器)相比差异更大?

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

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

要更深入地了解方差来源以及如何诊断它们,请查看此答案


推荐阅读