Разница в производительности между MSVC и GCC для высокооптимизированного кода матричного умножения

Я вижу большую разницу в производительности между кодом, скомпилированным в MSVC (в Windows) и GCC (в Linux) для системы Ivy Bridge. Код делает плотное матричное умножение. Я получаю 70% пиковых провалов с GCC и только 50% с MSVC. Я думаю, что я, возможно, выделил разницу в том, как они оба преобразуют следующие три свойства.

__m256 breg0 = _mm256_loadu_ps(&b[8*i])
_mm256_add_ps(_mm256_mul_ps(arge0,breg0), tmp0)

GCC делает это

vmovups ymm9, YMMWORD PTR [rax-256]
vmulps  ymm9, ymm0, ymm9
vaddps  ymm8, ymm8, ymm9

MSVC делает это

vmulps   ymm1, ymm2, YMMWORD PTR [rax-256]
vaddps   ymm3, ymm1, ymm3

Может кто-нибудь объяснить мне, если и почему эти два решения могут дать такую ​​большую разницу в производительности?

Несмотря на то, что MSVC использует на одну команду меньше, он связывает нагрузку с мультом, и, возможно, это делает его более зависимым (может быть, нагрузка не может быть выполнена не по порядку)? Я имею в виду, что Ivy Bridge может выполнять одну загрузку AVX, одно мультиплексирование AVX и одно добавление AVX за один такт, но для этого требуется, чтобы каждая операция была независимой.

Может проблема в другом? Вы можете увидеть полный код сборки для GCC и MSVC для внутреннего цикла ниже. Вы можете увидеть код C++ для цикла здесь Развертывание цикла для достижения максимальной пропускной способности с Ivy Bridge и Haswell

g ++ -S -masm = intel matrix.cpp -O3 -mavx -fopenmp

.L4:
    vbroadcastss    ymm0, DWORD PTR [rcx+rdx*4]
    add rdx, 1
    add rax, 256
    vmovups ymm9, YMMWORD PTR [rax-256]
    vmulps  ymm9, ymm0, ymm9
    vaddps  ymm8, ymm8, ymm9
    vmovups ymm9, YMMWORD PTR [rax-224]
    vmulps  ymm9, ymm0, ymm9
    vaddps  ymm7, ymm7, ymm9
    vmovups ymm9, YMMWORD PTR [rax-192]
    vmulps  ymm9, ymm0, ymm9
    vaddps  ymm6, ymm6, ymm9
    vmovups ymm9, YMMWORD PTR [rax-160]
    vmulps  ymm9, ymm0, ymm9
    vaddps  ymm5, ymm5, ymm9
    vmovups ymm9, YMMWORD PTR [rax-128]
    vmulps  ymm9, ymm0, ymm9
    vaddps  ymm4, ymm4, ymm9
    vmovups ymm9, YMMWORD PTR [rax-96]
    vmulps  ymm9, ymm0, ymm9
    vaddps  ymm3, ymm3, ymm9
    vmovups ymm9, YMMWORD PTR [rax-64]
    vmulps  ymm9, ymm0, ymm9
    vaddps  ymm2, ymm2, ymm9
    vmovups ymm9, YMMWORD PTR [rax-32]
    cmp esi, edx
    vmulps  ymm0, ymm0, ymm9
    vaddps  ymm1, ymm1, ymm0
    jg  .L4

MSVC / FAc / O2 / openmp / arch: AVX...

vbroadcastss ymm2, DWORD PTR [r10]    
lea  rax, QWORD PTR [rax+256]
lea  r10, QWORD PTR [r10+4] 
vmulps   ymm1, ymm2, YMMWORD PTR [rax-320]
vaddps   ymm3, ymm1, ymm3    
vmulps   ymm1, ymm2, YMMWORD PTR [rax-288]
vaddps   ymm4, ymm1, ymm4    
vmulps   ymm1, ymm2, YMMWORD PTR [rax-256]
vaddps   ymm5, ymm1, ymm5    
vmulps   ymm1, ymm2, YMMWORD PTR [rax-224]
vaddps   ymm6, ymm1, ymm6    
vmulps   ymm1, ymm2, YMMWORD PTR [rax-192]
vaddps   ymm7, ymm1, ymm7    
vmulps   ymm1, ymm2, YMMWORD PTR [rax-160]
vaddps   ymm8, ymm1, ymm8    
vmulps   ymm1, ymm2, YMMWORD PTR [rax-128]
vaddps   ymm9, ymm1, ymm9    
vmulps   ymm1, ymm2, YMMWORD PTR [rax-96]
vaddps   ymm10, ymm1, ymm10    
dec  rdx
jne  SHORT $LL3@AddDot4x4_

