Как конвертировать число в гекс? - PullRequest
0 голосов
/ 18 декабря 2018

Учитывая число в регистре (двоичное целое число), как преобразовать его в строку шестнадцатеричных цифр ASCII?

Цифры могут быть сохранены в памяти или распечатаны на лету, но сохранены в памяти ипечать за один раз обычно более эффективна.(Вы можете изменить цикл, который хранится, чтобы печатать по одному за раз.)

Можем ли мы эффективно обрабатывать все кусочки параллельно с SIMD?(SSE2 или позже?)

1 Ответ

0 голосов
/ 18 декабря 2018

16 - это степень 2. В отличие от десятичного числа ( Как вывести целое число в Программирование на уровне сборки без printf из библиотеки c? ) или других оснований, которые не являются степенью 2, мыне нужно делить, и мы можем сначала извлечь старшую значащую цифру вместо самой младшей и считать в обратном порядке.

Каждая 4-битная группа битов отображается в одну шестнадцатеричную цифру.Мы можем использовать сдвиги или повороты, а также маски AND, чтобы извлечь каждый 4-битный блок ввода как 4-битное целое число.

К сожалению, шестнадцатеричные цифры 0..9 a..fне смежный в наборе символов ASCII (http://www.asciitable.com/). Нам нужно либо условное поведение (ветвь или cmov), либо мы можем использовать таблицу поиска. Таблица поиска, как правило, наиболее эффективна для подсчета команд и производительности; современные процессорыимеют очень быстрые кэши L1d, которые делают повторные загрузки соседних байтов очень дешевыми. Конвейерное / неупорядоченное выполнение скрывает ~ 5 циклов задержки загрузки кэша L1d.

;; NASM syntax, i386 System V calling convention
global itohex
itohex:   ; inputs: char* output,  unsigned number
    push   edi           ; save a call-preserved register for scratch space
    mov    edi, [esp+8]  ; out pointer
    mov    eax, [esp+12] ; number

    mov    ecx, 8        ; 8 hex digits, fixed width zero-padded
.digit_loop:             ; do {
    rol    eax, 4          ; rotate the high 4 bits to the bottom

    mov    edx, eax
    and    edx, 0x0f       ; and isolate 4-bit integer in EDX

    movzx  edx, byte [hex_lut + edx]
    mov    [edi], dl       ; copy a character from the lookup table
    inc    edi             ; loop forward in the output buffer

    dec    ecx
    jnz    .digit_loop   ; }while(--ecx)

    pop    edi
    ret

section .rodata
    hex_lut:  db  "0123456789abcdef"

До BMI2 (shrx/ rorx), в x86 отсутствует инструкция копирования и сдвига, поэтому вращение на месте, а затем копирование / AND трудно превзойти 1 . Современные x86 (Intel и AMD) имеют задержку в 1 циклдля поворотов (https://agner.org/optimize/),, так что эта цепочка зависимостей, переносимая циклами, не становится узким местом. (В цикле слишком много инструкций, чтобы он мог выполняться даже по 1 циклу на итерацию даже для Ryzen шириной 5).

Даже если шОптимизирован с помощью cmp / jb с указателем конца для включения слияния cmp / jcc в Ryzen, это все еще 7 моп, больше, чем конвейер может обработать за 1 цикл.dec/jcc макрослияние в один моп происходит только в семействе Intel Sandybridge;AMD только сливает cmp или тест с jcc.Я использовал mov ecx,8 и dec / jnz для удобства чтения;lea ecx, [edi+8] и cmp edi, ecx / jb .digit_loop в целом меньше и более эффективны на большем количестве процессоров.

Сноска 1: Мы могли бы использовать SWAR (SIMD в регистре), чтобы выполнить AND перед сдвигом: x & 0x0f0f0f0fнизкий клочок и shr(x,4) & 0x0f0f0f0f высокий клочок , затем эффективно развернуть, чередуя обработку байта из каждого регистра.(Без какого-либо эффективного способа сделать эквивалент punpcklbw или сопоставления целых чисел с несмежными кодами ASCII, нам все равно нужно просто делать каждый байт отдельно. Но мы могли бы развернуть извлечение байтов и прочитать AH, а затем AL (сmovzx) для сохранения команд смены. Чтение регистров с большими 8 может добавить задержку, но я думаю, что это не потребует дополнительных мопов на текущих процессорах. Запись регистров с высокими 8 обычно не годится для процессоров Intel: это требует дополнительного слиянияUOP, чтобы прочитать полный регистр, с внешней задержкой, чтобы вставить его. Так что получение более широких хранилищ путем перестановки регистров, вероятно, не очень хорошо. В коде ядра, где вы не можете использовать регистры XMM, но можете использовать BMI2, если доступно, pdep может расширить клочки до байтов, но это, вероятно, хуже, чем просто маскировка 2-х способов.)

Тестовая программа:

// hex.c   converts argv[1] to integer and passes it to itohex
#include <stdio.h>
#include <stdlib.h>

void itohex(char buf[8], unsigned num);

int main(int argc, char**argv) {
    unsigned num = strtoul(argv[1], NULL, 0);  // allow any base
    char buf[9] = {0};
    itohex(buf, num);   // writes the first 8 bytes of the buffer, leaving a 0-terminated C string
    puts(buf);
}

скомпилировать с:

nasm -felf32 -g -Fdwarf itohex.asm
gcc -g -fno-pie -no-pie -O3 -m32 hex.c itohex.o

тестовых прогонов:

$ ./a.out 12315
0000301b
$ ./a.out 12315123
00bbe9f3
$ ./a.out 999999999
3b9ac9ff
$ ./a.out 9999999999   # apparently glibc strtoul saturates on overflow
ffffffff
$ ./a.out 0x12345678   # strtoul with base=0 can parse hex input, too
12345678

Альтернативные реализации:

Условно вместо таблицы поиска : требуется еще несколько инструкций и будетобычно медленнееНо для этого не нужны статические данные.Это можно сделать с помощью ветвления вместо cmov, но в большинстве случаев это будет еще медленнее.(Это не будет хорошо предсказано, если принять случайное сочетание 0,9 и цифр.)

Просто для удовольствия, эта версия начинается в конце буфера и уменьшается на aуказатель .(И условие цикла использует сравнение указателей.) Вы можете остановить его, как только EDX станет равным нулю, и использовать EDI + 1 в качестве начала числа, если вы не хотите, чтобы начальные нули были.

Использованиеcmp eax,9 / ja вместо cmov оставлено в качестве упражнения для читателя.В 16-битной версии этого могут использоваться другие регистры (например, BX в качестве временного), чтобы разрешить lea cx, [bx + 'a'-10] копирование и добавление.Или просто add / cmp и jcc, если вы хотите избежать cmov для сравнения с древними процессорами, которые не поддерживают расширения P6.

;; NASM syntax, i386 System V calling convention
itohex:   ; inputs: char* output,  unsigned number
itohex_conditional:
    push   edi             ; save a call-preserved register for scratch space
    push   ebx
    mov    edx, [esp+16]   ; number
    mov    ebx, [esp+12]   ; out pointer

    lea    edi, [ebx + 7]   ; First output digit will be written at buf+7, then we count backwards
.digit_loop:                ; do {
    mov    eax, edx
    and    eax, 0x0f            ; isolate the low 4 bits in EAX
    lea    ecx, [eax + 'a'-10]  ; possible a..f value
    add    eax, '0'             ; possible 0..9 value
    cmp    ecx, 'a'
    cmovae eax, ecx             ; use the a..f value if it's in range.
                                ; for better ILP, another scratch register would let us compare before 2x LEA,
                                ;  instead of having the compare depend on an LEA or ADD result.

    mov    [edi], al        ; *ptr-- = c;
    dec    edi

    shr    edx, 4

    cmp    edi, ebx         ; alternative:  jnz on flags from EDX to not write leading zeros.
    jae    .digit_loop      ; }while(ptr >= buf)

    pop    ebx
    pop    edi
    ret

Проверьте наличие ошибок off-1, используя число с шестнадцатеричными цифрами 9 и a:

$ nasm -felf32 -g -Fdwarf itohex.asm && gcc -g -fno-pie -no-pie -O3 -m32 hex.c itohex.o && ./a.out 0x19a2d0fb
19a2d0fb

SIMD с SSE2, SSSE3, AVX2 илиAVX512F и ~ 2 инструкции с AVX512VBMI

С SSSE3 и более поздними версиями лучше использовать байтовый случайный порядок в качестве таблицы поиска полубайтов.

Большинство этих версий SIMD можно использовать с двумя упакованными 32-битные целые числа в качестве входных данных, причем младшие и старшие 8 байтов вектора результатов содержат отдельные результаты, которые можно хранить отдельно с помощью movq и movhps.В зависимости от вашего управления перемешиванием это похоже на использование его для одного 64-разрядного целого числа.

SSSE3 pshufb таблица параллельного просмотра .Не нужно возиться с циклами, мы можем сделать это с помощью нескольких операций SIMD на процессорах с pshufb.(SSSE3 не является базовым даже для x86-64; он был новинкой для Intel Core2 и AMD Bulldozer).

pshufb - это байтовый случайный порядок , который контролируется вектором, а не непосредственным(в отличие от всех предыдущих SSE1 / SSE2 / SSE3 тасует).С фиксированным назначением и переменным управлением шаффлом, мы можем использовать его в качестве параллельной таблицы поиска для параллельного 16-кратного поиска (из 16-байтовой таблицы байтов в векторе).

Таким образом, мы загружаем всецелое число в векторный регистр, и распаковывает его кусочки в байты со сдвигом битов и punpcklbw.Затем используйте pshufb, чтобы сопоставить эти полубайты с шестнадцатеричными цифрами.

В результате мы получим ASCII-цифры из регистра XMM с наименьшей значащей цифрой в качестве младшего байта регистра.Поскольку x86 имеет младший порядок, нет свободного способа сохранить их в памяти в обратном порядке, в первую очередь с MSB.

Мы можем использовать дополнительные pshufb, чтобы переупорядочить байты ASCII в порядке печати, илииспользуйте bswap на входе в целочисленном регистре (и поменяйте местами полубайт -> распаковка байтов).Если целое число исходит из памяти, то проходит через регистр целых чисел для bswap вроде отстой (особенно для семейства AMD Bulldozer), но если у вас есть целое число в регистре GP, это довольно хорошо.

;; NASM syntax, i386 System V calling convention

section .rodata
 align 16
    hex_lut:  db  "0123456789abcdef"
    low_nibble_mask: times 16 db 0x0f
    reverse_8B: db 7,6,5,4,3,2,1,0,   15,14,13,12,11,10,9,8
    ;reverse_16B: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

section .text

global itohex_ssse3    ; tested, works
itohex_ssse3:
    mov    eax,  [esp+4]    ; out pointer
    movd   xmm1, [esp+8]    ; number

    movdqa xmm0, xmm1
    psrld  xmm1, 4          ; right shift: high nibble -> low  (with garbage shifted in)
    punpcklbw xmm0, xmm1    ; interleave low/high nibbles of each byte into a pair of bytes
    pand   xmm0, [low_nibble_mask]   ; zero the high 4 bits of each byte (for pshufb)
    ; unpacked to 8 bytes, each holding a 4-bit integer

    movdqa xmm1, [hex_lut]
    pshufb xmm1, xmm0       ; select bytes from the LUT based on the low nibble of each byte in xmm0

    pshufb xmm1, [reverse_8B]  ; printing order is MSB-first

    movq   [eax], xmm1      ; store 8 bytes of ASCII characters
    ret
;; The same function for 64-bit integers would be identical with a movq load and a movdqu store.
;; but you'd need reverse_16B instead of reverse_8B to reverse the whole reg instead of each 8B half

Можно упаковать маску AND и элемент управления pshufb в один 16-байтовый вектор, аналогично itohex_AVX512F ниже.

AND_shuffle_mask: times 8 db 0x0f       ; low half: 8-byte AND mask
                   db 7,6,5,4,3,2,1,0   ; high half: shuffle constant that will grab the low 8 bytes in reverse order

Загрузите его в векторный регистр и используйте его в качестве маски AND, затем используйте его в качестве pshufb элемента управления, чтобы захватить младшие 8 байтов в обратном порядке, оставив их в старшем 8. Вашокончательный результат (8 шестнадцатеричных ASCII-цифр) будет в верхней половине регистра XMM, поэтому используйте movhps [eax], xmm1.На процессорах Intel это всего лишь 1 UOP с плавким доменом, поэтому он так же дешев, как movq.Но на Ryzen, это стоит перетасовки на вершине магазина.Кроме того, этот прием бесполезен, если вы хотите преобразовать два целых числа параллельно или 64-разрядное целое число.

SSE2, гарантированно доступно в x86-64 :

Без SSSE3 pshufb нам нужно полагаться на скаляр bswap, чтобы расположить байты в правильном порядке печати, а punpcklbw - другой способ чередования с большим полубайтом каждой пары.

Вместопоиск таблицы, мы просто добавляем '0' и добавляем еще 'a' - ('0'+10) для цифр больше 9 (чтобы поместить их в диапазон 'a'..'f').SSE2 имеет упакованное сравнение байтов для больше, чем pcmpgtb.Наряду с побитовым AND, это все, что нам нужно для условного добавления чего-либо.

itohex:             ; tested, works.
global itohex_sse2
itohex_sse2:
    mov    edx,  [esp+8]    ; number
    mov    ecx,  [esp+4]    ; out pointer
    ;; or enter here for fastcall arg passing.  Or rdi, esi for x86-64 System V.  SSE2 is baseline for x86-64
    bswap  edx
    movd   xmm0, edx

    movdqa xmm1, xmm0
    psrld  xmm1, 4          ; right shift: high nibble -> low  (with garbage shifted in)
    punpcklbw xmm1, xmm0    ; interleave high/low nibble of each byte into a pair of bytes
    pand   xmm1, [low_nibble_mask]   ; zero the high 4 bits of each byte
    ; unpacked to 8 bytes, each holding a 4-bit integer, in printing order

    movdqa  xmm0, xmm1
    pcmpgtb xmm1, [vec_9]
    pand    xmm1, [vec_af_add] ; digit>9 ?  'a'-('0'+10)  :  0

    paddb   xmm0, [vec_ASCII_zero]
    paddb   xmm0, xmm1      ; conditional add for digits that were outside the 0..9 range, bringing them to 'a'..'f'

    movq   [ecx], xmm0      ; store 8 bytes of ASCII characters
    ret
    ;; would work for 64-bit integers with 64-bit bswap, just using movq + movdqu instead of movd + movq


section .rodata
align 16
    vec_ASCII_zero: times 16 db '0'
    vec_9:          times 16 db 9
    vec_af_add:     times 16 db 'a'-('0'+10)
    ; 'a' - ('0'+10) = 39 = '0'-9, so we could generate this from the other two constants, if we were loading ahead of a loop
    ; 'A'-('0'+10) = 7 = 0xf >> 1.  So we could generate this on the fly from an AND.  But there's no byte-element right shift.

    low_nibble_mask: times 16 db 0x0f

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

Это может быть реализовано только с MMX, с использованием только 8-байтовых констант, но тогда вам понадобится emms, так что это, вероятно, будет хорошей идеей только на очень старых процессорах, которые не имеют SSE2, иликоторые разбивают 128-битные операции на 64-битные половины (например, Pentium-M или K8).На современных процессорах с устранением mov для векторных регистров (таких как Bulldozer и IvyBrige) он работает только на регистрах XMM, а не MMX.Я организовал использование регистра таким образом, чтобы 2-й movdqa находился вне критического пути, но я не делал этого для первого.


AVX может сохранить movdqa, но более интересным являетсяс AVX2 мы можем потенциально генерировать 32 байта шестнадцатеричных цифр за раз из больших входных данных .2x 64-разрядных целых или 4x 32-разрядных целых числа;используйте 128-> 256-битную широковещательную загрузку для репликации входных данных в каждую полосу.Оттуда, на линии vpshufb ymm с вектором управления, который считывает нижнюю или верхнюю половину каждой 128-битной полосы, вы должны получить полубайты для младших 64 битов ввода, распакованных на нижней полосе, и полубайтыдля старших 64 битов входных данных, распакованных в верхнем ряду.

Или, если входные числа поступают из разных источников, возможно, vinserti128 старший может стоить на некоторых процессорах,По сравнению с просто выполнением отдельных 128-битных операций.


AVX512VBMI (Cannonlake, отсутствует в Skylake-X) имеет 2-регистровый байт в случайном порядке vpermt2b, которые могут сочетать чередование puncklbw с обращением байтов. Или, что еще лучше, у нас есть VPMULTISHIFTQB, который может извлечь 8 невыровненных байтов из каждого qword источника .

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

Чтобы использовать это для 64-битных целых чисел, используйте источник широковещания и управляющий вектор, который захватывает старшие 32 бита входного qword в нижней части вектора.и младшие 32 бита в верхней части вектора.

Чтобы использовать это для более чем 64-битного ввода, используйте vpmovzxdq для расширения нуля каждого входного слова в qword , настраивая для vpmultishiftqb с тем же 28,24, ..., 4,0 шаблоном управления в каждом слове.(например, создание вектора вывода zmm из 256-битного вектора ввода или четырех слов -> регистр ymm, чтобы избежать ограничений тактовой частоты и других эффектов фактического выполнения 512-битной инструкции AVX512.)

itohex_AVX512VBMI:                         ;  Tested with SDE
    vmovq          xmm1, [multishift_control]
    vpmultishiftqb xmm0, xmm1, qword [esp+8]{1to2}    ; number, plus 4 bytes of garbage.  Or a 64-bit number
    mov    ecx,  [esp+4]            ; out pointer

     ;; VPERMB ignores high bits of the selector byte, unlike pshufb which zeroes if the high bit is set
     ;; and it takes the bytes to be shuffled as the optionally-memory operand, not the control
    vpermb  xmm1, xmm0, [hex_lut]   ; use the low 4 bits of each byte as a selector

    vmovq   [ecx], xmm1     ; store 8 bytes of ASCII characters
    ret
    ;; For 64-bit integers: vmovdqa load [multishift_control], and use a vmovdqu store.

section .rodata
align 16
    hex_lut:  db  "0123456789abcdef"
    multishift_control: db 28, 24, 20, 16, 12, 8, 4, 0
    ; 2nd qword only needed for 64-bit integers
                        db 60, 56, 52, 48, 44, 40, 36, 32

# I don't have an AVX512 CPU, so I used Intel's Software Development Emulator
$ /opt/sde-external-8.4.0-2017-05-23-lin/sde -- ./a.out 0x1235fbac
1235fbac

vpermb xmm не является пересечением полос, поскольку задействована только одна полоса (в отличие от vpermb ymm или zmm).Но, к сожалению, на CannonLake ( в соответствии с результатами instlatx64 ) он все еще имеет 3-тактную задержку, поэтому pshufb будет лучше для задержки.Но pshufb условно нули на основе старшего бита, поэтому требуется маскировка вектора управления.Это ухудшает пропускную способность, предполагая, что vpermb xmm составляет всего 1 моп.В цикле, где мы можем хранить векторные константы в регистрах (вместо операндов памяти), он сохраняет только 1 инструкцию вместо 2.


AVX2 variable-shift или AVX512F merge-mask для сохранения чередования

С AVX512F мы можем использовать маскирование слиянием, чтобы сдвинуть один меч вправо, оставив другой без изменений, после передачи числа в регистр XMM.

Или мы могли бы использоватьAVX2 переменная-смещение vpsrlvd, чтобы сделать то же самое , с вектором отсчета смещения [4, 0, 0, 0].Intel Skylake и более поздние имеют однопроцессорную vpsrlvd;Haswell / Broadwell принимают несколько мопов (2p0 + p5).* * * * Ризен vpsrlvd xmm равен 1 моп, задержка 3 с, пропускная способность 1 на 2 такта.(Хуже, чем немедленные сдвиги).

Тогда нам понадобится только один регистр байтового тасования, vpshufb, для чередования полубайтов и обратного байта.Но тогда вам нужна константа в регистре масок, которая создает пару инструкций для создания.Это был бы больший выигрыш в цикле преобразования нескольких целых чисел в гекс.

Для нецикличной автономной версии функции я использовал две половины одной 16-байтовой константы для разных вещей: set1_epi8(0x0f) в верхней половине и 8 байтов pshufb управляющего вектора в нижнейполовина.Это не сильно экономит, потому что операнды вещательной памяти EVEX допускают vpandd xmm0, xmm0, dword [AND_mask]{1to4}, требуя только 4 байта пространства для константы.

itohex_AVX512F:       ;; Saves a punpcklbw.  tested with SDE
    vpbroadcastd  xmm0, [esp+8]    ; number.  can't use a broadcast memory operand for vpsrld because we need merge-masking into the old value
    mov     edx, 1<<3             ; element #3
    kmovd   k1, edx
    vpsrld  xmm0{k1}, xmm0, 4      ; top half:  low dword: low nibbles unmodified (merge masking).  2nd dword: high nibbles >> 4

    vmovdqa xmm2, [nibble_interleave_AND_mask]
    vpand   xmm0, xmm0, xmm2     ; zero the high 4 bits of each byte (for pshufb), in the top half
    vpshufb xmm0, xmm0, xmm2     ; interleave nibbles from the high two dwords into the low qword of the vector

    vmovdqa xmm1, [hex_lut]
    vpshufb xmm1, xmm1, xmm0       ; select bytes from the LUT based on the low nibble of each byte in xmm0

    mov      ecx,  [esp+4]    ; out pointer
    vmovq   [ecx], xmm1       ; store 8 bytes of ASCII characters
    ret

section .rodata
align 16
    ;hex_lut:  db  "0123456789abcdef"
    nibble_interleave_AND_mask: db 15,11, 14,10, 13,9, 12,8  ; shuffle constant that will interleave nibbles from the high half
                      times 8 db 0x0f              ; high half: 8-byte AND mask
...