Контрольный пример для adcx и adox
Я тестирую Intel ADX add с переносом и add с переполнением для конвейерного добавления больших целых чисел. Я хотел бы посмотреть, как должна выглядеть ожидаемая генерация кода. Из _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-разрядный результат без знака в out и вынос в dst (флаг переноса или переполнения).
Как я могу сгенерировать конвейер добавил бы с переносом / добавить с переполнением (adcx
/ adox
)?
Я на самом деле готов к тестированию Core i7 5-го поколения (обратите внимание на adx
флаг процессора):
$ 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
...
2 ответа
Это похоже на хороший тест-кейс. Он собирается для исправления рабочего кода, верно? В этом смысле компилятору полезно поддерживать встроенное, даже если он еще не поддерживает создание оптимального кода. Это позволяет людям начать использовать внутреннее. Это необходимо для совместимости.
В следующем году или всякий раз, когда будет сделана внутренняя поддержка компилятором для adcx/adox, тот же код будет компилироваться в более быстрые двоичные файлы без изменения исходного кода.
Я предполагаю, что это то, что происходит для GCC.
Реализация clang 3.8.1 более буквальна, но в итоге она делает ужасную работу: сохранение флага с помощью sahf и push / pop of eax. Смотрите это на Годболт.
Я думаю, что есть даже ошибка в исходных данных asm, так как mov eax, ch
не будет собираться. (В отличие от gcc, clang/LLVM использует встроенный ассемблер и на самом деле не проходит текстовое представление asm на пути от LLVM IR к машинному коду). Разборка показывает машинный код mov eax,ebp
там. Я думаю, что это тоже ошибка, потому что bpl
(или остальная часть регистра) не имеет полезного значения в этот момент. Наверное, хотел mov al, ch
или же movzx eax, ch
,
Когда GCC будет исправлен, чтобы генерировать намного лучший встроенный код для add_carryx_..., будьте осторожны с вашим кодом, потому что вариант цикла содержит сравнение (изменяет флаги C и O аналогично подинструкции) и приращение (модифицирует C и О флаги как инструкция добавления).
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 в вашем коде всегда будут обрабатываться с жалостью (сохраняются и восстанавливаются во временных регистрах на каждой итерации цикла). И полученный код, сгенерированный gcc, по понятным причинам будет выглядеть как предоставленная вами сборка.
С точки зрения времени выполнения, res [i] является непосредственной зависимостью между 2 инструкциями add_carryx, 2 инструкции не являются на самом деле независимыми и не получат выгоды от возможного архитектурного параллелизма в процессоре.
Я понимаю, что код является лишь примером, но, возможно, он не будет лучшим примером для использования при модификации gcc.
Добавление 3 чисел в арифметике большого целого является сложной задачей; Векторизация помогает, и тогда вам лучше использовать addcarryx для параллельной обработки вариантов цикла (инкремент и сравнение + ветвление в одной и той же переменной, еще одна сложная проблема).