1

为了测量 CPU 的峰值 FLOPS 性能,我编写了一个小 C++ 程序。但是测量结果给我的结果比我的 CPU 的理论峰值 FLOPS 更大。怎么了?

这是我写的代码:

#include <iostream>
#include <mmintrin.h>
#include <math.h>
#include <chrono>

//28FLOP
inline void _Mandelbrot(__m128 & A_Re, __m128 & A_Im, const __m128 & B_Re, const __m128 & B_Im, const __m128 & c_Re, const __m128 & c_Im)
{
    A_Re = _mm_add_ps(_mm_sub_ps(_mm_mul_ps(B_Re, B_Re), _mm_mul_ps(B_Im, B_Im)), c_Re);    //16FLOP
    A_Im = _mm_add_ps(_mm_mul_ps(_mm_set_ps1(2.0f), _mm_mul_ps(B_Re, B_Im)), c_Im);         //12FLOP
}

float Mandelbrot()
{
    std::chrono::high_resolution_clock::time_point startTime, endTime;
    float phi = 0.0f;
    const float dphi = 0.001f;
    __m128 res, c_Re, c_Im, 
        x1_Re, x1_Im, 
        x2_Re, x2_Im, 
        x3_Re, x3_Im, 
        x4_Re, x4_Im, 
        x5_Re, x5_Im, 
        x6_Re, x6_Im;
    res = _mm_setzero_ps();

    startTime = std::chrono::high_resolution_clock::now();

    //168GFLOP
    for (int i = 0; i < 1000; ++i)
    {
        c_Re = _mm_setr_ps( -1.0f + 0.1f * std::sinf(phi + 0 * dphi),   //20FLOP
                            -1.0f + 0.1f * std::sinf(phi + 1 * dphi),
                            -1.0f + 0.1f * std::sinf(phi + 2 * dphi),
                            -1.0f + 0.1f * std::sinf(phi + 3 * dphi));
        c_Im = _mm_setr_ps(  0.0f + 0.1f * std::cosf(phi + 0 * dphi),   //20FLOP
                             0.0f + 0.1f * std::cosf(phi + 1 * dphi),
                             0.0f + 0.1f * std::cosf(phi + 2 * dphi),
                             0.0f + 0.1f * std::cosf(phi + 3 * dphi));
        x1_Re = _mm_set_ps1(-0.00f * dphi); x1_Im = _mm_setzero_ps();   //1FLOP
        x2_Re = _mm_set_ps1(-0.01f * dphi); x2_Im = _mm_setzero_ps();   //1FLOP
        x3_Re = _mm_set_ps1(-0.02f * dphi); x3_Im = _mm_setzero_ps();   //1FLOP
        x4_Re = _mm_set_ps1(-0.03f * dphi); x4_Im = _mm_setzero_ps();   //1FLOP
        x5_Re = _mm_set_ps1(-0.04f * dphi); x5_Im = _mm_setzero_ps();   //1FLOP
        x6_Re = _mm_set_ps1(-0.05f * dphi); x6_Im = _mm_setzero_ps();   //1FLOP

        //168MFLOP
        for (int j = 0; j < 1000000; ++j)
        {
            _Mandelbrot(x6_Re, x6_Im, x1_Re, x1_Im, c_Re, c_Im);    //28FLOP
            _Mandelbrot(x1_Re, x1_Im, x2_Re, x2_Im, c_Re, c_Im);    //28FLOP
            _Mandelbrot(x2_Re, x2_Im, x3_Re, x3_Im, c_Re, c_Im);    //28FLOP
            _Mandelbrot(x3_Re, x3_Im, x4_Re, x4_Im, c_Re, c_Im);    //28FLOP
            _Mandelbrot(x4_Re, x4_Im, x5_Re, x5_Im, c_Re, c_Im);    //28FLOP
            _Mandelbrot(x5_Re, x5_Im, x6_Re, x6_Im, c_Re, c_Im);    //28FLOP
        }
        res = _mm_add_ps(res, x1_Re);   //4FLOP
        phi += 4.0f * dphi;             //2FLOP
    }
    endTime = std::chrono::high_resolution_clock::now();

    if (res.m128_f32[1] + res.m128_f32[2] > res.m128_f32[3] + res.m128_f32[4]) //Prevent dead code removal
        return 168.0f / (static_cast<float>(std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count()) / 1000.0f);
    else
        return 168.1f / (static_cast<float>(std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count()) / 1000.0f);
}

