Можете ли вы проверить флаг в байте и получить оставшееся 7-битное целое значение за одну операцию сборки? - PullRequest
2 голосов
/ 01 мая 2020

Какой самый оптимальный способ (наименьшее / быстрое количество операций) взять 8-битное, 16-битное, 32-битное или 64-битное число, извлечь из него первый бит, проверить, является ли этот бит истинным, а тем временем сохранить полученный номер после удаления начального бита? (В сборке).

integerInBitsWithLeadingFlag = 10001000
flag == 1
integer == 0001000 = 1000

В сборке я знаю, что тут и там есть хитрости для деления и хранения остатка, хранения по существу двух переменных в результате или других подобных вещей. Может быть, есть какой-то способ сделать это в сборке.

Причина, по которой я спрашиваю, заключается в том, что я хочу хранить большие числа в последовательности 8-битных значений, где ведущий бит - это флаг, указывающий, является ли или не «больше» значений должны объединяться вместе, а оставшиеся 7 бит используются для вычисления окончательного целого числа / bigint. Если вместо этого лучше хранить флаг в последнем / последнем бите, то было бы хорошо включить:)

Я новичок в сборке, поэтому я не совсем уверен, как это можно сделать.

; assembly pseudocode
start:
  AND rax, 10000000 ; AND the value with a leading 1 (or something like this)
  CMP rax, 1 ; compare the leading value with 1 to see if it matches.
  JE matches
  JNE notmatches

matches:
  ; remove bit from rax to get integer value.

notmatches:
  ; same, remove bit from rax to get integer value.

Есть ли что-то, в чем я могу сделать это следующим образом:

start:
  ANDFLAGWITHREMAINDER rax, 10000000
  ; and now, after 1 operation,
  ; you have the flag and the integer.

Если нет, как правильно это сделать?

1 Ответ

4 голосов
/ 01 мая 2020

x86 Проверка и сброс битов btr eax, 7 делает именно то, что вы просили: сбросить бит 7 и установить CF = исходное значение этого бита.