РЕДАКТИРОВАТЬ:

Я тестирую код, описывая общее число операций с плавающей запятой как 2.0*n^3 где n - ширина квадратной матрицы, деленная на время, измеренное с omp_get_wtime(), Я повторяю цикл несколько раз. В выводе ниже я повторил это 100 раз.

Выход MSVC2012 на Intel Xeon E5 1620 (Ivy Bridge) турбо для всех ядер составляет 3,7 ГГц

maximum GFLOPS = 236.8 = (8-wide SIMD) * (1 AVX mult + 1 AVX add) * (4 cores) * 3.7 GHz

n   64,     0.02 ms, GFLOPs   0.001, GFLOPs/s   23.88, error 0.000e+000, efficiency/core   40.34%, efficiency  10.08%, mem 0.05 MB
n  128,     0.05 ms, GFLOPs   0.004, GFLOPs/s   84.54, error 0.000e+000, efficiency/core  142.81%, efficiency  35.70%, mem 0.19 MB
n  192,     0.17 ms, GFLOPs   0.014, GFLOPs/s   85.45, error 0.000e+000, efficiency/core  144.34%, efficiency  36.09%, mem 0.42 MB
n  256,     0.29 ms, GFLOPs   0.034, GFLOPs/s  114.48, error 0.000e+000, efficiency/core  193.37%, efficiency  48.34%, mem 0.75 MB
n  320,     0.59 ms, GFLOPs   0.066, GFLOPs/s  110.50, error 0.000e+000, efficiency/core  186.66%, efficiency  46.67%, mem 1.17 MB
n  384,     1.39 ms, GFLOPs   0.113, GFLOPs/s   81.39, error 0.000e+000, efficiency/core  137.48%, efficiency  34.37%, mem 1.69 MB
n  448,     3.27 ms, GFLOPs   0.180, GFLOPs/s   55.01, error 0.000e+000, efficiency/core   92.92%, efficiency  23.23%, mem 2.30 MB
n  512,     3.60 ms, GFLOPs   0.268, GFLOPs/s   74.63, error 0.000e+000, efficiency/core  126.07%, efficiency  31.52%, mem 3.00 MB
n  576,     3.93 ms, GFLOPs   0.382, GFLOPs/s   97.24, error 0.000e+000, efficiency/core  164.26%, efficiency  41.07%, mem 3.80 MB
n  640,     5.21 ms, GFLOPs   0.524, GFLOPs/s  100.60, error 0.000e+000, efficiency/core  169.93%, efficiency  42.48%, mem 4.69 MB
n  704,     6.73 ms, GFLOPs   0.698, GFLOPs/s  103.63, error 0.000e+000, efficiency/core  175.04%, efficiency  43.76%, mem 5.67 MB
n  768,     8.55 ms, GFLOPs   0.906, GFLOPs/s  105.95, error 0.000e+000, efficiency/core  178.98%, efficiency  44.74%, mem 6.75 MB
n  832,    10.89 ms, GFLOPs   1.152, GFLOPs/s  105.76, error 0.000e+000, efficiency/core  178.65%, efficiency  44.66%, mem 7.92 MB
n  896,    13.26 ms, GFLOPs   1.439, GFLOPs/s  108.48, error 0.000e+000, efficiency/core  183.25%, efficiency  45.81%, mem 9.19 MB
n  960,    16.36 ms, GFLOPs   1.769, GFLOPs/s  108.16, error 0.000e+000, efficiency/core  182.70%, efficiency  45.67%, mem 10.55 MB
n 1024,    17.74 ms, GFLOPs   2.147, GFLOPs/s  121.05, error 0.000e+000, efficiency/core  204.47%, efficiency  51.12%, mem 12.00 MB

3 ответа

Решение

Поскольку мы рассмотрели проблему выравнивания, я думаю, что это так: http://en.wikipedia.org/wiki/Out-of-order_execution

