Какой из них лучше, gcc или armcc для оптимизации NEON?
Обращаясь к ответу @auselen здесь: Использование встроенных в ARM NEON для добавления альфа и перестановки, похоже, что компилятор armcc намного лучше, чем компилятор gcc для оптимизации NEON. Это правда? Я действительно не пробовал armcc компилятор. Но я получил довольно оптимизированный код, используя компилятор gcc с флагом оптимизации -O3. Но теперь мне интересно, действительно ли armcc так хорош? Так какой из двух компиляторов лучше, учитывая все факторы?
2 ответа
Компиляторы также являются программным обеспечением, они имеют тенденцию улучшаться с течением времени. Любое общее утверждение, например armcc, лучше, чем GCC на NEON (или лучше сказать, как векторизация), не может оставаться верным вечно, поскольку одна группа разработчиков может закрыть пробел с достаточным вниманием. Однако изначально логично ожидать, что разработанные аппаратными компаниями компиляторы будут превосходить их, поскольку им необходимо демонстрировать / продавать эти функции.
Один недавний пример, который я видел, был здесь на Переполнении стека об ответе для предсказания ветвления. Цитата из последней строки обновленного раздела "Это показывает, что даже зрелые современные компиляторы могут сильно различаться по своей способности оптимизировать код...".
Я большой поклонник GCC, но я бы не стал делать ставку на качество кода, созданного с помощью компиляторов Intel или ARM. Я ожидаю, что любой коммерческий компилятор создаст код, по крайней мере, такой же хороший, как GCC.
Одним из эмпирических ответов на этот вопрос могло бы стать использование примера неоновой оптимизации в гильбертовом пространстве и посмотреть, как его оптимизируют разные компиляторы.
void neon_convert (uint8_t * __restrict dest, uint8_t * __restrict src, int n)
{
int i;
uint8x8_t rfac = vdup_n_u8 (77);
uint8x8_t gfac = vdup_n_u8 (151);
uint8x8_t bfac = vdup_n_u8 (28);
n/=8;
for (i=0; i<n; i++)
{
uint16x8_t temp;
uint8x8x3_t rgb = vld3_u8 (src);
uint8x8_t result;
temp = vmull_u8 (rgb.val[0], rfac);
temp = vmlal_u8 (temp,rgb.val[1], gfac);
temp = vmlal_u8 (temp,rgb.val[2], bfac);
result = vshrn_n_u16 (temp, 8);
vst1_u8 (dest, result);
src += 8*3;
dest += 8;
}
}
Это armcc 5.01
20: f421140d vld3.8 {d1-d3}, [r1]!
24: e2822001 add r2, r2, #1
28: f3810c04 vmull.u8 q0, d1, d4
2c: f3820805 vmlal.u8 q0, d2, d5
30: f3830806 vmlal.u8 q0, d3, d6
34: f2880810 vshrn.i16 d0, q0, #8
38: f400070d vst1.8 {d0}, [r0]!
3c: e1520003 cmp r2, r3
40: bafffff6 blt 20 <neon_convert+0x20>
Это GCC 4.4.3-4.7.1
1e: f961 040d vld3.8 {d16-d18}, [r1]!
22: 3301 adds r3, #1
24: 4293 cmp r3, r2
26: ffc0 4ca3 vmull.u8 q10, d16, d19
2a: ffc1 48a6 vmlal.u8 q10, d17, d22
2e: ffc2 48a7 vmlal.u8 q10, d18, d23
32: efc8 4834 vshrn.i16 d20, q10, #8
36: f940 470d vst1.8 {d20}, [r0]!
3a: d1f0 bne.n 1e <neon_convert+0x1e>
Который выглядит очень похоже, поэтому у нас есть ничья. Увидев это, я попытался упомянуть, добавить альфа и переставить снова.
void neonPermuteRGBtoBGRA(unsigned char* src, unsigned char* dst, int numPix)
{
numPix /= 8; //process 8 pixels at a time
uint8x8_t alpha = vdup_n_u8 (0xff);
for (int i=0; i<numPix; i++)
{
uint8x8x3_t rgb = vld3_u8 (src);
uint8x8x4_t bgra;
bgra.val[0] = rgb.val[2]; //these lines are slow
bgra.val[1] = rgb.val[1]; //these lines are slow
bgra.val[2] = rgb.val[0]; //these lines are slow
bgra.val[3] = alpha;
vst4_u8(dst, bgra);
src += 8*3;
dst += 8*4;
}
}
Компиляция с gcc...
$ arm-linux-gnueabihf-gcc --version
arm-linux-gnueabihf-gcc (crosstool-NG linaro-1.13.1-2012.05-20120523 - Linaro GCC 2012.05) 4.7.1 20120514 (prerelease)
$ arm-linux-gnueabihf-gcc -std=c99 -O3 -c ~/temp/permute.c -marm -mfpu=neon-vfpv4 -mcpu=cortex-a9 -o ~/temp/permute_gcc.o
00000000 <neonPermuteRGBtoBGRA>:
0: e3520000 cmp r2, #0
4: e2823007 add r3, r2, #7
8: b1a02003 movlt r2, r3
c: e92d01f0 push {r4, r5, r6, r7, r8}
10: e1a021c2 asr r2, r2, #3
14: e24dd01c sub sp, sp, #28
18: e3520000 cmp r2, #0
1c: da000019 ble 88 <neonPermuteRGBtoBGRA+0x88>
20: e3a03000 mov r3, #0
24: f460040d vld3.8 {d16-d18}, [r0]!
28: eccd0b06 vstmia sp, {d16-d18}
2c: e59dc014 ldr ip, [sp, #20]
30: e2833001 add r3, r3, #1
34: e59d6010 ldr r6, [sp, #16]
38: e1530002 cmp r3, r2
3c: e59d8008 ldr r8, [sp, #8]
40: e1a0500c mov r5, ip
44: e59dc00c ldr ip, [sp, #12]
48: e1a04006 mov r4, r6
4c: f3c73e1f vmov.i8 d19, #255 ; 0xff
50: e1a06008 mov r6, r8
54: e59d8000 ldr r8, [sp]
58: e1a0700c mov r7, ip
5c: e59dc004 ldr ip, [sp, #4]
60: ec454b34 vmov d20, r4, r5
64: e1a04008 mov r4, r8
68: f26401b4 vorr d16, d20, d20
6c: e1a0500c mov r5, ip
70: ec476b35 vmov d21, r6, r7
74: f26511b5 vorr d17, d21, d21
78: ec454b34 vmov d20, r4, r5
7c: f26421b4 vorr d18, d20, d20
80: f441000d vst4.8 {d16-d19}, [r1]!
84: 1affffe6 bne 24 <neonPermuteRGBtoBGRA+0x24>
88: e28dd01c add sp, sp, #28
8c: e8bd01f0 pop {r4, r5, r6, r7, r8}
90: e12fff1e bx lr
Компилирование с armcc...
$ armcc
ARM C/C++ Compiler, 5.01 [Build 113]
$ armcc --C99 --cpu=Cortex-A9 -O3 -c permute.c -o permute_arm.o
00000000 <neonPermuteRGBtoBGRA>:
0: e1a03fc2 asr r3, r2, #31
4: f3870e1f vmov.i8 d0, #255 ; 0xff
8: e0822ea3 add r2, r2, r3, lsr #29
c: e1a031c2 asr r3, r2, #3
10: e3a02000 mov r2, #0
14: ea000006 b 34 <neonPermuteRGBtoBGRA+0x34>
18: f420440d vld3.8 {d4-d6}, [r0]!
1c: e2822001 add r2, r2, #1
20: eeb01b45 vmov.f64 d1, d5
24: eeb02b46 vmov.f64 d2, d6
28: eeb05b40 vmov.f64 d5, d0
2c: eeb03b41 vmov.f64 d3, d1
30: f401200d vst4.8 {d2-d5}, [r1]!
34: e1520003 cmp r2, r3
38: bafffff6 blt 18 <neonPermuteRGBtoBGRA+0x18>
3c: e12fff1e bx lr
В этом случае armcc производит намного лучший код. Я думаю, что это оправдывает ответ FGP выше. Большую часть времени GCC будет производить достаточно хороший код, но вы должны следить за критическими частями или, что самое важное, сначала вы должны измерить / профиль.
Если вы используете встроенные функции NEON, компилятор не должен иметь такого большого значения. Большинство (если не все) встроенные NEON преобразуются в одну инструкцию NEON, поэтому компилятору остается только распределение регистров и планирование команд. По моему опыту, и GCC 4.2, и Clang 3.1 достаточно хорошо справляются с этими задачами.
Обратите внимание, однако, что инструкции NEON немного более выразительны, чем элементы NEON. Например, инструкции загрузки / сохранения NEON имеют режимы адресации до и после приращения, которые объединяют загрузку или сохранение с приращением регистра адресов, тем самым сохраняя вам одну инструкцию. Встроенные функции NEON не предоставляют явного способа сделать это, но вместо этого полагаются на то, что компилятор объединяет встроенную функцию загрузки / хранения регулятора NEON и приращение адреса в инструкцию загрузки / сохранения с постинкрементным увеличением. Аналогичным образом, некоторые инструкции загрузки / сохранения позволяют указать выравнивание адреса памяти и выполняются быстрее, если вы указываете более строгие гарантии выравнивания. И опять же, свойства NEON не позволяют явно указывать выравнивание, а вместо этого полагаются на компилятор, который выводит правильный спецификатор выравнивания. Теоретически, вы используете атрибуты "align" в ваших указателях, чтобы предоставить подходящие подсказки компилятору, но, по крайней мере, Clang, кажется, игнорирует эти...
По моему опыту, ни Clang, ни GCC не очень хороши, когда дело доходит до такого рода оптимизаций. К счастью, дополнительный выигрыш в производительности этих видов оптимизации, как правило, не так уж велик - он больше на 10%, чем на 100%.
Другая область, где эти два компилятора не особенно умны, - это предотвращение разливов в стеке. Если ваш код использует больше векторных переменных, чем регистров NEON, я думаю, что оба компилятора создают ужасный код. По сути, то, что они делают, - это планирование инструкций на основе предположения о наличии достаточного количества регистров. Распределение регистров, кажется, происходит потом, и, кажется, просто выливает значения в стек после запуска регистров. Поэтому убедитесь, что ваш код имеет рабочий набор из менее чем 16 128-битных векторов или 32 64-битных векторов в любое время!
В целом, я получил довольно хорошие результаты как от GCC, так и от Clang, но мне регулярно приходилось немного реорганизовывать код, чтобы избежать идиосинкразий компилятора. Мой совет: придерживайтесь GCC или Clang, но регулярно проверяйте дизассемблер по вашему выбору.
Итак, в целом, я бы сказал, что придерживаться GCC - это нормально. Возможно, вы захотите взглянуть на разборку деталей, критичных к производительности, и проверить, выглядит ли это разумно.