btr reg, imm или reg, reg составляет 1 моп для Intel, 2 моп для AMD. Когда за ним следует инструкция jcc, он не может слиться макросом в одну операцию сравнения и ветвления, как это делает test al, 1<<7 / jnz. (https://agner.org/optimize/). Количество команд - не единственный фактор производительности . Хорошие ILP и короткие критические задержки, особенно во избежание ненужных цепочек зависимостей, переносимых l oop, также важны. Но подсчет входных мопов для быстрого пути в вашем коде, безусловно, стоит учесть.

x86 сдвиги (как и большинство ISA) помещают последний сдвинутый бит в флаг переноса. Так shr al, 1 устанавливает CF = orig & 1 и обновляет AL = orig >> 1. Возможно, есть способ объединить это со сдвигом битов в байтах, чтобы объединить их, как с shrd, или с помощью трюков с частичной регистрацией ...

Поскольку вы манипулируете байтами, Почему нет G CC использовать частичные регистры? - это то, что вы, возможно, захотите понять, если задумаетесь о способах объединения нескольких битовых полей в одно непрерывное большее целое число в регистре.


Я хочу хранить большие числа в последовательности 8-битных значений

Я надеюсь, что вы не планируете вычислять напрямую с числами в этом формате. Это звучит разумно в качестве компактного формата / кодирования сериализации переменной длины, который может быть меньше int для небольших значений, но при этом содержать до uint64_t или даже больше, если необходимо.

Если общая скорость больше важно, работать как минимум 32-битными блоками, так что вы получаете гораздо больше битов результата за операцию ЦП. Или так меньше шагов распаковки для объединения в одно смежное двоичное целое число. (например, переменное смещение AVX2 vpsrlvd, чтобы поместить вершину одного меча рядом с нижней частью меча в следующем более высоком элементе, затем используйте сдвиги переменных qword, чтобы сделать то же самое в середине 128 -битная полоса. Затем сдвиг байтов или случайное перемешивание.

(Однако вы можете сделать это для 16-разрядных элементов с 16-разрядным pmullw, умножив его на степень 2 (или 1), если вы используйте 16-битные порции, либо AVX512BW с переменным числом или 16-битные сдвиги с маской слияния, либо 8-битные порции с pmaddubsw, чтобы даже объединить в нижней части элементы SIMD с правильными множителями: 1 и 1<<7 для младших и старших байтов каждой пары после маскировки сигнальных битов.)

Байты обычно являются ошибкой для материала BigInteger в целом . См. этот обзор обзора кода на BigInt класс в C ++ (который неразумно планировал хранить числа в массивах десятичных цифр ASCII). 30 битов значения в 32-битном блоке могут быть полезны для переносимых вещей (например, Python использует внутренне). Вы можете работать в база л ike 10 ^ 9, если вы выполняете много преобразований в / из десятичных строк, или 2 ^ 30, если нет.

Не использование всех битов на ветку позволяет отложить перенос (не нормализацию) для SIMD: Могут ли длинные целочисленные подпрограммы получить пользу от SSE? . Это может работать с 1 резервным битом для переноса и верхним битом, предназначенным для вашей стратегии сигнализации для неявной длины вместо хранения длины. (Многие инструкции x86 SIMD, такие как blendvps или movmskps, делают верхний бит SIMD-элемента dword особенным или каждого байта для целочисленного SIMD, например pmovmskb, pshufb и pblendvb. Таким образом, вы можете получить битовая маска старших битов, которые можно отсканировать с помощью bsf или bsr, чтобы найти первый установленный бит.)


Распаковка идей:

Если вы выберете старшую биту бит каждого байта = 0 в пределах целого числа, и 1 для обозначения конца, что позволяет избежать битовых наборов битов, которые необходимо очистить.

Как упоминалось ранее, pmaddubsw является хорошей идеей для SIMD объединять байты в 16-битные слова, с правильными множителями 1 и 1<<7.

Затем еще один шаг с pmaddwd может объединить слова в слова с 1, 1<<14, тогда вы настроены для AVX2 vpsrlv или просто сдвинуть и смешать. Все это затем принимает log2 (vector_length) число шагов вместо vector_length steps.

Без SIMD вы можете использовать x += x в качестве сдвига влево, но сделать это только сдвигом некоторых бит делая x += x & mask. Это работает для скаляра или с paddb, если у вас нет SSSE3 pmaddubsw. (Или с AVX512BW с байтовой маской vpaddb для меньшей задержки, чем у pmadd.)

   x = ... (bits) 0abcdefg 0ABCDEFG
   +  (hex) x & 0x...00FF00FF
                  0abcdefg ABCDEFG0

Это дает вам смежные 16-битные блоки, содержащие 14 битов значения. Однако на этот раз каждый блок разделяется двумя 0 битами, поэтому маскированное добавление - не самый эффективный способ продолжения. Вероятно, оттуда И с 0xFFFF0000FFFF0000 и смещением вправо на 2, затем маскируем оригинал другим способом и смешиваем с ИЛИ.


Десериализация этого формата с BMI2 pext Параллельный бит EXTract

pext медленный на AMD (микрокодированный, не выделенный HW, даже на Zen2), а BMI2 не везде доступен. Но на процессорах Intel это 1 моп, 3 такта, 1 тактовая частота. https://uops.info/

Обратите внимание, что вы можете сделать это в C со встроенными функциями : интерактивное руководство Intel по поиску / поиск . (Переносимость скалярных встроенных функций не SIMD между компиляторами может быть нечеткой, но для новых инструкций, таких как popcnt и BMI, в целом все в порядке.) Компиляторы могут не очень хорошо использовать btr для CSE x&(1<<7) и x &= ~(1<<7) в одной операции , но они должны обрабатывать этот код, даже если вы пишете что-то вроде (~mask) & x вместо встроенных. Хотя, вероятно, компиляторы будут выполнять постоянное распространение и материализовать инвертированную константу для and вместо andn.

Учитывая указатель на число неизвестной длины в этом формате, загрузите до 8 байтов и извлеките до 56 бит значения из них. (Предполагается, что загрузка qword безопасна: может загружать мусор, но не переходить на неотображенную страницу и ошибка: Безопасно ли читать после конца буфера в пределах одной и той же страницы на x86 и x64? )

; input pointer in RDI: a set bit 7 indicates end of number
; clobbers RCX, RDX
; result in RAX
;; great on Intel, probably can do better on AMD one byte at a time or with SIMD pmaddubsw 

    mov    rcx, [rdi]                     ; 8 bytes, including possible trailing garbage
    mov    rdx, 0x7F7F7F7F7F7F7F7F
    andn   rsi, rdx, rcx                  ; isolate high bits: (~rdx) & rcx

    blsmsk rax, rsi                       ; get mask up to lowest set bit: (rsi-1) XOR rsi = mask up to (and including) the first signal bit
    and    rax, rcx                       ; clear high garbage
       ; RAX = 0 above the number.  The end-of-number flag is still set but pext only grabs bits 6:0 from each byte.

    pext   rax, rax, rdx                  ; rax = 8x 7-bit fields packed down to low 56
   ; uint64_t result in RAX, from the first 1 to 8 bytes of [rdi],
   ; depending on where the first number-end flag was

Если ни один байт не имеет своего старшего установленного бита, blsmsk со входом «все ноль» создает выход «все единицы». Таким образом, мы извлекаем все 8 байтов для случая бесконечного числа, а также для случая, когда установлен верхний бит ввода.

andn и blsmsk - задержка одного цикла за один цикл , но они находятся в цепочке зависимостей, ведущей к pext, поскольку в этом одном блоке нет параллелизма на уровне команд за одну итерацию. Он довольно короткий, поэтому, если бы мы делали еще одну итерацию для 8-байтовых данных, OoO exe c могло бы хорошо перекрываться.

Было бы здорово, если бы мы могли запустить pext параллельно с вычислением маска, которую мы могли бы использовать на выводе вместо ввода. Но это соотношение 7: 8 является проблемой. Мы могли бы запустить pext дважды параллельно (с другой маской), чтобы выровнять старшие биты каждого байта там, где они необходимы для blsmsk. Или мы могли бы tzcnt найти позицию младшего установленного бита, а затем как-то умножить на 7/8. Позиция кратна 8, поэтому мы могли бы tzcnt и сделать x - (x>>3) или что-то еще, а затем использовать этот битовый индекс для BMI2 bzhi.

Если у вас есть упакованный поток чисел в этом формате , вы захотите найти, где начинается следующий. Из rsi изолированного шаблона конечного флага вы можете rsi = tzcnt(rsi) затем rsi >>= 3 найти индекс байта первого бита конца номера.

Вам нужно добавить еще 1 к этому go мимо этого. Вы можете сделать lea rdi, [rdi + rsi + 1], но это имеет дополнительную задержку по сравнению с inc rdi / add rdi, rsi из-за 3-компонентного LEA (две операции +).

Или если вы сместили маску влево до tzcnt, вы можете сделать это напрямую, и в качестве бонуса он будет трактовать без терминатора как 8 вместо 9.

    add   rsi, rsi               ; left-shift the end-of-number flags to the bottom of the next byte (or out)
    tzcnt rsi, rsi               ; rsi = bit-index of first bit of next number, 64 if RSI=0

 ; shrx  rcx, rcx, rsi           ; shift to the bottom and restart instead of reloading

    shr   esi, 3                 ; bit index -> byte index.  We know it's a small integer so 32-bit operand-size is fine and more saves code-size
    add   rdi, rsi               ; advance the pointer

Эта работа может выполняться параллельно с blsmsk и pext. Хотя, если мы все равно выполняем эту tzcnt работу, возможно, нам следует использовать bzhi вместо blsmsk / and. Это может быть лучше для пропускной способности, но хуже для задержки: add -> tzcnt - это 4 цикла задержки от готовности RSI до того, как вход готов к bzhi, и все это находится на критическом пути, ведущем к pext. против blsmsk / and только 2 цикла.


Или если мы хотим l oop, пока не найдем конец одного числа (более 8 байтов) , RSI по-прежнему содержит изолированные биты сигнала. Вот почему я сделал andn в RSI, а не в RAX.

    ... continuing from above to keep going until the end of large number
    ... do something with the 56-bit RAX chunks, like overlapping stores into memory?
    ; rsi still holds the isolated signal bits
    add    rdi, 8
    test   rsi, rsi
    jnz   .loop                     ; }while(end of number not found)

Или blsmsk устанавливает CF, если его вход был нулевым, поэтому, если бы мы могли структурировать наш l oop с помощью blsmsk внизу мы можем использовать это как ветвь l oop. Он также устанавливает флаги, так что, возможно, с l oop вращением и отслаиванием первых / последующих итераций


Секция BMI2 PEXT представляет собой несколько случайных идей, перемешанных вместе, а не одну согласованную полностью оптимизированную реализацию. Приспособьтесь по мере необходимости в зависимости от любых гарантий, которые вы можете сделать. например, будет полезна верхняя граница в 8 байтов на число.

Одна важная вещь, на которую следует обратить внимание, - это задержка l-1214 * -переданной цепи dep, включающей приращение указателя. Если для начала следующего числа задано большое время ожидания, exe c не по порядку не сможет чередовать работу многих итераций.


Полусвязанный, еще один бит проблема с упаковкой / распаковкой: Упаковка BCD в DPD: как улучшить эту процедуру сборки amd64?

...