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