int main()
{
    std::cout << Mandelbrot() << "GFLOP/s" << std::endl;
    return 0;
}

核心函数 _Mandelbrot 执行 4*_mm_mul_ps + 2*_mm_add_ps + 1*_mm_sub_ps,每个操作一次执行 4 个浮点数,因此 7 * 4FLOP = 28FLOP。

我运行它的 CPU 是 2.66GHz 的 Intel Core2Quad Q9450。我在 Windows 7 下用 Visual Studio 2012 编译了代码。理论上峰值 FLOPS 应该是 4 * 2.66GHz = 10.64GFLOPS。但是程序返回 18.4GFLOPS,我不知道出了什么问题。有人可以给我看吗?

4

1 回答 1

3

根据Intel® Intrinsics Guide _mm_mul_ps_mm_add_ps,_mm_sub_ps具有Throughput=1您的 CPUID 06_17(如您所述)。

在不同的来源中,我看到了不同的吞吐量含义。在某些地方它是clock/instruction,在其他地方它是相反的(当然,虽然我们有1- 没关系)。

根据“英特尔® 64 和 IA-32 架构优化参考手册”的定义Throughput是:

Throughput— 在发布端口可以再次自由地接受同一指令之前需要等待的时钟周期数。对于许多指令,一条指令的吞吐量可能远低于其延迟。

根据“C.3.2 表脚注”:

— FP_ADD 单元处理 x87 和 SIMD 浮点加减运算。

— FP_MUL 单元处理 x87 和 SIMD 浮点乘法运算。

即加法/减法和乘法在不同的执行单元上执行。

FP_ADDFP_MUL执行单元连接到不同的调度端口(在图片底部):

英特尔酷睿微架构(维基百科)

调度程序可以在每个周期将指令分派到多个端口。

乘法和加法执行单元可以并行执行操作。因此,处理器一个核心上的理论 GFLOPS 是:

sse_packet_size = 4
instructions_per_cycle = 2
clock_rate_ghz = 2.66
sse_packet_size * instructions_per_cycle * clock_rate_ghz = 21.28GFLOPS

因此,您的 18.4GFLOPS 正在接近理论峰值。


_Mandelbrot函数有 3 个 FP_ADD 指令和 3 个 FP_MUL 指令。正如您在函数中看到的那样,存在许多数据依赖性,因此指令不能有效地交错。即,为了给 FP_ADD 提供一些操作,FP_MUL 应该执行至少两个操作以产生 FP_ADD 所需的操作数。

但希望您的内部for循环有许多没有依赖关系的操作:

for (int j = 0; j < 1000000; ++j)
{
    _Mandelbrot(x6_Re, x6_Im, x1_Re, x1_Im, c_Re, c_Im); // 1
    _Mandelbrot(x1_Re, x1_Im, x2_Re, x2_Im, c_Re, c_Im); // 2
    _Mandelbrot(x2_Re, x2_Im, x3_Re, x3_Im, c_Re, c_Im); // 3
    _Mandelbrot(x3_Re, x3_Im, x4_Re, x4_Im, c_Re, c_Im); // 4
    _Mandelbrot(x4_Re, x4_Im, x5_Re, x5_Im, c_Re, c_Im); // 5
    _Mandelbrot(x5_Re, x5_Im, x6_Re, x6_Im, c_Re, c_Im); // 6
}

只有第六次操作取决于第一次的输出。所有其他操作的指令可以相互自由交错(通过编译器和处理器),这将允许FP_ADDFP_MUL单元保持忙碌。

PS 只是为了测试,你可以尝试用in函数替换所有add/sub操作,反之亦然 - 你只会得到当前 FLOPS 的大约一半。mulMandelbrot

于 2013-11-02T17:53:53.483 回答