Поскольку g++ выдает отдельную инструкцию загрузки, ваш процессор может переупорядочить инструкции для предварительной выборки следующих данных, которые понадобятся при добавлении и умножении. MSVC, бросающий указатель на mul, выполняет загрузку и привязку к одной и той же инструкции, поэтому изменение порядка выполнения инструкций ничего не помогает.

РЕДАКТИРОВАТЬ: сервер (ы) Intel со всеми документами сегодня меньше злиться, так что здесь больше исследований о том, почему выполнение не по порядку (часть) ответ.

Прежде всего, похоже, что ваш комментарий совершенно прав насчет возможности декодирования версии инструкции умножения для MSVC в отдельные операции ввода-вывода, которые можно оптимизировать с помощью процессора из строя. Самое интересное в том, что современные секвенсоры микрокодов являются программируемыми, поэтому фактическое поведение зависит как от аппаратного, так и от встроенного программного обеспечения. Различия в сгенерированной сборке, по-видимому, связаны с GCC и MSVC, каждый из которых пытается устранить различные потенциальные узкие места. Версия GCC пытается дать свободу движку вышедшего из строя (как мы уже рассмотрели). Однако версия MSVC в конечном итоге использует функцию, называемую "микрооперация слияния". Это из-за ограничений выхода на пенсию µ-op. Конец конвейера может удалить только 3 µ-операции за такт. Микрооперация слияния, в определенных случаях, берет два микрооперации, которые должны быть выполнены на двух разных исполнительных блоках (т. Е. Чтение и арифметика памяти), и связывает их с одной микрооперацией для большей части конвейера. Слитая µ-операция разделяется только на две действительные µ-операции непосредственно перед назначением исполнительной единицы. После выполнения операции операции снова сливаются, что позволяет их удалить как единое целое.

Механизм неработоспособности видит только слитую операционную систему, поэтому он не может вытащить операционную нагрузку из умножения. Это приводит к зависанию конвейера в ожидании следующего операнда для завершения поездки на автобусе.

ВСЕ ССЫЛКИ!!!: http://download-software.intel.com/sites/default/files/managed/71/2e/319433-017.pdf

http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf

http://www.agner.org/optimize/microarchitecture.pdf

http://www.agner.org/optimize/optimizing_assembly.pdf

http://www.agner.org/optimize/instruction_tables.ods(ПРИМЕЧАНИЕ. Excel жалуется на то, что эта таблица частично повреждена или отрывочна, поэтому открывайте ее на свой страх и риск. К остальной части моих исследований Агнер Фог великолепен. После того, как я включился в шаг восстановления Excel, я обнаружил, что он полон тонн отличных данных)

http://cs.nyu.edu/courses/fall13/CSCI-GA.3033-008/Microprocessor-Report-Sandy-Bridge-Spans-Generations-243901.pdf

http://www.syncfusion.com/Content/downloads/ebook/Assembly_Language_Succinctly.pdf


МНОГО ПОСЛЕДНЕГО РЕДАКТИРОВАНИЯ: Вау, здесь было несколько интересных обновлений. Я предполагаю, что я ошибся в отношении того, насколько сильно на конвейер влияет микрооперация. Может быть, отличий в проверке состояния цикла больше, чем я ожидал, из-за различий в проверке состояния цикла, где неиспользуемые инструкции позволяют GCC чередовать сравнение и переход с последними векторами загрузки и арифметическими шагами?

vmovups ymm9, YMMWORD PTR [rax-32]
cmp esi, edx
vmulps  ymm0, ymm0, ymm9
vaddps  ymm1, ymm1, ymm0
jg  .L4

Я могу подтвердить, что использование кода GCC в Visual Studio действительно повышает производительность. Я сделал это, преобразовав объектный файл GCC в Linux для работы в Visual Studio. Эффективность возросла с 50% до 60% при использовании всех четырех ядер (и от 60% до 70% для одного ядра).

Microsoft удалила встроенную сборку из 64-битного кода, а также сломала их 64-битный диссамблер, так что код не может быть похож без модификации ( но 32-битная версия все еще работает). Они, очевидно, думали, что встроенных функций будет достаточно, но, как показывает этот случай, они ошибаются.

Может быть, слитые инструкции должны быть отдельными внутренностями?

