В чем разница между инструкциями ADC и ADCX на ia32/ia64?
Я просматривал руководство разработчика программного обеспечения Intel, когда столкнулся с ADCX
инструкция, которая ранее была мне неизвестна; его кодировка 66 0F 38 F6
, Кажется, что он почти идентичен ADC
инструкция, так почему бы вам использовать ADCX
когда:
- поддерживается только в современных процессорах
- кодирование команд занимает больше места (4 байта против 1 для
ADC
)
Есть ли какой-то другой побочный эффект или особый случай, когда ADCX
оказывается выгоднее ADC
? Должно быть, была веская причина, по которой это было добавлено в репертуар инструкций.
3 ответа
Цитата из статьи Новые инструкции поддержки большой целочисленной арифметики от Intel:
Инструкции adcx и adox являются расширениями инструкции adc, разработанной для поддержки двух отдельных цепочек переноса. Они определены как:
adcx dest/src1, src2 adox dest/src1, src2
Обе инструкции вычисляют сумму src1 и src2 плюс перенос и генерируют выходную сумму dest и вынос. Разница между этими двумя инструкциями состоит в том, что adcx использует флаг CF для переноса и выполнения (оставляя флаг OF без изменений), тогда как adox инструкция использует флаг OF для переноса и выполнения (оставляя флаг CF без изменений),
Основное преимущество этих инструкций над adc состоит в том, что они поддерживают две независимые цепи переноса.
Эти инструкции используются для ускорения арифметики с большими целыми числами.
Перед этими инструкциями добавление больших чисел часто приводило к кодовой последовательности, которая выглядела так:
add
adc
adc
adc
adc
Здесь важно отметить, что если результат сложения не помещается в машинное слово, флаг переноса устанавливается и переносится на следующее старшее машинное слово. Все эти инструкции зависят друг от друга, потому что они учитывают предыдущий дополнительный флаг переноса и генерируют новое значение флага переноса после выполнения.
Поскольку процессоры x86 способны выполнять несколько инструкций одновременно, это стало огромным узким местом. Цепочка зависимостей просто не позволяла процессору выполнять арифметику параллельно. (чтобы быть точным на практике вы найдете загрузки и сохранения между последовательностями add/adc, но производительность все еще была ограничена зависимостью переноса).
Чтобы улучшить это, Intel добавила вторую цепочку переноса, переосмыслив флаг переполнения.
adc
Инструкция получила два варианта новостей: adcx
а также adox
adcx
такой же как adc
за исключением того, что он больше не изменяет флаг OF (переполнение).
adox
такой же как adc
, но он хранит информацию переноса в флаге OF. Он также больше не изменяет флаг переноса.
Как вы можете видеть два новых adc
варианты не влияют друг на друга в отношении использования флага. Это позволяет вам запускать два длинных целочисленных дополнения параллельно, чередуя инструкции и используя adcx
для одной последовательности и adox
для другой последовательности.
Старый вопрос, но дополняющий ответ Нильса Пипенбринка и отвечающий на запрос Госвина фон Бредерлоу о примере использования:
Рассмотрим следующую функцию C:
void sum(uint128 a[2], uint128 b[2], uint128 sum[3])
{
uint128 a_sum = a[0] + a[1];
uint128 b_sum = b[0] + b[1];
sum[0] = a_sum;
sum[1] = b_sum;
sum[2] = a_sum + b_sum;
}
Компилятор может скомпилировать эту функцию как:
sum:
; Windows x64 calling convention:
; uint128 *a in RCX, *b in RDX, *sum in R8
push rsi ; non-volatile registers
push rdi
; clear CF and OF flags, carry-in = 0 to start both chains
xor eax,eax ; before loading data into RAX
; Or less efficient but doesn't destroy a register: test eax, eax
; sub rsp, 40 would also leave OF and CF=0
;; load all the data first for example purposes only,
;; into human-friendly pairs of registers like RDX:RCX
mov rax,r8 ; sum = rax
; r11:r10 = b[1] ;; standard notation is hi:lo
mov r11,[rdx+24] ; high half
mov r10,[rdx+16] ; low half
; r9:r8 = b[0]
mov r9,[rdx+8]
mov r8,[rdx]
; rdi:rsi = a[1]
mov rdi,[rcx+24]
mov rsi,[rcx+16]
; rdx:rcx = a[0] (overwriting the input pointers)
mov rdx,[rcx+8]
mov rcx,[rcx]
;;;;;; Now the ADCX/ADOX part
; compute a_sum in rdx:rcx
; compute b_sum in r9:r8
; Lower halves
; CF=0 and OF=0 thanks to an earlier instruction
adcx rcx, rsi ; a_sum.lo = a[0].lo + a[1].lo + 0
adox r8, r10 ; b_sum.lo = b[0].lo + b[1].lo + 0
; Higher halves
; CF and OF have carry-out from low halves
adcx rdx,rdi ; a_sum.hi = a[0].hi + a[1].hi + carry-in(CF)
adox r9,r11 ; b_sum.hi = b[0].hi + b[1].hi + carry-in(OF)
; nothing uses the CF and OF outputs, but that's fine.
; sum[0] = rdx:rcx
mov [rax],rcx
mov [rax+8],rdx
; sum[1] = r9:r8
mov [rax+16],r8
mov [rax+24],r9
;; Final addition the old fashioned way
; sum[2] = rdx:rcx (a_sum + b_sum)
add rcx,r8
adc rdx,r9
mov [rax+32],rcx
mov [rax+40],rdx
; clean up
pop rdi
pop rsi
ret
Это не оптимизация сама по себе; внеочередное выполнение может перекрываться одно с другимadd/adc
поскольку обе цепи очень короткие. При добавлении нескольких очень больших целых чисел может быть некоторая польза от чередования работы с более длинными цепочками зависимостей из 20 или 40 или более операций, которые составляют значительную часть размера планировщика нарушения порядка ЦП (количества невыполненных операций). он может заглянуть вперед для самостоятельной работы).
Если бы вы занимались оптимизацией, вам бы нужны были такие операнды источника памяти, какadcx r10, [rcx]
/adox r11, [rdx]
поэтому вы можете использовать меньше регистров (нет необходимости сохранять/восстанавливать RSI и RDI) и меньше инструкций (избегая некоторых отдельныхmov
нагрузки). Таким образом, у вас, вероятно, будет загрузка (и, возможно, хранилище), смешанная между каждой инструкцией ADCX и ADOX. Мы избегаем этого здесь только в целях иллюстрации, поэтому значения uint128 находятся в «очевидных» парах, таких как r9:r8 вместо rax:r9 или что-то в этом роде.
Если только данные не поступали от других операций, в частностиmulx
в умножении BigInt, что является одним из предполагаемых вариантов использования инструкций ADX. Там вы генерируете числа, которые необходимо складывать по мере умножения, а возможность чередования цепочек сложения иногда позволяет избежать необходимости сохранять больше чисел на потом или выполнятьadc reg, 0
чтобы применить перенос и завершить цепочку на данный момент, или материализовать перенос как целое число 0/1 с помощьюsetc al
. (Информационный документ Intel: Новые инструкции, поддерживающие арифметику больших целых чисел .)