给出寄存器中的数字(二进制整数),如何将其转换为十六进制ASCII数字字符串?
数字既可以存储在内存中,也可以即时打印,但是通常存储在内存中并一次打印通常会更有效。 (您可以修改存储的循环以一次打印一个。)
我们可以与SIMD并行有效地处理所有半字节吗? (SSE2或更高版本?)
答案 0 :(得分:9)
16是2的幂。与十进制(How do I print an integer in Assembly Level Programming without printf from the c library?)或其他不是2的幂的基数不同,我们不需要除法,并且我们可以首先提取最高有效位而不是最低有效数字并倒数。
每个4位位组映射到一个十六进制数字。我们可以使用移位或旋转以及AND掩码将输入的每个4位块提取为4位整数。
不幸的是,0..9 a..f十六进制数字在ASCII字符集(http://www.asciitable.com/)中不连续。我们要么需要条件行为(分支或cmov),要么可以使用查找表。查找表通常是最有效的指令计数和性能。现代CPU具有非常快的L1d高速缓存,这使得附近字节的重复加载非常便宜。流水线/无序执行隐藏了L1d缓存负载的〜5个周期延迟。
;; 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/),因此,此循环传输的依赖链不会成为瓶颈。 (循环中有太多指令,即使在5宽Ryzen上,每个迭代甚至无法运行1个周期。)
即使我们通过使用带有结尾指针的cmp / jb
在Ryzen上启用cmp / jcc融合进行了优化,它仍然是7 oups,比管道在1个周期内可以处理的更多。 dec/jcc
宏融合到单个uop中仅发生在Intel Sandybridge系列上; AMD仅将cmp融合或与jcc进行测试。我使用mov ecx,8
和dec / jnz来提高可读性; lea ecx, [edi+8]
和cmp edi, ecx / jb .digit_loop
总体上较小,在更多CPU上效率更高。
脚注1:我们可能会在移位x & 0x0f0f0f0f
低半字节和shr(x,4) & 0x0f0f0f0f
高半字节之前使用SWAR(寄存器中的SIMD)进行与操作,然后有效地展开通过交替处理每个寄存器中的一个字节。 (没有任何有效的方法来等效于punpcklbw
或将整数映射到非连续的ASCII代码,我们仍然只需要分别处理每个字节。但是我们可能需要展开字节提取并读取AH然后再读取AL (使用movzx
)来保存移位指令。读取高8位寄存器会增加延迟,但我认为在当前CPU上并不会花费额外的成本。在英特尔CPU上写入高8位寄存器通常是不好的:花费额外的合并uop读取完整的寄存器,并插入前端会有延迟。因此,通过改组寄存器来扩大存储范围可能不是一件好事。在内核代码中,您不能使用XMM reg,但是可以使用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..f数字随机混合,则预测效果不佳。)
很有趣,此版本从缓冲区的末尾开始并减少指针。 (并且循环条件使用指针比较。)如果您不希望前导零,则可以使它在EDX变为零后停止,并使用EDI + 1作为数字的开头。
使用cmp eax,9
/ ja
代替cmov
留给读者练习。其16位版本可以使用不同的寄存器(例如BX作为临时寄存器)来仍然允许lea cx, [bx + 'a'-10]
复制和添加。如果您想避免add
与不支持P6扩展的古老CPU兼容,或者只是cmp
/ jcc
和cmov
。
;; 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
使用十六进制数字为9
和a
的数字检查1错误:
$ nasm -felf32 -g -Fdwarf itohex.asm && gcc -g -fno-pie -no-pie -O3 -m32 hex.c itohex.o && ./a.out 0x19a2d0fb
19a2d0fb
对于SSSE3和更高版本,最好将字节混洗用作半字节查找表。
大多数这些SIMD版本都可以使用两个压缩的32位整数作为输入,结果向量的低8位和高8位包含单独的结果,可以分别用movq
和{{1 }}。
根据您的随机播放控件,这就像将其用于一个64位整数一样。
SSSE3 movhps
并行查找表。无需弄乱循环,我们可以在具有pshufb
的CPU上执行一些SIMD操作。 (SSSE3甚至不是x86-64的基准;它是Intel Core2和AMD Bulldozer的新功能。)
pshufb
is a byte shuffle由向量控制,而不是由立即数控制(与所有早期的SSE1 / SSE2 / SSE3随机播放不同)。具有固定的目标位置和可变的shuffle控件,我们可以将其用作并行查找表,以并行方式执行16x查找(从向量中的16个字节的条目表开始)。
因此,我们将整个整数加载到向量寄存器中,并通过移位和punpcklbw
将其半字节解压缩为字节。然后使用pshufb
将这些半字节映射到十六进制数字。
剩下的是ASCII码,这是XMM寄存器,其最低有效位是寄存器的最低字节。由于x86是低位字节序,因此没有免费的方法可以将它们以相反的顺序存储到内存中,首先是MSB。
我们可以使用额外的pshufb
将ASCII字节重新排序为打印顺序,或者在整数寄存器的输入上使用pshufb
(并反转半字节->字节解包)。如果该整数来自内存,则通过bswap
的整数寄存器有点糟(尤其是对于AMD Bulldozer系列),但是如果您首先在GP寄存器中拥有该整数,那就很好了。>
bswap
可以将AND掩码和pshufb控件打包到一个16字节向量中,类似于下面的;; 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
。
itohex_AVX512F
将其加载到向量寄存器中,并将其用作AND掩码,然后将其用作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
控件,以相反的顺序获取低8个字节,将其保留为高8。最后的结果(8 ASCII十六进制数字)将位于XMM寄存器的上半部分,因此请使用pshufb
。在Intel CPU上,这仍然只是1个融合域uop,因此它与movhps [eax], xmm1
一样便宜。但是在Ryzen上,它需要在商店顶部进行洗牌。另外,如果您要并行转换两个整数或64位整数,则此技巧没有用。
SSE2,保证在x86-64中可用:
在没有SSSE3 movq
的情况下,我们需要依靠标量pshufb
将字节以正确的打印顺序放置,并且bswap
的另一种方式是首先与每对的高半字节交织
我们只需要添加punpcklbw
,然后为大于9的数字添加另一个'0'
(而不是查找表)(将它们置于'a' - ('0'+10)
范围内)即可。 SSE2的压缩字节比较大于pcmpgtb
。加上按位与,这就是我们有条件添加的全部内容。
'a'..'f'
此版本比其他大多数版本需要更多的向量常量。 4x 16字节为64字节,可容纳在一个缓存行中。您可能想在第一个向量之前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
而不是align 64
,所以它们都来自同一缓存行。
这甚至可以仅使用MMX来实现,仅使用8字节常量,但是您需要一个align 16
,所以这可能仅在没有SSE2的非常老的CPU上是一个好主意,或将128位操作分成64位(例如Pentium-M或K8)。在具有消除运动的矢量寄存器的现代CPU(如Bulldozer和IvyBrige)上,它仅适用于XMM寄存器,不适用于MMX。我确实安排了寄存器的使用,因此第二个emms
不在关键路径上,但是我没有第一次这样做。
AVX可以保存movdqa
,但更有趣的是 AVX2,我们可以一次从大量输入中一次生成32个字节的十六进制数字。 2x 64位整数或4x 32位整数;使用128-> 256位广播负载将输入数据复制到每个通道中。从那里开始,带有控制矢量的车道movdqa
从每个128位通道的低半或高半部分读取,应该为低通道中的低64位输入设置零字节,并且在高通道中解压输入的高64位输入。
或者,如果输入数字来自不同的来源,那么vpshufb ymm
可能在某些CPU上是{em>可能最高的价格,而不是仅仅执行128位操作。
AVX512VBMI (Cannonlake,Skylake-X中不存在)具有2寄存器字节混洗vpermt2b
,可以将vinserti128
与字节倒车。 甚至更好的是,我们有VPMULTISHIFTQB
可以从源的每个qword提取8个未对齐的字节。
我们可以使用它来将所需的半字节直接提取为所需的顺序,从而避免了单独的右移指令。 (不过,它仍然带有垃圾位。)
要将其用于64位整数,请使用广播源和控制矢量,该矢量在矢量底部捕获输入qword的高32位,在矢量顶部捕获低32位。 / p>
要将其用于64位以上的输入,请使用puncklbw
将每个输入dword零扩展为qword ,并为vpmovzxdq
设置相同的28每个qword中的,24,...,4,0控制模式。 (例如,从输入的256位向量或四个dword-> ymm reg生成输出的zmm向量,以避免时钟速度限制和实际运行512位AVX512指令的其他影响。)
vpmultishiftqb
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
或zmm不同)。但是不幸的是,在CannonLake(according to instlatx64 results)上,它仍然具有3个周期的延迟,因此vpermb ymm
的延迟会更好。但是pshufb
根据高位有条件地为零,因此需要屏蔽控制向量。假设pshufb
仅1 uop,这会使吞吐量更糟。在可以将向量常量保存在寄存器中(而不是内存操作数)的循环中,它只保存1条指令,而不是2条指令。
使用AVX512F,在将数字广播到XMM寄存器中之后,我们可以使用合并掩码右移一个双字,而另一个双字保持不变。
或者我们可以使用AVX2可变移位vpermb xmm
来做完全相同的事情,移位计数向量为vpsrlvd
。英特尔Skylake及更高版本具有单处理器[4, 0, 0, 0]
; Haswell / Broadwell取多个uops(2p0 + p5)。 Ryzen的vpsrlvd
为1 uop,延迟为3c,每2时钟吞吐量为1。 (比即时变化更糟糕)。
这时,我们只需要一个单寄存器字节混洗vpsrlvd xmm
来交织半字节和字节反转。但是然后您需要一个掩码寄存器中的常数,该常数需要几个指令来创建。在将多个整数转换为十六进制的循环中,这将是更大的胜利。
对于该函数的非循环独立版本,我将两半的一个16字节常量用于不同的事情:vpshufb
位于上半部,而8字节是set1_epi8(0x0f)
控件下半部分的向量。这并不会节省很多,因为EVEX广播内存操作数允许pshufb
,只需要4个字节的空间即可。
vpandd xmm0, xmm0, dword [AND_mask]{1to4}
答案 1 :(得分:2)
根据要求,将我的 asm 答案的某些版本移植到 C(我写的也是有效的 C++)。 Godbolt compiler-explorer link。它们编译回 asm 几乎和我手写的 asm 一样好。 (并且我检查了编译器生成的 asm 中的向量常量是否与我的 db
指令匹配。在将 asm 转换为内在函数时绝对需要检查,特别是如果您使用 _mm_set_
而不是 setr
用于在最高优先顺序中看起来更“自然”的常量。setr
使用内存顺序,与 asm 相同。)
与我的 32 位 asm 不同,这些正在优化它们在寄存器中的输入数字,而不是假设它必须从内存中加载。 (所以我们不假设广播是免费的。)但是 TODO:探索使用 bswap
而不是 SIMD shuffle 将字节放入打印顺序。特别是对于 bswap 只有 1 uop 的 32 位整数(而 64 位寄存器的 Intel 为 2,与 AMD 不同)。
这些以 MSD 优先打印顺序打印整数。 为小端内存顺序输出调整多移位常量或随机控制,就像人们显然想要大散列的十六进制输出一样。或者对于 SSSE3 版本,只需删除 pshufb 字节反转。)
AVX2 / 512 还允许更宽的版本,一次处理 16 或 32 字节的输入,产生 32 或 64 字节的十六进制输出。可能通过改组在 128 位通道内以两倍宽度的向量重复每 64 位,例如vpermq
喜欢 _mm256_permutex_epi64(_mm256_castsi128_si256(v), _MM_SHUFFLE(?,?,?,?))
。
#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 位广播加载其堆栈 arg,即使是 u32 arg。但这只是为了我可以将负载折叠到 vpmultishiftqb
的内存源操作数中。没有办法告诉编译器它可以使用 64 位广播内存源操作数,其中高 32 位是“无关紧要的”,如果该值无论如何都来自内存(并且已知不在一个末尾)未映射页面之前的页面,例如 32 位模式堆栈 arg)。因此,在 C 中无法进行次要优化。通常在内联之后,您的变量将位于寄存器中,如果您有一个指针,您将不知道它是否位于页面的末尾。 uint64_t 版本确实需要广播,但由于内存中的对象是 uint64_t,编译器可以使用 {1to2}
广播内存源操作数。 (至少 clang 和 ICC 足够智能,可以使用 -m32 -march=icelake-client
,或者在 64 位模式下使用引用而不是值 arg。)
clang -O3 -m32
实际上与我手写的 asm 的编译完全相同,除了常量的 vmovdqa
加载,而不是 vmovq
,因为在这种情况下它实际上是全部需要的。当常量的前 8 个字节为 0 时,编译器不够聪明,无法仅使用 vmovq
加载并省略 .rodata 中的 0 个字节。另请注意,asm 输出中的 multishift 常量匹配,因此 {{1 }} 是正确的; .
这利用了输入为 32 位整数的优势;该策略不适用于 64 位(因为它需要两倍宽的位移位)。
_mm_set_epi8
以上是我认为更好的,尤其是在 Haswell 上,但在 Zen 上也是如此,其中可变移位 // 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)
}
具有较低的吞吐量和较高的延迟,即使它只是一个 uop。即使在 Skylake 上也能更好地解决后端端口瓶颈问题:3 条指令仅在端口 5 上运行,而以下版本的 4 条指令(包括 vpsrlvd
、vmovd xmm, reg
和 2x vpbroadcastd xmm,xmm
) , 但前端 uops 的数量相同(假设向量常量作为内存源操作数的微融合)。它还需要少 1 个向量常量,这总是很好,特别是如果它不在循环中。
AVX-512 可以使用合并屏蔽移位而不是可变计数移位,以需要设置屏蔽寄存器为代价节省了一个向量常量。这节省了 vpshufb
中的空间,但不会消除所有常量,因此缓存未命中仍会阻止此操作。并且 .rodata
/ mov r,imm
是 2 uops 而不是 1 在您使用它的任何循环之外。
还有 AVX2:itohex_AVX512F asm 版本的端口,带有我后来添加的 kmov k,r
想法。
vpsrlvd
与SSSE3版本相比,这通过使用// 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)
}
(或掩码移位)将vpunpcklbw
和vpsrlvd
的字节放入同一个XMM来节省num>>4
register 以设置 1 寄存器字节洗牌。 num
在 Skylake 及更高版本以及 Zen 1/Zen 2 上是单 uop。不过,在 Zen 上它的延迟更高,并且没有根据 https://uops.info/ 完全流水线化(2c 吞吐量而不是您的 1c d 期望它是一个端口的单个 uop。)但至少它不会与这些 CPU 上的 vpsrlvd
和 vpshufb
竞争相同的端口。 (在 Haswell 上,它有 2 个 uops,其中一个用于 p5,所以它确实竞争,这比 SSSE3 版本更糟糕,因为它需要一个额外的常量。)
Haswell 的一个不错的选择可能是 vpbroadcastd xmm,xmm
/ _mm_slli_epi64(v, 32-4)
- _mm_blend_epi32
在任何端口上运行,不需要 shuffle 端口。或者甚至在一般情况下,因为只需要一个 vpblendd
设置,而不是 vmovd
+ vmovd
该函数需要另外 2 个向量常量(十六进制 lut,以及组合的 AND 和 shuffle 掩码)。 GCC 和 clang 愚蠢地将一个掩码的 2 次使用“优化”为 2 个单独的掩码常量,这真的很愚蠢。(但在循环中,只需要设置开销和一个寄存器,不需要额外的每次转换成本。)无论如何,对于 vpbroadcastd
版本,您需要 2 个单独的 16 字节常量,但我的手写 asm 版本通过使用一个 16 字节常量的 2 个半部分很聪明。
MSVC 避免了这个问题:它更直接地编译内在函数并且不尝试优化它们(这通常是一件坏事,但在这里它避免了这个问题。)但是 MSVC 错过了使用 AVX-512 GP-register-source vpbroadcastd xmm0, esi
的 {{3}} {1}} 与 uint64_t
。使用 _mm_set1_epi32
(所以广播必须用 2 个单独的指令完成)它使用该向量常量作为内存源操作数两次(对于 -arch:AVX512
和 -arch:AVX2
)而不是加载到寄存器中,这很值得怀疑,但可能没问题,实际上可以节省前端 uops。 IDK 在提升负载的循环中会做什么更明显。
vpand
:vpshufb
使用 GCC 和 Clang 完全高效地编译(它们有效地优化了终止为 0 的字符串文字,并且只发出一个对齐的向量常量)。但不幸的是,MSVC 将实际字符串保存在 .rdata 中,而不对其进行对齐。所以我用了更长的,不太好读的,hex_lut
答案 2 :(得分:-1)
确实是
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