Но Microsoft не единственная, которая производит менее оптимальный внутренний код. Если вы поместите приведенный ниже код в http://gcc.godbolt.org/ вы сможете увидеть, что делают Clang, ICC и GCC. ICC дал еще худшую производительность, чем MSVC. Он использует vinsertf128 но я не знаю почему. Я не уверен, что делает Clang, но похоже, что он ближе к GCC в другом порядке (и больше кода).

Это объясняет, почему Агнер Фог написал в своем руководстве " Оптимизация подпрограмм на языке ассемблера" в отношении "недостатков использования встроенных функций":

Компилятор может модифицировать код или реализовать его менее эффективным способом, чем предполагал программист. Может быть необходимо взглянуть на код, сгенерированный компилятором, чтобы увидеть, оптимизирован ли он так, как задумал программист.

Это разочаровывает в случае использования встроенных функций. Это означает, что нужно либо по-прежнему писать 64-битный ассемблерный код, либо найти компилятор, который реализует встроенные функции так, как задумал программист. В этом случае только GCC, кажется, делает это (и, возможно, Clang).

#include <immintrin.h>
extern "C" void AddDot4x4_vec_block_8wide(const int n, const float *a, const float *b, float *c, const int stridea, const int strideb, const int stridec) {     
    const int vec_size = 8;
    __m256 tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7;
    tmp0 = _mm256_loadu_ps(&c[0*vec_size]);
    tmp1 = _mm256_loadu_ps(&c[1*vec_size]);
    tmp2 = _mm256_loadu_ps(&c[2*vec_size]);
    tmp3 = _mm256_loadu_ps(&c[3*vec_size]);
    tmp4 = _mm256_loadu_ps(&c[4*vec_size]);
    tmp5 = _mm256_loadu_ps(&c[5*vec_size]);
    tmp6 = _mm256_loadu_ps(&c[6*vec_size]);
    tmp7 = _mm256_loadu_ps(&c[7*vec_size]);

    for(int i=0; i<n; i++) {
        __m256 areg0 = _mm256_set1_ps(a[i]);

        __m256 breg0 = _mm256_loadu_ps(&b[vec_size*(8*i + 0)]);
        tmp0 = _mm256_add_ps(_mm256_mul_ps(areg0,breg0), tmp0);    
        __m256 breg1 = _mm256_loadu_ps(&b[vec_size*(8*i + 1)]);
        tmp1 = _mm256_add_ps(_mm256_mul_ps(areg0,breg1), tmp1);
        __m256 breg2 = _mm256_loadu_ps(&b[vec_size*(8*i + 2)]);
        tmp2 = _mm256_add_ps(_mm256_mul_ps(areg0,breg2), tmp2);    
        __m256 breg3 = _mm256_loadu_ps(&b[vec_size*(8*i + 3)]);
        tmp3 = _mm256_add_ps(_mm256_mul_ps(areg0,breg3), tmp3);   
        __m256 breg4 = _mm256_loadu_ps(&b[vec_size*(8*i + 4)]);
        tmp4 = _mm256_add_ps(_mm256_mul_ps(areg0,breg4), tmp4);    
        __m256 breg5 = _mm256_loadu_ps(&b[vec_size*(8*i + 5)]);
        tmp5 = _mm256_add_ps(_mm256_mul_ps(areg0,breg5), tmp5);    
        __m256 breg6 = _mm256_loadu_ps(&b[vec_size*(8*i + 6)]);
        tmp6 = _mm256_add_ps(_mm256_mul_ps(areg0,breg6), tmp6);    
        __m256 breg7 = _mm256_loadu_ps(&b[vec_size*(8*i + 7)]);
        tmp7 = _mm256_add_ps(_mm256_mul_ps(areg0,breg7), tmp7);    
    }
    _mm256_storeu_ps(&c[0*vec_size], tmp0);
    _mm256_storeu_ps(&c[1*vec_size], tmp1);
    _mm256_storeu_ps(&c[2*vec_size], tmp2);
    _mm256_storeu_ps(&c[3*vec_size], tmp3);
    _mm256_storeu_ps(&c[4*vec_size], tmp4);
    _mm256_storeu_ps(&c[5*vec_size], tmp5);
    _mm256_storeu_ps(&c[6*vec_size], tmp6);
    _mm256_storeu_ps(&c[7*vec_size], tmp7);
}

MSVC сделал именно то, что вы просили. Если вы хотите vmovups инструкция выпущена, используйте _mm256_loadu_ps внутренняя.

Другие вопросы по тегам