Чтобы измерить пиковую производительность 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 ГФЛОПС. Но программа возвращает 18.4GFLOPS, и я не могу понять, что не так. Может кто-нибудь показать мне?
В соответствии с Intel® Intrinsics Guide _mm_mul_ps
, _mm_add_ps
, _mm_sub_ps
иметь Throughput=1
для вашего CPUID 06_17
(как вы заметили).
В разных источниках я видел разные значения пропускной способности. В некоторых местах это было clock/instruction
в других это было наоборот (конечно, пока мы 1
— Это не имеет значения).
В соответствии с «Справочное руководство по оптимизации архитектур Intel® 64 и IA-32» значение Throughput
является:
Throughput
— Количество тактов, необходимых для ожидания, пока порты выдачи не смогут снова принять ту же инструкцию. Для многих инструкций пропускная способность команды может быть значительно меньше, чем ее задержка.
Согласно «С.3.2 Таблицы сносок»:
— Блок FP_ADD обрабатывает операции сложения и вычитания с плавающей точкой x87 и SIMD.
— Блок FP_MUL обрабатывает операции умножения с плавающей запятой x87 и SIMD.
То есть сложения / вычитания и умножения выполняются на разных исполнительных блоках.
FP_ADD
а также FP_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_ADD
а также FP_MUL
единицы.
Постскриптум Просто для проверки можно попробовать заменить все add
/sub
операции с mul
в Mandelbrot
функция или наоборот — и вы получите только ~ половину текущих FLOPS.
Других решений пока нет …