Как конвертировать число в гекс?
Учитывая число в регистре (двоичное целое число), как преобразовать его в строку шестнадцатеричных цифр ASCII?
Цифры могут быть сохранены в памяти или распечатаны на лету, но хранение в памяти и одновременная печать обычно более эффективны. (Вы можете изменить цикл, который хранит, чтобы печатать по одному.)
Можем ли мы эффективно обрабатывать все кусочки параллельно с SIMD? (SSE2 или позже?)
3 ответа
16- степень 2. В отличие от десятичного числа ( Как вывести целое число в программировании на уровне сборки без printf из библиотеки c?) Или других оснований, которые не являются степенью 2, нам не нужно деление, и мы можем сначала извлеките наиболее значимую цифру вместо наименее значащей и считайте в обратном порядке.
Каждая 4-битная группа битов отображается в одну шестнадцатеричную цифру.Мы можем использовать сдвиги или повороты, а также маски AND, чтобы извлечь каждый 4-битный фрагмент ввода как 4-битное целое число.
К сожалению, шестнадцатеричные цифры 0..9 a..f не являются смежными в наборе символов ASCII ( http://www.asciitable.com/). Нам либо нужно условное поведение (ветвь или cmov), либо мы можем использовать таблицу поиска. Таблица поиска, как правило, наиболее эффективна для подсчета команд и производительности; современные процессоры имеют очень быстрые кэши 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"
До ИМТ2 (shrx
/rorx
), в x86 отсутствует инструкция копирования и сдвига, поэтому вращение на месте, а затем копирование /AND трудно превзойти. Современный x86 (Intel и AMD) имеет задержку в 1 цикл для поворотов ( https://agner.org/optimize/), поэтому цепочка зависимостей, переносимых циклами, не становится узким местом. (В цикле слишком много инструкций, чтобы он выполнялся даже по 1 циклу за итерацию даже на 5-кратном Ryzen.)
Даже если мы оптимизировали с помощью cmp / jb
с указателем конца, чтобы включить cmp/jcc fusion на 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
в целом меньше и эффективнее на большем количестве процессоров.
Тестовая программа:
// 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 и... цифр.)
Просто для удовольствия, эта версия начинается в конце буфера и уменьшает указатель. (И условие цикла использует сравнение указателей.) Вы можете остановить его, как только 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 и AVX512
Большинство из этих версий SIMD могут использоваться с двумя упакованными 32-разрядными целыми числами в качестве входных данных, причем младшие и старшие 8 байтов вектора результатов содержат отдельные результаты, которые можно хранить отдельно сmovq
а такжеmovhps
, В зависимости от вашего элемента управления в случайном порядке это похоже на использование его для одного 64-разрядного целого числа.
SSSE3pshufb
таблица параллельного поиска. Нет необходимости возиться с циклами, мы можем сделать это с помощью нескольких операций 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
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
, Наряду с побитовым И это все, что нам нужно, чтобы условно добавить что-то.
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
Эта версия требует больше векторных констант, чем большинство других. 4x 16 байтов - это 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: ; and AVX1. 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
или змм). Но, к сожалению, на CannonLake ( согласно результатам instlatx64) он все еще имеет задержку в 3 цикла, поэтому pshufb
было бы лучше для задержки. Но pshufb
требует маскировки вектора управления, так что это ухудшает пропускную способность, предполагая, vpermb
только 1 моп. В цикле, где мы можем хранить векторные константы в регистрах (вместо операндов памяти), он сохраняет только 1 инструкцию вместо 2.
Или с AVX512F, мы можем использовать маскирование слиянием, чтобы сдвинуть одно слово вправо, оставив другое неизменным, после передачи числа в регистр XMM. Тогда нам нужен только один регистр байтового перемешивания, vpshufb
, чередовать клев и обратный байт. Но тогда вам нужна константа в регистре масок, которая создает пару инструкций для создания. Это был бы больший выигрыш в цикле преобразования нескольких целых чисел в гекс.
Для нецикличной автономной версии функции я использовал две половины одной 16-байтовой константы для разных вещей: set1_epi8(0x0f)
в верхней половине, и 8 байтов pshufb
контрольный вектор в нижней половине. Это не сильно экономит, потому что операнды вещательной памяти EVEX позволяют vpandd xmm0, xmm0, dword [AND_mask]{1to4}
, требующий только 4 байта пространства для константы.
itohex_AVX512F: ;; and AVX1. Saves a pshufb. 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
С внутренними компонентами AVX2 или AVX-512
По запросу, перенос некоторых версий моего asm-ответа на C (который, как я написал, также будет действительным C ++). Ссылка на компилятор и обозреватель Godbolt. Они компилируются обратно в asm почти так же хорошо, как мой рукописный asm. (И я проверил, что векторные константы в сгенерированном компилятором asm соответствуют моему
db
директивы. Определенно что-то, что нужно проверить при переводе asm на встроенные функции, особенно если вы используете
_mm_set_
вместо констант, которые могут показаться более "естественными" в порядке наивысшего приоритета.
setr
использует порядок памяти, такой же, как asm.)
В отличие от моего 32-битного asm, они оптимизируются для того, чтобы их входной номер находился в регистре, не предполагая, что он все равно должен загружаться из памяти. (Таким образом, мы не предполагаем, что трансляция бесплатна.) Но TODO: исследуйте использование
bswap
вместо тасования SIMD для получения байтов в порядке печати. Особенно для 32-битных целых чисел, где bswap составляет всего 1 моп (против 2 у Intel для 64-битных регистров, в отличие от AMD).
Они печатают целое число в порядке печати MSD-first. Настройте константу множественного сдвига или элементы управления перемешиванием для вывода в памяти с прямым порядком байтов, как люди, очевидно, хотят выводить шестнадцатеричный вывод большого хэша. Или для версии SSSE3 просто удалите pshufb с обратным байтом.)
AVX2 / 512 также допускает более широкие версии, которые работают с 16 или 32 байтами ввода за раз, создавая 32 или 64 байта шестнадцатеричного вывода. Вероятно, путем перетасовки для повторения каждых 64 бита в 128-битной полосе в векторе с удвоенной шириной, например, с
<tcode id="4290012"></tcode> подобно
_mm256_permutex_epi64(_mm256_castsi128_si256(v), _MM_SHUFFLE(?,?,?,?))
.
AVX512VBMI (Ice Lake и новее)
#include <immintrin.h>
#include <stdint.h>
#if defined(__AVX512VBMI__) || defined(_MSC_VER)
// AVX512VBMI was new in Icelake
//template<typename T> // also works for uint64_t, storing 16 or 8 bytes.
void itohex_AVX512VBMI(char *str, uint32_t input_num)
{
__m128i v;
if (sizeof(input_num) <= 4) {
v = _mm_cvtsi32_si128(input_num); // only low qword needed
} else {
v = _mm_set1_epi64x(input_num); // bcast to both halves actually needed
}
__m128i multishift_control = _mm_set_epi8(32, 36, 40, 44, 48, 52, 56, 60, // high qword takes high 32 bits. (Unused for 32-bit input)
0, 4, 8, 12, 16, 20, 24, 28); // low qword takes low 32 bits
v = _mm_multishift_epi64_epi8(multishift_control, v);
// bottom nibble of each byte is valid, top holds garbage. (So we can't use _mm_shuffle_epi8)
__m128i hex_lut = _mm_setr_epi8('0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f');
v = _mm_permutexvar_epi8(v, hex_lut);
if (sizeof(input_num) <= 4)
_mm_storel_epi64((__m128i*)str, v); // 8 ASCII hex digits (u32)
else
_mm_storeu_si128((__m128i*)str, v); // 16 ASCII hex digits (u64)
}
#endif
Моя версия asm использовала 64-битную широковещательную загрузку своего аргумента стека из памяти даже для аргумента u32. Но это было только для того, чтобы я мог сложить загрузку в операнд источника памяти для
vpmultishiftqb
. Невозможно сообщить компилятору, что он может использовать 64-битный операнд источника широковещательной памяти, где старшие 32 бита будут «безразлично», если значение все равно поступало из памяти (и известно, что оно не находится в конце page перед неотображенной страницей, например, аргумент стека 32-битного режима). Так что эта небольшая оптимизация недоступна в C. И обычно после встраивания ваши вары будут в регистрах, и если у вас есть указатель, вы не узнаете, находится он в конце страницы или нет. Версия uint64_t делает необходимость трансляции, но так как объект в памяти является uint64_t компилятор может использовать
{1to2}
операнд источника памяти широковещательной передачи. (По крайней мере, clang и ICC достаточно умны, чтобы с
-m32 -march=icelake-client
, или в 64-битном режиме со ссылкой вместо значения arg.)
clang -O3 -m32
фактически компилируется так же, как и мой рукописный asm, за исключением
vmovdqa
load константы, а не, потому что в этом случае это действительно все, что нужно. Компиляторы недостаточно умны, чтобы использовать только
vmovq
загружает и пропускает 0 байтов из .rodata, когда верхние 8 байтов константы равны 0. Также обратите внимание, что константа множественного сдвига в выводе asm совпадает, поэтому
_mm_set_epi8
правильно; .
AVX2
При этом используется 32-разрядное целое число на входе; стратегия не работает для 64-битной версии (потому что для нее требуется сдвиг бит в два раза больше).
// Untested, and different strategy from any tested asm version.
// requires AVX2, can take advantage of AVX-512
// Avoids a broadcast, which costs extra without AVX-512, unless the value is coming from mem.
// With AVX-512, this just saves a mask or variable-shift constant. (vpbroadcastd xmm, reg is as cheap as vmovd, except for code size)
void itohex_AVX2(char *str, uint32_t input_num)
{
__m128i v = _mm_cvtsi32_si128(input_num);
__m128i hi = _mm_slli_epi64(v, 32-4); // input_num >> 4 in the 2nd dword
// This trick to avoid a shuffle only works for 32-bit integers
#ifdef __AVX512VL__
// UNTESTED, TODO: check this constant
v = _mm_ternarylogic_epi32(v, hi, _mm_set1_epi8(0x0f), 0b10'10'10'00); // IDK why compilers don't do this for us
#else
v = _mm_or_si128(v, hi); // the overlaping 4 bits will be masked away anyway, don't need _mm_blend_epi32
v = _mm_and_si128(v, _mm_set1_epi8(0x0f)); // isolate the nibbles because vpermb isn't available
#endif
__m128i nibble_interleave = _mm_setr_epi8(7,3, 6,2, 5,1, 4,0,
0,0,0,0, 0,0,0,0);
v = _mm_shuffle_epi8(v, nibble_interleave); // and put them in order into the low qword
__m128i hex_lut = _mm_setr_epi8('0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f');
v = _mm_shuffle_epi8(hex_lut, v);
_mm_storel_epi64((__m128i*)str, v); // movq 8 ASCII hex digits (u32)
}
Вышеупомянутое, я думаю, лучше, особенно на Haswell, но также и на Zen, где переменный сдвиг имеет более низкую пропускную способность и большую задержку, хотя это всего лишь один муп. Это лучше для узких мест внутреннего порта даже на Skylake: 3 инструкции, которые выполняются только на порту 5, по сравнению с 4 (включая
vmovd xmm, reg
`` и 2x) для версии ниже, но такое же количество интерфейсных мопов (при условии микро-слияния векторных констант в качестве операндов источника памяти). Также требуется на 1 векторную константу меньше, что всегда хорошо, особенно если это не цикл.
AVX-512 может использовать сдвиг с маской слияния вместо сдвига с переменным счетом, экономя одну векторную константу за счет необходимости настройки регистра маски. Это экономит место в
.rodata
но не удаляет все константы, поэтому промах в кеше все равно остановит это. И
mov r,imm
/
kmov k,r
составляет 2 мопса вместо 1 вне любого цикла, с которым вы его используете.
также AVX2: порт asm-версии itohex_AVX512F с идеей, которую я добавил позже.
// combining shuffle and AND masks into a single constant only works for uint32_t
// uint64_t would need separate 16-byte constants.
// clang and GCC wastefully replicate into 2 constants anyway!?!
// Requires AVX2, can take advantage of AVX512 (for cheaper broadcast, and alternate shift strategy)
void itohex_AVX2_slrv(char *str, uint32_t input_num)
{
__m128i v = _mm_set1_epi32(input_num);
#ifdef __AVX512VL__
// save a vector constant, at the cost of a mask constant which takes a couple instructions to create
v = _mm_mask_srli_epi32(v, 1<<3, v, 4); // high nibbles in the top 4 bytes, low nibbles unchanged.
#else
v = _mm_srlv_epi32(v, _mm_setr_epi32(0,0,0,4)); // high nibbles in the top 4 bytes, low nibbles unchanged.
#endif
__m128i nibble_interleave_AND_mask = _mm_setr_epi8(15,11, 14,10, 13,9, 12,8, // for PSHUFB
0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f); // for PAND
v = _mm_and_si128(v, nibble_interleave_AND_mask); // isolate the nibbles because vpermb isn't available
v = _mm_shuffle_epi8(v, nibble_interleave_AND_mask); // and put them in order into the low qword
__m128i hex_lut = _mm_setr_epi8('0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f');
v = _mm_shuffle_epi8(hex_lut, v);
_mm_storel_epi64((__m128i*)str, v); // movq 8 ASCII hex digits (u32)
}
По сравнению с версией SSSE3, это экономит
vpunpcklbw
используя (или маскированный сдвиг), чтобы получить байты
num>>4
и
num
в тот же регистр XMM для настройки перетасовки байтов с 1 регистром.
vpsrlvd
является одинарным в Skylake и более поздних версиях, а также в Zen 1 / Zen 2. Однако в Zen это более высокая задержка и, согласно https://uops.info/ , не полностью конвейерная (пропускная способность 2c вместо ожидаемой 1c). из-за того, что это один uop для одного порта.) Но, по крайней мере, он не конкурирует за тот же порт, что и и
vpbroadcastd xmm,xmm
на этих процессорах. (На Haswell это 2 мупа, включая один для p5, поэтому он действительно конкурирует, и это строго хуже, чем версия SSSE3, потому что для этого требуется дополнительная константа.)
Хорошим вариантом для Haswell может быть
_mm_slli_epi64(v, 32-4)
/
_mm_blend_epi32
-
vpblendd
работает на любом порту, не нуждаясь в случайном порте. Или, может быть, даже в целом, поскольку для этого нужна только настройка, а не
vmovd
+
vpbroadcastd
Для этой функции требуются две другие векторные константы (шестнадцатеричный lut и комбинированная маска AND и перетасовки). GCC и clang глупо «оптимизируют» 2 использования одной маски в 2 отдельные константы маски, что действительно глупо. (Но в цикле затраты только на установку и регистр, без дополнительных затрат на преобразование.) В любом случае вам понадобятся 2 отдельные 16-байтовые константы для
uint64_t
версия этой, но моя рукописная версия asm была умной, используя две половины одной 16-байтовой константы.
MSVC избегает этой проблемы: он компилирует встроенные функции более буквально и не пытается их оптимизировать (что часто плохо, но здесь позволяет избежать этой проблемы). Но MSVC упускает возможность использования AVX-512 GP-register-source
<tcode id="4290047"></tcode> за
_mm_set1_epi32
с
-arch:AVX512
. С
-arch:AVX2
(поэтому трансляция должна выполняться двумя отдельными инструкциями) он использует эту векторную константу в качестве операнда источника памяти дважды (для
vpand
и
vpshufb
) вместо загрузки в регистр, что довольно сомнительно, но, вероятно, нормально и на самом деле сохраняет внешние ошибки. IDK, что он будет делать в цикле, где подъем груза более очевиден.
Письмо
hex_lut
компактнее:
hex_lut = _mm_loadu_si128((const __m128i*)"0123456789abcdef");
полностью эффективно компилируется с помощью GCC и Clang (они эффективно оптимизируют строковый литерал с его завершающим 0 и просто генерируют выровненную векторную константу). Но MSVC, к сожалению, сохраняет фактическую строку в .rdata, не выравнивая ее. Поэтому я использовал более длинный, менее приятный для чтения,
_mm_setr_epi8('0', '1', ..., 'f');
шустро это
section .data
msg resb 8
db 10
hex_nums db '0123456789ABCDEF'
xx dd 0FF0FEFCEh
length dw 4
section .text
global main
main:
mov rcx, 0
mov rbx, 0
sw:
mov ah, [rcx + xx]
mov bl, ah
shr bl, 0x04
mov al, [rbx + hex_nums]
mov [rcx*2 + msg], al
and ah, 0x0F
mov bl, ah
mov ah, [rbx + hex_nums]
mov [rcx*2 + msg + 1], ah
inc cx
cmp cx, [length]
jl sw
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, 9 ;8 + 1
syscall
mov rax, 60
mov rdi, 0
syscall
nasm -f elf64 x.asm -o t.o
gcc -no-pie t.o -o t