Почему мое приложение не может достичь максимальной производительности FP ядра Core i7 920 - PullRequest
3 голосов
/ 29 февраля 2012

У меня есть вопрос о пиковой производительности FP моего ядра i7 920. У меня есть приложение, которое выполняет много операций MAC (в основном, операцию свертки), и я не могу достичь пиковой производительности FP процессора в ~ 8 раз при использовании многопоточности и инструкций SSE. Когда я попытался выяснить причину этого, я получил упрощенный фрагмент кода, работающий в одном потоке и не использующий инструкции SSE, которые работают одинаково плохо:

for(i=0; i<49335264; i++)
{
    data[i] += other_data[i] * other_data2[i];
}

Если я прав (все массивы data и other_data - FP), этот фрагмент кода требует:

49335264 * 2 = 98670528 FLOPs

Это выполняется за ~ 150 мс (я очень уверен, что это время правильно, поскольку таймеры C и Intel VTune Profiler дают мне тот же результат)

Это означает, что производительность этого фрагмента кода:

98670528 / 150.10^-3 / 10^9 = 0.66 GFLOPs/sec

Где максимальная производительность этого процессора должна составлять 2 * 3,2 Гфлоп / с (2 модуля FP, процессор 3,2 ГГц), верно?

Есть ли объяснение этому огромному разрыву? Потому что я не могу это объяснить.

Заранее большое спасибо, и я действительно могу использовать вашу помощь!

Ответы [ 4 ]

5 голосов
/ 01 марта 2012

Я бы использовал SSE.

Редактировать: Я сам провел еще несколько тестов и обнаружил, что ваша программа не ограничена ни пропускной способностью памяти (теоретический предел примерно в 3-4 раза выше, чем ваш результат), ниПроизводительность с плавающей запятой (с еще более высоким пределом) ограничивается отложенным выделением страниц памяти операционной системой.

#include <chrono>
#include <iostream>
#include <x86intrin.h>

using namespace std::chrono;

static const unsigned size = 49335264;

float data[size], other_data[size], other_data2[size];

int main() {
#if 0
        for(unsigned i=0; i<size; i++) {
                data[i] = i;
                other_data[i] = i;
                other_data2[i] = i;
        }
#endif
    system_clock::time_point start = system_clock::now();
        for(unsigned i=0; i<size; i++) 
                data[i] += other_data[i]*other_data2[i];

    microseconds timeUsed = system_clock::now() - start;

    std::cout << "Used " << timeUsed.count() << " us, " 
              << 2*size/(timeUsed.count()/1e6*1e9) << " GFLOPS\n";
}

Перевод с g++ -O3 -march=native -std=c++0x.Программа выдает

Used 212027 us, 0.465368 GFLOPS

в качестве выходных данных, хотя горячий цикл преобразуется в

400848:       vmovaps 0xc234100(%rdx),%ymm0
400850:       vmulps 0x601180(%rdx),%ymm0,%ymm0
400858:       vaddps 0x17e67080(%rdx),%ymm0,%ymm0
400860:       vmovaps %ymm0,0x17e67080(%rdx)
400868:       add    $0x20,%rdx
40086c:       cmp    $0xbc32f80,%rdx
400873:       jne    400848 <main+0x18>

Это означает, что он полностью векторизован, использует 8 операций с плавающей запятой на итерацию и даже использует преимущества AVX.После игры с потоковой инструкцией, такой как movntdq, которая ничего не купила, я решил на самом деле инициализировать массивы чем-то - иначе они будут нулевыми страницами, которые будут отображаться в реальную память только в том случае, если они записаны.Изменение #if 0 на #if 1 немедленно приводит к

Used 48843 us, 2.02016 GFLOPS

, что очень близко к пропускной способности памяти системы (4 плавают 4 байта на два FLOPS = 16 ГБайт / с, теоретический предел равен 2Каналы DDR3 каждые 10 667 ГБайт / с).

3 голосов
/ 29 февраля 2012

Объяснение простое: хотя ваш процессор может работать на (скажем) 6,4 ГГц, ваша подсистема памяти может вводить / выводить данные только со скоростью примерно 1/10 этой скорости (широкое правило для большинства современных товаров ЦП). Таким образом, достижение устойчивой скорости флопа на уровне 1/8 от теоретического максимума для вашего процессора на самом деле очень хорошая производительность.

Поскольку вы, похоже, имеете дело с около 370 МБ данных, что, вероятно, больше, чем кэши вашего процессора, ваши вычисления связаны с вводом / выводом.

1 голос
/ 29 февраля 2012

Как объяснил High Performance Mark, ваш тест, скорее всего, будет связан с памятью, а не с вычислениями.

Я хотел бы добавить, что для количественной оценки этого эффекта вы можете изменить тесттак что он работает с данными, которые помещаются в кэш L1:

for(i=0, j=0; i<6166908; i++) 
{ 
    data[j] += other_data[j] * other_data2[j]; j++;
    data[j] += other_data[j] * other_data2[j]; j++; 
    data[j] += other_data[j] * other_data2[j]; j++;
    data[j] += other_data[j] * other_data2[j]; j++;
    data[j] += other_data[j] * other_data2[j]; j++;
    data[j] += other_data[j] * other_data2[j]; j++; 
    data[j] += other_data[j] * other_data2[j]; j++; 
    data[j] += other_data[j] * other_data2[j]; j++; 

    if ((j & 1023) == 0) j = 0;
} 

Производительность этой версии кода должна быть ближе к теоретическому максимуму FLOPS.Конечно, это, вероятно, не решит вашу первоначальную проблему, но, надеюсь, это поможет понять, что происходит.

0 голосов
/ 01 марта 2012

В моем первом посте я посмотрел код сборки многократного накопления фрагмента кода, и он выглядит так:

movq  0x80(%rbx), %rcx
movq  0x138(%rbx), %rdi
movq  0x120(%rbx), %rdx
movq  (%rcx), %rsi
movq  0x8(%rdi), %r8
movq  0x8(%rdx), %r9
movssl  0x1400(%rsi), %xmm0
mulssl  0x90(%r8), %xmm0
addssl  0x9f8(%r9), %xmm0
movssl  %xmm0, 0x9f8(%r9)

По общему количеству циклов я рассчитал, что для этого требуется ~ 10 цикловвыполнить умножение-накопить.

Похоже, проблема в том, что компилятор не может конвейерно выполнять цикл, даже если между циклами нет зависимостей, я прав?

У кого-нибудь есть другие идеи?/ решения для этого?

Спасибо за помощь, пока!

...