- Published on
SIMD 加速:AVX2 指令集实现大小端转换
- Authors
- Name
- Yusheng Zheng (云微)
- @yunwei37
在应用 thrift 进行 RPC 通信的时候,由于 Thrift 采用了大端序,而 x86_64 等常见的处理器架构均为小端序,因此对于 list 这一类的数据类型需要一个循环来实现小端到大端的转换。而这个过程如果能够利用 SIMD 指令的话,可以极大的提高性能。这篇文章是在探索实现 Thrift 编译后端 Auto-vectorization Pass 的时候进行的一个尝试和学习,使用 avx2 指令集实现了一个简单的大小端转换的功能,并且对比了在不同条件下的加速性能。
原理
大小端转换
计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式)。
大端模式
,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。小端模式
,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。
例如,对于内存中存放的数0x12345678来说
- 如果是采用大端模式存放的,则其真实的数是:0x12345678
- 如果是采用小端模式存放的,则其真实的数是:0x78563412
可以使用如下 API 进行转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
也可以直接使用移位进行实现
inline uint32_t Swap32(uint32_t x)
{
return (
((x & 0x000000FF) << 24) |
((x & 0x0000FF00) << 8) |
((x & 0x00FF0000) >> 8) |
((x & 0xFF000000) >> 24));
}
bswap
大部分编译器同时提供了 bswap
指令,来帮助实现这一转换过程,例如在 gcc 中,我们可以使用 __builtin_bswap{16,32,64}
:
inline uint32_t Swap32(uint32_t x)
{
return __builtin_bswap32(x);
}
这是一个编译器的内置函数。在 x86_64 机器上,它会被编译为这样的指令序列:
Swap32(unsigned int):
mov eax, edi
bswap eax
ret
在 arm 机器上,它会被编译为这样的指令序列:
Swap32(unsigned int):
rev w0, w0
ret
通常来说,我们自己使用的移位函数实现的大小端转换,在编译器优化 O2 时也会被自动识别替换为 bswap 指令。
avx2 指令集
使用 SIMD 对于这样可以高度并行化的计算应该是一个更快的选择。bswap指令可以反转 2, 4, 或 8 字节的字节顺序,但 x86 中的 SIMD 扩展允许仅使用一条指令对多条数据通道进行并行操作。就像原子地反转寄存器中的所有四个字节一样,它提供了一个完整的算术指令集,允许使用一条指令同时并行处理多个数据实例。这些被操作的数据块往往被称为 vectors。一般来说可用的有如下几种 SIMD 指令集:
- MMX (1996)
- SSE (1999)
- SSE2 (2001)
- SSE3 (2004)
- SSSE3 (2006)
- SSE4 a/1/2 (2006)
- AVX (2008)
- AVX2 (2013)
- AVX512 (2015)
目前较为常用的是 avx/avx2 指令集,早先的某些指令集主要是为了兼容性而保留的。具体的指令信息,可以参考 Intel 指令集查询。
我们这里主要使用的是 _mm256_shuffle_epi8
指令,在 C 中它被定义在了 #include <immintrin.h>
头文件中。它实现了一个 vector 中字节的重排序,例如将一个 128 位的字节序列完全反转:
const __m256i ShuffleRev = _mm256_set_epi8(
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15, // first 128-bit lane
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15 // second 128-bit lane
);
// Load 32 elements at once into one 32-byte register
__m256i x = _mm256_loadu_si256(
reinterpret_cast<__m256i*>(&Array8[i])
);
// Reverse each the bytes in each 128-bit lane
x = _mm256_shuffle_epi8(x,ShuffleRev);
它接受一个ShuffleRev,定义具体每个字节应该被放到哪个位置。注意每128位为一个通道,重排范围只能在128位内,不能将前128位的内容重排至后128位。可以参照下图,可能会比较直观:
来源:https://www.officedaytime.com/simd512e/simdimg/si.php?f=pshufb
在 gcc -O3 中,Auto-vectorization Pass 可以帮助我们自动识别可以被向量化的循环,并且使用 avx 指令集进行并行化优化。
avx2 code
这是一个对于 64 位整数的大小端转换 load-swap-store 循环,使用 avx2 指令集进行加速的简单示例:
void reverse64_avx2(void *Array, size_t Count)
{
uint64_t *Array64 = (uint64_t *)(Array);
size_t i = 0;
const __m256i ShuffleRev = _mm256_set_epi8(
8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7);
for (; i < (Count & ~0b11); i += 4)
{
// Load 4 elements
__m256i bytes = _mm256_loadu_si256(
(__m256i *)(&Array64[i]));
// Reverse the byte order of our 64-byte vectors
bytes = _mm256_shuffle_epi8(bytes, ShuffleRev);
// Place them back into the array
_mm256_storeu_si256(
(__m256i *)(&Array64[i]),
bytes);
}
// Naive swaps for leftover elements
for (; i < Count; ++i)
{
Array64[i] = Swap64(Array64[i]);
}
}
avx2 指令集的向量是 256 位长度,相当于 4 个 64bit 的整数。由于输入的数组并不一定被 4 整除,因此结尾的部分使用一般转换法逐个进行大小端转换。
benchmark
测试环境:
- Linux ubuntu 5.13.0-51-generic #58~20.04.1-Ubuntu SMP Tue Jun 14 11:29:12 UTC 2022 x86_64
- Intel Core i7-10750H
编译指令:
gcc main.c -mavx2 -fno-tree-vectorize -O3 -o avx
basic 对照函数(这里 Swap64 会被 gcc 自动编译为 bswap 指令):
void reverse64_basic(void *Array, size_t Count)
{
uint64_t *Array64 = (uint64_t *)(Array);
size_t i = 0;
// Naive swaps
for (; i < Count; ++i)
{
Array64[i] = Swap64(Array64[i]);
}
}
我们分别对 64/32/16 位的整数进行大小端转换,并测试 bswap 和 avx2 的加速比:
array size | avx2 64bit | basic 64bit | avx2 32bit | basic 32bit | avx2 16bit | basic 16bit |
---|---|---|---|---|---|---|
4 | 2ns | 3ns | 3ns | 3ns | 4ns | 2ns |
8 | 3ns | 4ns | 2ns | 4ns | 15ns | 9ns |
16 | 5ns | 9ns | 3ns | 9ns | 3ns | 10ns |
32 | 9ns | 37ns | 5ns | 18ns | 4ns | 19ns |
64 | 19ns | 34ns | 9ns | 34ns | 6ns | 59ns |
128 | 35ns | 181ns | 15ns | 76ns | 9ns | 82ns |
256 | 52ns | 120ns | 26ns | 477ns | 11ns | 712ns |
512 | 86ns | 248ns | 44ns | 192ns | 29ns | 254ns |
1024 | 179ns | 510ns | 96ns | 422ns | 47ns | 486ns |
2048 | 383ns | 996ns | 179ns | 812ns | 96ns | 981ns |
4096 | 726ns | 2190ns | 457ns | 2675ns | 384ns | 1878ns |
8192 | 1544ns | 4170ns | 748ns | 3434ns | 401ns | 4511ns |
16384 | 3570ns | 8977ns | 1704ns | 6771ns | 793ns | 7941ns |
可以注意到,对于宽度更小的整数的数组,并行度更高,avx2 加速比更加明显。在 64 位时,加速比约为 2.5,在 16 位时,加速比已经达到了 10 倍。
生成的汇编
objdump -d ./avx > dump.s
我们可以再检查一下生成的汇编指令:
使用 bswap 的大小端转换
0000000000001c30 <reverse64_basic>: 1c30: f3 0f 1e fa endbr64 1c34: 48 85 f6 test %rsi,%rsi 1c37: 74 1a je 1c53 <reverse64_basic+0x23> 1c39: 48 8d 14 f7 lea (%rdi,%rsi,8),%rdx 1c3d: 0f 1f 00 nopl (%rax) 1c40: 48 8b 07 mov (%rdi),%rax 1c43: 48 83 c7 08 add $0x8,%rdi 1c47: 48 0f c8 bswap %rax 1c4a: 48 89 47 f8 mov %rax,-0x8(%rdi) 1c4e: 48 39 d7 cmp %rdx,%rdi 1c51: 75 ed jne 1c40 <reverse64_basic+0x10> 1c53: c3 retq 1c54: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)
avx2:vpshufb
0000000000001bb0 <reverse64_avx2>: 1bb0: f3 0f 1e fa endbr64 1bb4: 48 89 f2 mov %rsi,%rdx 1bb7: 48 83 e2 fc and $0xfffffffffffffffc,%rdx 1bbb: 74 46 je 1c03 <reverse64_avx2+0x53> 1bbd: c5 fd 6f 0d fb 14 00 vmovdqa 0x14fb(%rip),%ymm1 # 30c0 <_IO_stdin_used+0xc0> 1bc4: 00 1bc5: 48 8d 4a ff lea -0x1(%rdx),%rcx 1bc9: 48 89 f8 mov %rdi,%rax 1bcc: 48 8d 14 d7 lea (%rdi,%rdx,8),%rdx 1bd0: c5 fa 6f 10 vmovdqu (%rax),%xmm2 1bd4: c4 e3 6d 38 40 10 01 vinserti128 $0x1,0x10(%rax),%ymm2,%ymm0 1bdb: 48 83 c0 20 add $0x20,%rax 1bdf: c4 e2 7d 00 c1 vpshufb %ymm1,%ymm0,%ymm0 1be4: c5 fa 7f 40 e0 vmovdqu %xmm0,-0x20(%rax) 1be9: c4 e3 7d 39 40 f0 01 vextracti128 $0x1,%ymm0,-0x10(%rax) 1bf0: 48 39 d0 cmp %rdx,%rax 1bf3: 75 db jne 1bd0 <reverse64_avx2+0x20> 1bf5: 48 83 e1 fc and $0xfffffffffffffffc,%rcx 1bf9: 48 89 ca mov %rcx,%rdx 1bfc: 48 83 c2 04 add $0x4,%rdx 1c00: c5 f8 77 vzeroupper 1c03: 48 39 d6 cmp %rdx,%rsi 1c06: 76 1b jbe 1c23 <reverse64_avx2+0x73> 1c08: 48 8d 04 d7 lea (%rdi,%rdx,8),%rax 1c0c: 48 8d 0c f7 lea (%rdi,%rsi,8),%rcx 1c10: 48 8b 10 mov (%rax),%rdx 1c13: 48 83 c0 08 add $0x8,%rax 1c17: 48 0f ca bswap %rdx 1c1a: 48 89 50 f8 mov %rdx,-0x8(%rax) 1c1e: 48 39 c1 cmp %rax,%rcx 1c21: 75 ed jne 1c10 <reverse64_avx2+0x60> 1c23: c3 retq 1c24: 66 66 2e 0f 1f 84 00 data16 nopw %cs:0x0(%rax,%rax,1)