В чем разница между инструкциями 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: Новые инструкции, поддерживающие арифметику больших целых чисел .)

Другие вопросы по тегам