Тестовый чехол для adcx и adox
Я тестирую Intel ADX добавляет с переносом и добавлением с переполнением в конвейерные добавляет большие целые числа. Я хотел бы посмотреть, как должно выглядеть ожидаемое поколение кода. Из _addcarry_u64 и _addcarryx_u64 с MSVC и ICC я думал, что это будет подходящий тестовый пример:
#include <stdint.h>
#include <x86intrin.h>
#include "immintrin.h"
int main(int argc, char* argv[])
{
#define MAX_ARRAY 100
uint8_t c1 = 0, c2 = 0;
uint64_t a[MAX_ARRAY]={0}, b[MAX_ARRAY]={0}, res[MAX_ARRAY];
for(unsigned int i=0; i< MAX_ARRAY; i++){
c1 = _addcarryx_u64(c1, res[i], a[i], (unsigned long long int*)&res[i]);
c2 = _addcarryx_u64(c2, res[i], b[i], (unsigned long long int*)&res[i]);
}
return 0;
}
Когда я рассмотрю сгенерированный код из GCC 6.1, используя -O3
и -madx
, он показывает сериализованный addc
. -O1
и -O2
дают похожие результаты:
main:
subq $688, %rsp
xorl %edi, %edi
xorl %esi, %esi
leaq -120(%rsp), %rdx
xorl %ecx, %ecx
leaq 680(%rsp), %r8
.L2:
movq (%rdx), %rax
addb $-1, %sil
adcq %rcx, %rax
setc %sil
addb $-1, %dil
adcq %rcx, %rax
setc %dil
movq %rax, (%rdx)
addq $8, %rdx
cmpq %r8, %rdx
jne .L2
xorl %eax, %eax
addq $688, %rsp
ret
Итак, я предполагаю, что тестовый пример не совсем ударил по знаку, или я делаю что-то неправильно, или я что-то неправильно использую,...
Если я правильно разбираю документы Intel на _addcarryx_u64
, я считаю, что код C должен генерировать конвейер. Поэтому я предполагаю, что я делаю что-то неправильно:
Описание
Добавить неподписанные 64-битные целые числа a и b с беззнаковым 8-разрядным переносом c_in (флаг переноса или переполнения) и сохранить 64-битный результат без знака, и выполнение в dst (флаг переноса или переполнения).
Как я могу сгенерировать добавление конвейера с переносом/добавлением с переполнением (adcx
/adox
)?
На самом деле я получил готовность к тестированию 5-го поколения Core i7 (обратите внимание на флаг adx
cpu):
$ cat /proc/cpuinfo | grep adx
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush
dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc
arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni
pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 fma cx16 xtpr pdcm pcid sse4_1
sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm
3dnowprefetch ida arat epb pln pts dtherm tpr_shadow vnmi flexpriority ept vpid fsgsbase
tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm rdseed adx smap xsaveopt
...
Ответы
Ответ 1
Это похоже на хороший тестовый сценарий. Он собирается, чтобы исправить рабочий код, не так ли? Полезно, чтобы компилятор поддерживал внутреннее значение в этом смысле, даже если он еще не поддерживает создание оптимального кода. Это позволяет людям начать использовать внутреннее. Это необходимо для совместимости.
В следующем году или всякий раз, когда выполняется поддержка backend для adcx/adox компилятора, тот же код будет скомпилирован для более быстрых двоичных файлов без изменения исходного кода.
Я предполагаю, что то, что происходит для gcc.
clang 3.8.1 реализация более буквальна, но в итоге она делает ужасную работу: сохранение флага с помощью sahf и push/pop eax. Посмотрите на Godbolt.
Я думаю, что есть ошибка в выходе источника asm, так как mov eax, ch
не собирается. (В отличие от gcc, clang/LLVM использует встроенный ассемблер и на самом деле не просматривает текстовое представление asm на пути от LLVM IR к машинным кодам). Разборка кода машины показывает mov eax,ebp
. Я думаю, что также ошибка, потому что bpl
(или остальная часть регистра) не имеет полезного значения в этой точке. Вероятно, он хотел mov al, ch
или movzx eax, ch
.
Ответ 2
Когда GCC будет исправлен, чтобы генерировать гораздо лучший встроенный код для add_carryx _..., будьте осторожны с вашим кодом, потому что вариант цикла содержит сравнение (модифицирует флаги C и O аналогично вспомогательной инструкции) и приращение (изменяет флаги C и O, такие как команда добавления).
for(unsigned int i=0; i< MAX_ARRAY; i++){
c1 = _addcarryx_u64(c1, res[i], a[i], (unsigned long long int*)&res[i]);
c2 = _addcarryx_u64(c2, res[i], b[i], (unsigned long long int*)&res[i]);
}
По этой причине c1 и c2 в вашем коде всегда будут обрабатываться (сохранены и восстановлены в регистрах temp на каждой итерации цикла). И полученный код, созданный gcc, по-прежнему будет выглядеть как сборка, предоставленная вами по уважительным причинам.
С точки зрения времени выполнения res [i] является непосредственной зависимостью между двумя командами add_carryx, две инструкции не являются действительно независимыми и не будут использовать возможный архитектурный parallelism в процессоре.
Я понимаю, что код является лишь примером, но, возможно, это не лучший пример для использования, когда gcc будет изменен.
Добавление трех чисел в большой целочисленной арифметике является сложной проблемой; а затем вам лучше использовать addcarryx для параллельной обработки вариантов цикла (приращение и сравнение + ветвь по одной и той же переменной, еще одна сложная проблема).