是否有一种可编程的方法来估计CPU执行fp操作所需的时间

Is there a programmatic way to estimate the time my CPU takes to perform a fp operation?

本文关键字:执行 CPU fp 操作 时间 方法 是否 可编程 一种      更新时间:2023-10-16

这里的"fp运算"是指"浮点运算"。我正在开发一个Linux操作系统。是否有一个系统调用返回这个值作为一个静态度量,或者你可以用C/c++/其他语言的算法来测试这个?

edit:我应该提到这不是为了测试我的代码的效率。在一次采访中,有人问我一个理论算法需要运行多长时间。我必须计算出将执行多少个FLOPS,并将其乘以每个操作所需的时间,从而得出一个粗略的估计。我只是觉得这是个有趣的问题。

这几乎肯定不是一个有用的指标。还有很多其他因素影响代码效率——尤其是缓存命中/未命中。

话虽如此,在这个主题上有一个ServerFault线程,它链接到您可以使用的英特尔基准测试套件,但您应该意识到,您可能不会看到系统的最大FLOPS与应用程序性能之间的任何相关性。

您要使用的是Agner Fog的软件"用于测量时钟周期和性能监控的测试程序"。他用它来测量指令的延迟和吞吐量,从而生成他著名的指令表。它有很好的文档记录,包括可以在自己的代码中使用的设备驱动程序(以及如何安装它们的说明)。这是特别有用的,因为为了测量某些数量,如实际CPU频率,您需要访问特定于模型的寄存器(MSR),而用户级代码通常无法访问这些寄存器。

编辑:根据你对面试问题的编辑来估计运行一个浮点密集型运算需要多长时间,你可以使用这个公式:

time = efficiency * number_of_floating_point_operations / peak_flops.

可以从这个链接中找到许多处理器的每个内核的峰值flop -per-cycle

这些数量中的每一个都可能很难计算/估计,但效率是最困难的,因为它可能取决于许多因素,例如:

  1. 算法是计算受限还是内存受限?
  2. 该算法使用SIMD(例如SSE/AVX/FMA)的效果如何?
  3. 该算法如何使用MIMD(例如多核)?
  4. 你的实现如何使用不同的缓存级别?

为了更清楚地说明这一点,让我们考虑两个算法:矩阵乘法和点积。计算这两种算法的浮点运算次数很容易。矩阵乘法的浮点运算次数是2*n*n*n。点积是2*n

矩阵乘法如果做得对,它是计算界,可以充分受益于SIMD和MIMD。对于小n,它的效率开始很低,对于大n,它的效率稳定。我自己的实现达到了75%。英特尔MKL获得95%以上(但使用FMA不到90%)。

因此,对于大n的矩阵乘法时间的粗略估计是假设在给定 time = 2*n^3/peak_flops时效率为100%。

然而,对于点积,对于小n,效率将开始很高,对于大n,效率将下降到平台。这是因为它是内存限制。所以对于大n,效率是由读取内存的速度决定的。对于一台现代机器来说,大约是10gb/s。由于具有四核的现代桌面将具有超过100 GLOPS的峰值flops,并且浮点数为4或8字节,我估计在 time = 0.01*n/peak_flops 时,大型n的效率接近1%。我对此进行了测试(参见下面的代码)。我在我的系统上得到了大约2.2 GFLOPS,峰值为236 GFLOPS,所以这大约是峰值的1%。我的系统带宽大约是11gb/s。

大多数算法都是内存限制的,所以知道你的系统读取内存的速度(DDR3, DDR4,…)是估计时间最有用的指标之一。

所以一般来说,如果你知道一个算法的浮点运算次数和处理器的峰值flops,你应该问的第一件事是,对于大n,该算法是计算边界还是内存边界,然后对于时间的粗略估计,我会假设计算边界的效率是100%,对于内存边界,我会查找带宽来估计效率。

此代码从点积估计数据速率和GFLOPS。

    #include <time.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>
    #include <stdint.h>
    float dot(float *x, float *y, int n) {
        float sum = 0;
        for(int i=0; i<n; i++) {
            sum += x[i]*y[i];
        }
        return sum;
    }
    int main(){
        const int LEN = 1 << 28;
        float *x = new float[LEN];
        float *y = new float[LEN];
        for(int i=0; i<LEN; i++) { x[i] = 1.0*rand()/RAND_MAX - 0.5; y[i] = 1.0*rand()/RAND_MAX - 0.5;}
        uint32_t size = 2*sizeof(float)*LEN;
        clock_t time0 = clock();
        float sum = dot(x,y,LEN);
        clock_t time1 = clock();
        double dtime = (double)(time1 - time0) / CLOCKS_PER_SEC;
        double rate = 1.0*size/dtime*1E-9;
        double flops = 2.0*LEN/dtime*1E-9;
        printf("sum %f, dtime %f, rate %f, flops %fn", sum, dtime, rate,flops);
    }

试图确定FLOP"在真空中"所花费的时间并没有多大意义,因为有很多其他因素影响它(操作数是否在内存/缓存/寄存器中,它实际上是什么类型的操作,如果编译器发出x87/SSE/SSE2/…指令,是否涉及"奇怪"的IEEE754值,处理器管道是否被有效使用,代码是否分支预测器友好,…)。

你应该在你的算法的实际代码上使用一个分析器,看看什么是真正的瓶颈,以及在你的特定代码中在这些瓶颈上实际花费了多少时间。

有一种快速的方法,在执行某些操作之前和之后获取时间戳并将它们相减以查看消耗了多少时间。然而,这并不十分准确。

下面提供了来自我的一个基准测试的可变性的概念,该基准测试使用相同的汇编代码指令的长序列,试图填充管道,不同的测试使用可变数量的寄存器。这只是加法。

  Intel(R) Core(TM) i7-4820K CPU running at near 3.9 GHz
 Speeds adding to     1 Register  2 Registers  3 Registers  4 Registers
 32 bit Integer MIPS     4303         8553        11997        12294
 32 bit Float MFLOPS     1304         2608         3866         3866 SP
 64 bit Float MFLOPS     1304         2608         3866         3865 DP
 32 bit MMX Int MIPS     7824        14902        14936        14902
 32 bit SSE MFLOPS       5215        10431        15464        15463 SP
 64 bit SSE2 MFLOPS      2608         5216         7732         7731 DP
 32 bit SSE2 Int MIPS   15647        29803        29872        29803
 64 bit SSE2 Int MIPS    7823        14902        14936        14902