使用矢量指令进行复杂数据重组
Complex data reorganization with vector instructions
我需要按照以下模式将 12 个字节加载并重新排列为 16 个(或 24 个为 32 个):
ABC DEF GHI JKL
变成
ABBC DEEF GHHI JKKL
您能否建议使用 SSE(2) and/or AVX(2) 指令实现此目的的有效方法?
这需要重复执行,因此允许预存掩码或常量。
到目前为止,最好的选择是使用字节随机播放 (pshufb
)。在元素内移动本身是不够的,因为 JKL
必须比 DEF
向右移动得更远,等等。因此您需要多个指令来执行不同的移动并混合结果。
pshufb
(_mm_shuffle_epi8
) 需要 SSSE3,但可以在一条快速指令中完成 12B->16B 的工作。它使用向量作为随机播放控制掩码。这是第一个可变控制洗牌,也是第一个灵活的字节洗牌。 (SSE2 shuffle 全部使用 imm8 控制操作数,或者有固定的数据移动(例如 punpcklbw
)。
编写一个循环加载 16B,将第一个 12B 洗牌到 16B,然后存储应该很容易。使用未对齐的加载,并在必要时使用未对齐的存储。不是使用标量清理循环来处理最后几个字节,而是将输入的最后 16B 加载到向量中,然后打乱其中的 last 12B。如果数组不是 12B 的倍数,则存储将与循环中的最后一个存储重叠,但这没关系。
如果输入和输出在 L1 缓存中是热的,则 128b 循环应该能够维持每个输出时钟 16B。可能需要一些展开才能实现这一点,例如:
# shuffle mask in xmm5
# rsi=src, rdi=dst, rcx=src+size (pointer to last full vector)
.loop:
movdqu xmm0, [rsi]
pshufb xmm0, xmm5
movdqu [rdi], xmm0
movdqu xmm0, [rsi+12]
pshufb xmm0, xmm5
movdqu [rdi+16], xmm0
add rsi, 24
add rdi, 32
cmp rsi, rcx ;; still 9 fused-domain uops in the loop, so it bottlenecks on the frontend. Need more unroll :/
jb .loop
或者使用索引加载之类的技巧,索引计数为零。那将节省一个uop。 (add rsi, 24
/ jl .loop
)。 (如果你试图诱使编译器这样做,或者 实际上 手动编写 asm,请确保它是使用 2 寄存器寻址模式的负载,因为 would stop the stores from micro-fusing.)
AVX2
有四种处理通道交叉的选项(32B 负载将在低源通道中为高结果通道提供 4B 数据):
- 使用 16B load/pshufb/store,与不使用 AVX 相同。 3 微指令,因此需要循环展开以维持每个时钟 16B 的存储。
- double-shuffle:32B 加载/vpermd 在通道之间移动字节/32B vpshufb/32B 存储。应该在没有展开的情况下使洗牌端口饱和,每 2 个时钟维持一个 32B 存储。 (这有助于
vpermd
可以作为加载和洗牌工作,节省 uops。)
- inserti128:两个 16B 加载/32B vpshufb/32B 存储。可以走得更快,但需要大量展开(因此需要清理代码)。
使用对齐的负载,这样就不需要跨通道数据移动。 (需要缓冲区开头的特殊情况)。参见 BeeOnRope 的回答;这显然是最好的方法,只需要一个 vpshufb ymm
,因此它废弃了这个答案的大部分其余部分。无论如何我们都需要做未对齐的加载。
(AVX512):vpermb
是一个完整的跨通道字节洗牌,控制掩码中有 6 位索引(对于 512b版本)。要洗牌的字节可以是内存操作数,因此可以用作加载和洗牌。 (vpshufb
可以在内存中拥有其控制掩码,但不能作为加载和随机播放工作。大概是因为它是在 32 位仍然重要的情况下设计的,其中只有 8 个向量 reg 可用)。
SnB/IvB 可以进行 128b 洗牌,每 0.5c 吞吐量一次,但由于它们只有 16B 数据路径到 L1 缓存,所以您最好让它们(和 AMD Bulldozer 系列)最大限度地发挥它们的作用使用非 AVX 版本存储吞吐量。它们支持 AVX1 但不支持 AVX2。 不用费心制作 AVX1 版本; SSSE3 版本 没有任何好处,除了可能在某处避免 vzeroupper
。 (您可以在存储之前将两个 128b 随机播放结果与 vinsertf128
合并,这可能是一个微小的优势。)
Haswell/Skylake 核心只有一个洗牌端口,因此每 32B 结果需要两次洗牌的 double-shuffle 版本将成为瓶颈。但是,它所需的总融合域 uop 吞吐量远低于 16B 版本,因此您根本不需要展开以最大化吞吐量。尽管如此,如果你要制作一个展开的 SSSE3 版本,你最好使用它而不是用这种方式制作 AVX2 版本。如果您不打算使用非 AVX 版本,或者希望保持简单,此 应该使用最不复杂的源代码 提供良好的性能。特别是如果您的输出缓冲区(通常)是 32B 对齐的。
double-shuffle 对超线程也更友好,因为它 运行 的微指令更少。在这种情况下,它可能仍然受益于一个小的展开以减少循环开销,因此当它只获得一半的前端/发布周期时它仍然可以使洗牌端口饱和。它还增加了乱序 window:〜相同数量的飞行中加载和存储正在访问两倍的内存。这可能有助于减少缓存未命中造成的管道气泡,但对于像这样的顺序访问可能几乎没有影响。 Cache-line-crossing 32B loads/stores 可能比 16B 差。 (对齐输出缓冲区是一个非常好的主意,并确保输入缓冲区至少 4B 对齐。)
vinserti128
版本:
诀窍是 vinserti128
具有内存源不需要随机端口:任何 ALU 端口都可以。所以理论上我们每个周期可以做两个重叠的 16B 加载和一个 32B 存储。 Haswell/Skylake 在实践中无法维持这一点,因为一些商店将 运行 他们的 AGU uop 在端口 2 或 3 上,而不是端口 7 专用商店 AGU。英特尔的优化手册(在第 2.1.3 节中,请参阅 x86 标记 wiki 以获取链接)给出了 table Skylake 上 L1、L2 等的峰值吞吐量与持续吞吐量的对比。 Skylake 只能维持约 81B/周期总 to/from L1D 缓存,而峰值为每个时钟 96B(2 次加载和一次存储)。我认为有些商店从负载中窃取执行端口的原因,即使我们的负载只有 16B,这也会影响我们。
另一个主要问题:每个时钟流水线宽度的 4 个融合域微指令:vinserti128
是 2 个融合域微指令,所以 vmovdqu
(16B 负载) / vinserti128 y,y,m,i
/ vpshufb
/ vmovdqu
(32B store) 在不考虑循环开销的情况下已经是 5 uops。因此,即使有大量展开,我们能做的最好的事情就是保持随机播放和 load/store 端口的 4/5 被占用。这略低于每个时钟 81B 的瓶颈,所以这可能不会发挥作用。尽管如此,将近 32B * 4 / 5c 还是对 16B / c 的稳固胜利。
不要展开太多,因为我们需要前端提供每个时钟 4 微指令。如果循环低于 28 微指令左右,循环缓冲区将有助于避免出现瓶颈。 (或禁用超线程后更大,Skylake 可能增加了它。)
gcc 和 clang 即使使用 -funroll-loops
也无法展开循环,大概是因为迭代次数在编译时未知。 -funroll-all-loops
几乎没有减少开销,只是在循环体中放置了多个增量和循环退出分支。因此,您需要手动展开循环以使 vinserti128
版本具有任何优势。
代码:
插入和双随机版本,没有展开。既未测试也未调试,但 asm 看起来不错。
您需要整理这些并完善清理代码以满足您的要求。可能还会对这两个版本进行基准测试(如果您编写的是非 AVX 版本,则可能是三个版本)。
查看 godbolt compiler explorer 上的代码和 asm:
#include <immintrin.h>
#include <assert.h>
// This version won't have much advantage over a 16B loop,
// without significant loop unrolling in the source and expanding the cleanup code to match
void expand12to16_insert128(char *restrict dst, const char *restrict src, size_t src_bytes) {
// setr: args in little-endian order
const __m256i byteshuf = _mm256_setr_epi8(0,1,1,2, 3,4,4,5, 6,7,7,8, 9,10,10,11,
0,1,1,2, 3,4,4,5, 6,7,7,8, 9,10,10,11);
//const __m256i byteshuf = _mm256_broadcastsi128_si256(byteshuf128); // gcc is dumb and makes bad code for this, but it does save space
assert(src_bytes >= 28); // 28 because the cleanup code reads 4B before the last 24B and then shifts. That can potentially segfault
// FIXME: handle this case if needed.
// maybe with a load that avoids crossing any cache-line boundaries not crossed by the input,
// and then a VPMASKMOVD conditional store
const char *lastsrcvec = src + src_bytes - 24;
for ( ; src < lastsrcvec ; dst += 32, src += 24 ){
#if 1
__m256i in = _mm256_castsi128_si256( _mm_loadu_si128((__m128i*)src) );
__m128i in_hi = _mm_loadu_si128((__m128i*)(src+12) );
in = _mm256_inserti128_si256(in, in_hi, 1);
#else
__m128i in_lo = _mm_loadu_si128((__m128i*)(src+0));
__m128i in_hi = _mm_loadu_si128((__m128i*)(src+12) );
__m256i in = _mm256_set_m128i(in_hi, in_lo); // clang supports this, but gcc doesn't. Same asm, nicer C syntax
#endif
__m256i out = _mm256_shuffle_epi8(in, byteshuf);
_mm256_storeu_si256((__m256i*)dst, out);
}
// grab the last 24B with loads that don't go past the end of the array (i.e. offset by -4)
// Instead of using a 2nd shuffle mask to shuffle from these offset positions,
// byte-shift each lane back down to the bottom of the 16B
// Note that the shift count is a compile time constant: it's the amount of overlap that can vary
// movq / pinsrd could be useful as a 12B load
__m256i in = _mm256_castsi128_si256( _mm_loadu_si128((__m128i*)(lastsrcvec-4)) );
__m128i in_hi = _mm_loadu_si128((__m128i*)(lastsrcvec-4 + 12) );
// byte shifting just the hi lane would mean the low lane wouldn't have to be offset
// but then there'd have to be a load separate from the inserti128
in = _mm256_inserti128_si256(in, in_hi, 1); // [ ABC DEF GHI JKL XXXX | same ]
in = _mm256_bsrli_epi128(in, 4); // [ 0000 ABC DEF GHI JKL | same ]
__m256i out = _mm256_shuffle_epi8(in, byteshuf);
dst -= (src - lastsrcvec) * 16 / 12; // calculate the overlap
// If the caller already needs to calculate dst_bytes, pass that instead of src_bytes
// Because *3/4 is cheaper than *4/3
_mm256_storeu_si256((__m256i*)dst, out);
//return byteshuf;
}
// clang-3.8 miscompiles this to shuffle one shuffle mask with the other, and copy that constant to the whole dst
void expand12to16_doubleshuffle(char *restrict dst, const char *restrict src, size_t src_bytes) {
assert(src_bytes >= 24);
// setr: args in little-endian order
const __m128i byteshuf128 = _mm_setr_epi8(0,1,1,2, 3,4,4,5, 6,7,7,8, 9,10,10,11);
//0,1,1,2, 3,4,4,5, 6,7,7,8, 9,10,10,11);
const __m256i byteshuf = _mm256_broadcastsi128_si256(byteshuf128); // gcc is dumb and use a 128b load then vinserti128, instead of a vpbroadcast128i load :/
// const __m256i lane_adjust_shuf = _mm256_setr_epi32(0,1,2,2, 3,4,5,5);
// save some space by using a 8->32 pmovzx load.
const __m256i lane_adjust_shuf = _mm256_cvtepu8_epi32(_mm_setr_epi8(0,1,2,2, 3,4,5,5,
/* unused padding that isn't optimized away :( */ 0,0,0,0, 0,0,0,0));
const char *lastsrcvec = src + src_bytes - 24;
for ( ; src < lastsrcvec ; dst += 32, src += 24 ){
__m256i in = _mm256_loadu_si256((__m256i*)(src+0));
in = _mm256_permutevar8x32_epi32(in, lane_adjust_shuf);
__m256i out = _mm256_shuffle_epi8(in, byteshuf);
_mm256_storeu_si256((__m256i*)dst, out);
}
// Use the insert cleanup code because it's easier to load just the last 24B we want
// slightly modified from the insert128 version to only load the last 24, not 28B
__m256i in = _mm256_castsi128_si256( _mm_loadu_si128((__m128i*)(lastsrcvec)) );
__m128i in_hi = _mm_loadu_si128((__m128i*)(lastsrcvec-4 + 12) );
// byte shift pshufd instead of bsrli, so the load can fold into it
// before: [ LKJ IHG FED CBA XXXX ]
in_hi = _mm_shuffle_epi32(in_hi, _MM_SHUFFLE(3,3,2,1)); // [ LKJI LKJ IHG FED CBA ]
in = _mm256_inserti128_si256(in, in_hi, 1);
__m256i out = _mm256_shuffle_epi8(in, byteshuf);
// see the full comments in the other version
dst -= (src - lastsrcvec) * 16 / 12; // calculate the overlap
_mm256_storeu_si256((__m256i*)dst, out);
//return byteshuf;
}
(clang bug report filed for the mis-compiled shuffles)
内部循环,from gcc 5.3 -O3 -march=haswell -masm=intel:
#insert version
.L4:
vmovdqu xmm0, XMMWORD PTR [rsi] #,* src
add rsi, 24 # src,
vinserti128 ymm0, ymm0, XMMWORD PTR [rsi-12], 0x1 # tmp128, tmp124,
add rdi, 32 # dst,
vpshufb ymm0, ymm0, ymm1 # tmp131, tmp128, tmp157
vmovdqu YMMWORD PTR [rdi-32], ymm0 #, tmp131
cmp rax, rsi # lastsrcvec, src
ja .L4 #,
7 个融合域 uops,应该 运行 每 2 个时钟迭代一次。 (即每个周期存储 16B)。展开可以更快。
#double-shuffle version
.L16:
vpermd ymm0, ymm2, YMMWORD PTR [rsi] # tmp126, D.27207,* src
add rsi, 24 # src,
vpshufb ymm0, ymm0, ymm1 # tmp127, tmp126, D.27202
add rdi, 32 # dst,
vmovdqu YMMWORD PTR [rdi-32], ymm0 #, tmp127
cmp rax, rsi # lastsrcvec, src
ja .L16 #,
6 个融合域 uops,还应该 运行 每 2 个时钟迭代一次。但是,由于洗牌端口瓶颈,这是它所能达到的最快速度。如果您不 打算展开,我会测试两者,但我怀疑这个会很好。
继 Peter 的解决方案之后,对于 AVX2,您似乎可以通过抵消 32B 负载达到 32B/周期(输出字节),因此 16B 边界落在正确的位置,在两组 12 字节之间:
例如:
byte: 0123456789012345|0123456789012345
load: xxxxAAABBBCCCDDD|EEEFFFGGGHHHxxxx
pshuf AAAABBBBCCCCDDDD|EEEEFFFFGGGGHHHH
现在不需要跨车道移动,所以通过相同的展开原始 SSE3 解决方案,我认为您很容易达到 32 字节 - 除非缓存行交叉未对齐的访问对您造成太大伤害。
我需要按照以下模式将 12 个字节加载并重新排列为 16 个(或 24 个为 32 个):
ABC DEF GHI JKL
变成
ABBC DEEF GHHI JKKL
您能否建议使用 SSE(2) and/or AVX(2) 指令实现此目的的有效方法?
这需要重复执行,因此允许预存掩码或常量。
到目前为止,最好的选择是使用字节随机播放 (pshufb
)。在元素内移动本身是不够的,因为 JKL
必须比 DEF
向右移动得更远,等等。因此您需要多个指令来执行不同的移动并混合结果。
pshufb
(_mm_shuffle_epi8
) 需要 SSSE3,但可以在一条快速指令中完成 12B->16B 的工作。它使用向量作为随机播放控制掩码。这是第一个可变控制洗牌,也是第一个灵活的字节洗牌。 (SSE2 shuffle 全部使用 imm8 控制操作数,或者有固定的数据移动(例如 punpcklbw
)。
编写一个循环加载 16B,将第一个 12B 洗牌到 16B,然后存储应该很容易。使用未对齐的加载,并在必要时使用未对齐的存储。不是使用标量清理循环来处理最后几个字节,而是将输入的最后 16B 加载到向量中,然后打乱其中的 last 12B。如果数组不是 12B 的倍数,则存储将与循环中的最后一个存储重叠,但这没关系。
如果输入和输出在 L1 缓存中是热的,则 128b 循环应该能够维持每个输出时钟 16B。可能需要一些展开才能实现这一点,例如:
# shuffle mask in xmm5
# rsi=src, rdi=dst, rcx=src+size (pointer to last full vector)
.loop:
movdqu xmm0, [rsi]
pshufb xmm0, xmm5
movdqu [rdi], xmm0
movdqu xmm0, [rsi+12]
pshufb xmm0, xmm5
movdqu [rdi+16], xmm0
add rsi, 24
add rdi, 32
cmp rsi, rcx ;; still 9 fused-domain uops in the loop, so it bottlenecks on the frontend. Need more unroll :/
jb .loop
或者使用索引加载之类的技巧,索引计数为零。那将节省一个uop。 (add rsi, 24
/ jl .loop
)。 (如果你试图诱使编译器这样做,或者 实际上 手动编写 asm,请确保它是使用 2 寄存器寻址模式的负载,因为 would stop the stores from micro-fusing.)
AVX2
有四种处理通道交叉的选项(32B 负载将在低源通道中为高结果通道提供 4B 数据):
- 使用 16B load/pshufb/store,与不使用 AVX 相同。 3 微指令,因此需要循环展开以维持每个时钟 16B 的存储。
- double-shuffle:32B 加载/vpermd 在通道之间移动字节/32B vpshufb/32B 存储。应该在没有展开的情况下使洗牌端口饱和,每 2 个时钟维持一个 32B 存储。 (这有助于
vpermd
可以作为加载和洗牌工作,节省 uops。) - inserti128:两个 16B 加载/32B vpshufb/32B 存储。可以走得更快,但需要大量展开(因此需要清理代码)。
使用对齐的负载,这样就不需要跨通道数据移动。 (需要缓冲区开头的特殊情况)。参见 BeeOnRope 的回答;这显然是最好的方法,只需要一个
vpshufb ymm
,因此它废弃了这个答案的大部分其余部分。无论如何我们都需要做未对齐的加载。(AVX512):
vpermb
是一个完整的跨通道字节洗牌,控制掩码中有 6 位索引(对于 512b版本)。要洗牌的字节可以是内存操作数,因此可以用作加载和洗牌。 (vpshufb
可以在内存中拥有其控制掩码,但不能作为加载和随机播放工作。大概是因为它是在 32 位仍然重要的情况下设计的,其中只有 8 个向量 reg 可用)。
SnB/IvB 可以进行 128b 洗牌,每 0.5c 吞吐量一次,但由于它们只有 16B 数据路径到 L1 缓存,所以您最好让它们(和 AMD Bulldozer 系列)最大限度地发挥它们的作用使用非 AVX 版本存储吞吐量。它们支持 AVX1 但不支持 AVX2。 不用费心制作 AVX1 版本; SSSE3 版本 没有任何好处,除了可能在某处避免 vzeroupper
。 (您可以在存储之前将两个 128b 随机播放结果与 vinsertf128
合并,这可能是一个微小的优势。)
Haswell/Skylake 核心只有一个洗牌端口,因此每 32B 结果需要两次洗牌的 double-shuffle 版本将成为瓶颈。但是,它所需的总融合域 uop 吞吐量远低于 16B 版本,因此您根本不需要展开以最大化吞吐量。尽管如此,如果你要制作一个展开的 SSSE3 版本,你最好使用它而不是用这种方式制作 AVX2 版本。如果您不打算使用非 AVX 版本,或者希望保持简单,此 应该使用最不复杂的源代码 提供良好的性能。特别是如果您的输出缓冲区(通常)是 32B 对齐的。
double-shuffle 对超线程也更友好,因为它 运行 的微指令更少。在这种情况下,它可能仍然受益于一个小的展开以减少循环开销,因此当它只获得一半的前端/发布周期时它仍然可以使洗牌端口饱和。它还增加了乱序 window:〜相同数量的飞行中加载和存储正在访问两倍的内存。这可能有助于减少缓存未命中造成的管道气泡,但对于像这样的顺序访问可能几乎没有影响。 Cache-line-crossing 32B loads/stores 可能比 16B 差。 (对齐输出缓冲区是一个非常好的主意,并确保输入缓冲区至少 4B 对齐。)
vinserti128
版本:
诀窍是 vinserti128
具有内存源不需要随机端口:任何 ALU 端口都可以。所以理论上我们每个周期可以做两个重叠的 16B 加载和一个 32B 存储。 Haswell/Skylake 在实践中无法维持这一点,因为一些商店将 运行 他们的 AGU uop 在端口 2 或 3 上,而不是端口 7 专用商店 AGU。英特尔的优化手册(在第 2.1.3 节中,请参阅 x86 标记 wiki 以获取链接)给出了 table Skylake 上 L1、L2 等的峰值吞吐量与持续吞吐量的对比。 Skylake 只能维持约 81B/周期总 to/from L1D 缓存,而峰值为每个时钟 96B(2 次加载和一次存储)。我认为有些商店从负载中窃取执行端口的原因,即使我们的负载只有 16B,这也会影响我们。
另一个主要问题:每个时钟流水线宽度的 4 个融合域微指令:vinserti128
是 2 个融合域微指令,所以 vmovdqu
(16B 负载) / vinserti128 y,y,m,i
/ vpshufb
/ vmovdqu
(32B store) 在不考虑循环开销的情况下已经是 5 uops。因此,即使有大量展开,我们能做的最好的事情就是保持随机播放和 load/store 端口的 4/5 被占用。这略低于每个时钟 81B 的瓶颈,所以这可能不会发挥作用。尽管如此,将近 32B * 4 / 5c 还是对 16B / c 的稳固胜利。
不要展开太多,因为我们需要前端提供每个时钟 4 微指令。如果循环低于 28 微指令左右,循环缓冲区将有助于避免出现瓶颈。 (或禁用超线程后更大,Skylake 可能增加了它。)
gcc 和 clang 即使使用 -funroll-loops
也无法展开循环,大概是因为迭代次数在编译时未知。 -funroll-all-loops
几乎没有减少开销,只是在循环体中放置了多个增量和循环退出分支。因此,您需要手动展开循环以使 vinserti128
版本具有任何优势。
代码:
插入和双随机版本,没有展开。既未测试也未调试,但 asm 看起来不错。
您需要整理这些并完善清理代码以满足您的要求。可能还会对这两个版本进行基准测试(如果您编写的是非 AVX 版本,则可能是三个版本)。
查看 godbolt compiler explorer 上的代码和 asm:
#include <immintrin.h>
#include <assert.h>
// This version won't have much advantage over a 16B loop,
// without significant loop unrolling in the source and expanding the cleanup code to match
void expand12to16_insert128(char *restrict dst, const char *restrict src, size_t src_bytes) {
// setr: args in little-endian order
const __m256i byteshuf = _mm256_setr_epi8(0,1,1,2, 3,4,4,5, 6,7,7,8, 9,10,10,11,
0,1,1,2, 3,4,4,5, 6,7,7,8, 9,10,10,11);
//const __m256i byteshuf = _mm256_broadcastsi128_si256(byteshuf128); // gcc is dumb and makes bad code for this, but it does save space
assert(src_bytes >= 28); // 28 because the cleanup code reads 4B before the last 24B and then shifts. That can potentially segfault
// FIXME: handle this case if needed.
// maybe with a load that avoids crossing any cache-line boundaries not crossed by the input,
// and then a VPMASKMOVD conditional store
const char *lastsrcvec = src + src_bytes - 24;
for ( ; src < lastsrcvec ; dst += 32, src += 24 ){
#if 1
__m256i in = _mm256_castsi128_si256( _mm_loadu_si128((__m128i*)src) );
__m128i in_hi = _mm_loadu_si128((__m128i*)(src+12) );
in = _mm256_inserti128_si256(in, in_hi, 1);
#else
__m128i in_lo = _mm_loadu_si128((__m128i*)(src+0));
__m128i in_hi = _mm_loadu_si128((__m128i*)(src+12) );
__m256i in = _mm256_set_m128i(in_hi, in_lo); // clang supports this, but gcc doesn't. Same asm, nicer C syntax
#endif
__m256i out = _mm256_shuffle_epi8(in, byteshuf);
_mm256_storeu_si256((__m256i*)dst, out);
}
// grab the last 24B with loads that don't go past the end of the array (i.e. offset by -4)
// Instead of using a 2nd shuffle mask to shuffle from these offset positions,
// byte-shift each lane back down to the bottom of the 16B
// Note that the shift count is a compile time constant: it's the amount of overlap that can vary
// movq / pinsrd could be useful as a 12B load
__m256i in = _mm256_castsi128_si256( _mm_loadu_si128((__m128i*)(lastsrcvec-4)) );
__m128i in_hi = _mm_loadu_si128((__m128i*)(lastsrcvec-4 + 12) );
// byte shifting just the hi lane would mean the low lane wouldn't have to be offset
// but then there'd have to be a load separate from the inserti128
in = _mm256_inserti128_si256(in, in_hi, 1); // [ ABC DEF GHI JKL XXXX | same ]
in = _mm256_bsrli_epi128(in, 4); // [ 0000 ABC DEF GHI JKL | same ]
__m256i out = _mm256_shuffle_epi8(in, byteshuf);
dst -= (src - lastsrcvec) * 16 / 12; // calculate the overlap
// If the caller already needs to calculate dst_bytes, pass that instead of src_bytes
// Because *3/4 is cheaper than *4/3
_mm256_storeu_si256((__m256i*)dst, out);
//return byteshuf;
}
// clang-3.8 miscompiles this to shuffle one shuffle mask with the other, and copy that constant to the whole dst
void expand12to16_doubleshuffle(char *restrict dst, const char *restrict src, size_t src_bytes) {
assert(src_bytes >= 24);
// setr: args in little-endian order
const __m128i byteshuf128 = _mm_setr_epi8(0,1,1,2, 3,4,4,5, 6,7,7,8, 9,10,10,11);
//0,1,1,2, 3,4,4,5, 6,7,7,8, 9,10,10,11);
const __m256i byteshuf = _mm256_broadcastsi128_si256(byteshuf128); // gcc is dumb and use a 128b load then vinserti128, instead of a vpbroadcast128i load :/
// const __m256i lane_adjust_shuf = _mm256_setr_epi32(0,1,2,2, 3,4,5,5);
// save some space by using a 8->32 pmovzx load.
const __m256i lane_adjust_shuf = _mm256_cvtepu8_epi32(_mm_setr_epi8(0,1,2,2, 3,4,5,5,
/* unused padding that isn't optimized away :( */ 0,0,0,0, 0,0,0,0));
const char *lastsrcvec = src + src_bytes - 24;
for ( ; src < lastsrcvec ; dst += 32, src += 24 ){
__m256i in = _mm256_loadu_si256((__m256i*)(src+0));
in = _mm256_permutevar8x32_epi32(in, lane_adjust_shuf);
__m256i out = _mm256_shuffle_epi8(in, byteshuf);
_mm256_storeu_si256((__m256i*)dst, out);
}
// Use the insert cleanup code because it's easier to load just the last 24B we want
// slightly modified from the insert128 version to only load the last 24, not 28B
__m256i in = _mm256_castsi128_si256( _mm_loadu_si128((__m128i*)(lastsrcvec)) );
__m128i in_hi = _mm_loadu_si128((__m128i*)(lastsrcvec-4 + 12) );
// byte shift pshufd instead of bsrli, so the load can fold into it
// before: [ LKJ IHG FED CBA XXXX ]
in_hi = _mm_shuffle_epi32(in_hi, _MM_SHUFFLE(3,3,2,1)); // [ LKJI LKJ IHG FED CBA ]
in = _mm256_inserti128_si256(in, in_hi, 1);
__m256i out = _mm256_shuffle_epi8(in, byteshuf);
// see the full comments in the other version
dst -= (src - lastsrcvec) * 16 / 12; // calculate the overlap
_mm256_storeu_si256((__m256i*)dst, out);
//return byteshuf;
}
(clang bug report filed for the mis-compiled shuffles)
内部循环,from gcc 5.3 -O3 -march=haswell -masm=intel:
#insert version
.L4:
vmovdqu xmm0, XMMWORD PTR [rsi] #,* src
add rsi, 24 # src,
vinserti128 ymm0, ymm0, XMMWORD PTR [rsi-12], 0x1 # tmp128, tmp124,
add rdi, 32 # dst,
vpshufb ymm0, ymm0, ymm1 # tmp131, tmp128, tmp157
vmovdqu YMMWORD PTR [rdi-32], ymm0 #, tmp131
cmp rax, rsi # lastsrcvec, src
ja .L4 #,
7 个融合域 uops,应该 运行 每 2 个时钟迭代一次。 (即每个周期存储 16B)。展开可以更快。
#double-shuffle version
.L16:
vpermd ymm0, ymm2, YMMWORD PTR [rsi] # tmp126, D.27207,* src
add rsi, 24 # src,
vpshufb ymm0, ymm0, ymm1 # tmp127, tmp126, D.27202
add rdi, 32 # dst,
vmovdqu YMMWORD PTR [rdi-32], ymm0 #, tmp127
cmp rax, rsi # lastsrcvec, src
ja .L16 #,
6 个融合域 uops,还应该 运行 每 2 个时钟迭代一次。但是,由于洗牌端口瓶颈,这是它所能达到的最快速度。如果您不 打算展开,我会测试两者,但我怀疑这个会很好。
继 Peter 的解决方案之后,对于 AVX2,您似乎可以通过抵消 32B 负载达到 32B/周期(输出字节),因此 16B 边界落在正确的位置,在两组 12 字节之间:
例如:
byte: 0123456789012345|0123456789012345
load: xxxxAAABBBCCCDDD|EEEFFFGGGHHHxxxx
pshuf AAAABBBBCCCCDDDD|EEEEFFFFGGGGHHHH
现在不需要跨车道移动,所以通过相同的展开原始 SSE3 解决方案,我认为您很容易达到 32 字节 - 除非缓存行交叉未对齐的访问对您造成太大伤害。