Превышение теоретического пикового значения FLOPS

Чтобы измерить пиковую производительность процессора FLOPS, я написал небольшую программу на C++. Но измерения дают мне результаты, превышающие теоретические пиковые 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.

Процессор, на котором я это запустил, — Intel Core2Quad Q9450 с тактовой частотой 2,66 ГГц. Я скомпилировал код с помощью Visual Studio 2012 под Windows 7. Теоретический пик FLOPS должен быть 4 * 2,66 ГГц = 10,64 GFLOPS. Но программа возвращает 18,4 GFLOPS, и я не могу понять, что не так. Может кто-нибудь показать мне?


person Dominic Hofer    schedule 30.10.2013    source источник
comment
Остерегайтесь своего оптимизатора.   -  person SLaks    schedule 30.10.2013
comment
en.wikipedia.org/wiki/FLOPS#Computing   -  person Karoly Horvath    schedule 30.10.2013
comment
люди .hsc.edu/faculty-staff/robbk/Coms361/Lectures/ Slide 20–23 — слайды, соответствующие вашему вопросу.   -  person Zac Howland    schedule 30.10.2013


Ответы (1)


Согласно Руководству Intel® Intrinsics _mm_mul_ps, _mm_add_ps, _mm_sub_ps имеют Throughput=1 для вашего CPUID 06_17 (как вы заметили).

В разных источниках видел разные значения пропускной способности. Где-то было clock/instruction, где-то наоборот (разумеется, пока у нас 1 - не беда).

Согласно "Справочное руководство по оптимизации архитектур Intel® 64 и IA-32" определение Throughput:

Throughput — количество тактов, необходимое для ожидания, прежде чем порты выдачи смогут снова принять ту же инструкцию. Для многих инструкций пропускная способность инструкции может быть значительно меньше, чем ее задержка.

Согласно «Сноскам таблицы C.3.2»:

— Модуль FP_ADD обрабатывает операции сложения и вычитания с плавающей запятой x87 и SIMD.

— Модуль FP_MUL обрабатывает операции умножения с плавающей запятой x87 и SIMD.

т.е. сложения/вычитания и умножения выполняются на разных исполнительных устройствах.

Исполнительные блоки FP_ADD и FP_MUL подключены к разным Dispatch Ports (внизу рисунка):

Микроархитектура Intel Core (википедия)

Планировщик может отправлять инструкции нескольким портам каждый цикл.

Блоки выполнения умножения и сложения могут выполнять операции параллельно. Таким образом, теоретические 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,4 GFLOPS.


Функция _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_ADD, так и FP_MUL блоки.

P.S. Для пробы можно попробовать заменить все операции add/sub на mul в функции Mandelbrot или наоборот - и получится только ~половина текущих FLOPS.

person Evgeny Panasyuk    schedule 02.11.2013