Итак, фактическую разработку новых оптимизаций в GCC 5.0 можно считать законченной. Продукт GCC 5.0 находится сейчас в фазе stage3, то есть идет доработка уже внедренных оптимизаций. В данной и последующих статьях я расскажу об оптимизациях, реализованных в GCC 5.0 для х86 и об их влиянии на производительность программ для процессоров линейки Intel Atom и Intel Core. Сегодня речь пойдет о векторизации групповых обращений в память. В последующих статьях я расскажу об ускорениях в 32-битном PIC режиме и дополнительном усилении векторизации.
Как вы уже, наверное, догадались, в GCC 5.0 существенно улучшена векторизация групповых обращений в память. Под групповым обращением в память понимается итерируемая последовательность обращений. Например:
x = a[i], y = a[i + 1], z = a[i + 2]
итерируемое по i — это группа загрузок из памяти длиной 3. Длина группы обращений в память — это расстояние между самым младшим и старшим адресами в группе. Для примера выше — это (i + 2) – (i) + 1 = 3
Количество обращений в память в группе не превосходит ее длину. Например:
x = a[i], z = a[i + 2]
итерируемое по i — это группа длиной 3, несмотря на то, что обращений в память всего 2.
GCC 4.9 векторизует группы, где длина — это степень 2 (2, 4, 8 …).
GCC 5.0 векторизует группы, где длина равна 3 или степени 2 (2, 4, 8 …). Другие длины не векторизуются, так как встречаются в реальных приложениях очень редко.
Чаще всего векторизация группы обращений в память применяется при работе с массивами структур.
1. Конвертация изображений, скажем, в структуре RGB. Пример.
2. Работа с N-мерными координатами (скажем, чтобы нормализовать трехмерные точки). Пример.
3. Умножение векторов на константную матрицу:
a[i][0] = 7 * b[i][0] - 3 * b[i][1];
a[i][1] = 2 * b[i][0] + b[i][1];
В целом в GCC 5.0 (по сравнению с 4.9)
- Появилась векторизация группы обращений в память длиной 3
- Существенно улучшилась векторизации группы загрузок из памяти для любой длины
- Стали использоваться техники векторизации групп обращений в память оптимальные для конкретного x86 процессора.
Далее приведены таблицы, позволяющие оценить увеличение производительности при использовании GCC 5.0 на байтовых структурах (наибольшее количество элементов в векторе) по сравнению с GCC 4.9. Для оценки используется следующий цикл:
int i, j, k;
byte *in = a, *out = b;
for (i = 0; i < 1024; i++)
{
for (k = 0; k < STGSIZE; k++)
{
byte s = 0;
for (j = 0; j < LDGSIZE; j++)
s += in[j] * c[j][k];
out[k] = s;
}
in += LDGSIZE;
out += STGSIZE;
}
Где:
- c — это константная матрица:
const byte c[8][8] = {1, -1, 1, -1, 1, -1, 1, -1,
1, 1, -1, -1, 1, 1, -1, -1,
1, 1, 1, 1, -1, -1, -1, -1,
-1, 1, -1, 1, -1, 1, -1, 1,
-1, -1, 1, 1, -1, -1, 1, 1,
-1, -1, -1, -1, 1, 1, 1, 1,
-1, -1, -1, 1, 1, 1, -1, 1,
1, -1, 1, 1, 1, -1, -1, -1};
Такая матрица используется чтобы минимизировать вычисления внутри цикла до сравнительно быстрых сложений и вычитаний.
- in и out — указатели на глобальные массивы «a[1024 * LDGSIZE]» и «b[1024 * STGSIZE]»
- byte это unsigned char
- LDGSIZE и STGSIZE – макросы, определяющие длину группы загрузок из памяти и сохранений в память соответственно
Опции компиляции "-Ofast" плюс "-march=slm" для Silvermont, "-march=core-avx2" для Haswell и все комбинации -DLDGSIZE={1,2,3,4,8} -DSTGSIZE={1,2,3,4,8}
Прирост производительности GCC 5.0 по сравнению с 4.9 (во сколько раз ускорилось, больше — лучше).
Silvermont: Intel® Atom(TM) CPU C2750 @ 2.41GHz
Прирост до 6.5 раз!
Как видно из таблицы, результаты для групп сохранений в память длиной 3 не очень хорошие. Это происходит потому, что для такого преобразования требуется 8 инструкций «pshufb», длительность исполнения которой на Silvermont около 5 тактов. Несмотря на это, векторизация других команд в цикле может дать больший прирост. Это видно на примере группы загрузок из памяти длиной 2, 3, 4 и 8.
Пример GCC ассемблера для группы загрузок из памяти длиной 2:
GCC 4.9 | GCC 5.0 |
movdqa .LC0(%rip), %xmm7 movdqa .LC1(%rip), %xmm6 movdqa .LC2(%rip), %xmm5 movdqa .LC3(%rip), %xmm4 movdqu a(%rax,%rax), %xmm1 movdqu a+16(%rax,%rax), %xmm0 movdqa %xmm1, %xmm3 pshufb %xmm7, %xmm3 movdqa %xmm0, %xmm2 pshufb %xmm5, %xmm1 pshufb %xmm6, %xmm2 pshufb %xmm4, %xmm0 por %xmm2, %xmm3 por %xmm0, %xmm2 |
movdqa .LC0(%rip), %xmm3 movdqu a(%rax,%rax), %xmm0 movdqu a+16(%rax,%rax), %xmm1 movdqa %xmm3, %xmm4 pand %xmm0, %xmm3 psrlw $8, %xmm0 pand %xmm1, %xmm4 psrlw $8, %xmm1 packuswb %xmm4, %xmm3 packuswb %xmm1, %xmm0 |
Здесь, как и в приведенном ниже примере для Haswell, стоит отметить, что большинство констант ".LC" будут инвариантами в цикле, однако только при наличии свободных регистров. В противном случае их придется ставить непосредственно в phufb: «pshufb .LC0, %xmm3». Такой pshufb будет больше на размер адреса и потенциально дольше исполнятся.
Haswell: Intel® Core(TM) i7-4770K CPU @ 3.50GHz
Прирост до 3 раз!
В данном случае результаты для групп сохранений в память длиной 3 намного лучше, так как на Haswell инструкция «pshufb» исполняется за 1 такт. Более того, наибольший прирост здесь именно для внедренной векторизации групп обращений в память длиной 3.
Пример GCC ассемблера для группы загрузок из памяти длиной 2:
GCC 4.9 | GCC 5.0 |
vmovdqa .LC0(%rip), %ymm7 vmovdqa .LC1(%rip), %ymm6 vmovdqa .LC2(%rip), %ymm5 vmovdqa .LC3(%rip), %ymm4 vmovdqu a(%rax,%rax), %ymm0 vmovdqu a+32(%rax,%rax), %ymm2 vpshufb %ymm7, %ymm0, %ymm1 vpshufb %ymm5, %ymm0, %ymm0 vpshufb %ymm6, %ymm2, %ymm3 vpshufb %ymm4, %ymm2, %ymm2 vpor %ymm3, %ymm1, %ymm1 vpor %ymm2, %ymm0, %ymm0 vpermq $216, %ymm1, %ymm1 vpermq $216, %ymm0, %ymm0 |
vmovdqa .LC0(%rip), %ymm3 vmovdqu a(%rax,%rax), %ymm0 vmovdqu a+32(%rax,%rax), %ymm2 vpand %ymm0, %ymm3, %ymm1 vpsrlw $8, %ymm0, %ymm0 vpand %ymm2, %ymm3, %ymm4 vpsrlw $8, %ymm2, %ymm2 vpackuswb %ymm4, %ymm1, %ymm1 vpermq $216, %ymm1, %ymm1 vpackuswb %ymm2, %ymm0, %ymm0 vpermq $216, %ymm0, %ymm0 |
Из всего вышесказанного следует вывод: используя GCC 5.0, можно существенно ускорить производительность приложений, подобных описанным выше. Начинать пробовать можно уже сейчас! Большинство правок планируется портировать в Android NDK.
Компиляторы используемые в замерах:
Скачать пример, на котором производились замеры, можно из оригинального текста статьи на английском.
Автор: Evgeny1982