问题描述
针对 AVX2,转置包含 64 位整数(或双精度)的 8x8 矩阵的最快方法是什么?
我搜索了这个网站,发现了几种进行 8x8 转置的方法,但主要是针对 32 位浮点数。所以我主要是问,因为我不确定使这些算法快速转换的原理是否可以轻松转换为 64 位和秒,显然 AVX2 只有 16 个寄存器,所以只加载所有值就会占用所有寄存器。>
一种方法是调用 2x2 _MM_TRANSPOSE4_PD,但我想知道这是否是最佳的:
#define _MM_TRANSPOSE4_PD(row0,row1,row2,row3) \
{ \
__m256d tmp3,tmp2,tmp1,tmp0; \
\
tmp0 = _mm256_shuffle_pd((row0),(row1),0x0); \
tmp2 = _mm256_shuffle_pd((row0),0xF); \
tmp1 = _mm256_shuffle_pd((row2),(row3),0x0); \
tmp3 = _mm256_shuffle_pd((row2),0xF); \
\
(row0) = _mm256_permute2f128_pd(tmp0,0x20); \
(row1) = _mm256_permute2f128_pd(tmp2,tmp3,0x20); \
(row2) = _mm256_permute2f128_pd(tmp0,0x31); \
(row3) = _mm256_permute2f128_pd(tmp2,0x31); \
}
仍然假设 AVX2,原则上转置 double[8][8]
和 int64_t[8][8]
是否大致相同?
PS:出于好奇,拥有 AVX512 会大大改变事情,对吗?
解决方法
经过评论中的一些思考和讨论,我认为这是最有效的版本,至少当源和目标数据在 RAM 中时。不需要AVX2,AVX1就够了。
主要思想是,现代 CPU 可以执行两倍于存储的加载微操作,并且在许多 CPU 上,使用 vinsertf128
将内容加载到向量的上半部分与常规 16 字节加载具有相同的成本。与您的宏相比,此版本不再需要这些相对昂贵的(大多数 CPU 上有 3 个延迟周期)vperm2f128
shuffle。
struct Matrix4x4
{
__m256d r0,r1,r2,r3;
};
inline void loadTransposed( Matrix4x4& mat,const double* rsi,size_t stride = 8 )
{
// Load top half of the matrix into low half of 4 registers
__m256d t0 = _mm256_castpd128_pd256( _mm_loadu_pd( rsi ) ); // 00,01
__m256d t1 = _mm256_castpd128_pd256( _mm_loadu_pd( rsi + 2 ) ); // 02,03
rsi += stride;
__m256d t2 = _mm256_castpd128_pd256( _mm_loadu_pd( rsi ) ); // 10,11
__m256d t3 = _mm256_castpd128_pd256( _mm_loadu_pd( rsi + 2 ) ); // 12,13
rsi += stride;
// Load bottom half of the matrix into high half of these registers
t0 = _mm256_insertf128_pd( t0,_mm_loadu_pd( rsi ),1 ); // 00,01,20,21
t1 = _mm256_insertf128_pd( t1,_mm_loadu_pd( rsi + 2 ),1 );// 02,03,22,23
rsi += stride;
t2 = _mm256_insertf128_pd( t2,1 ); // 10,11,30,31
t3 = _mm256_insertf128_pd( t3,1 );// 12,13,32,33
// Transpose 2x2 blocks in registers.
// Due to the tricky way we loaded stuff,that's enough to transpose the complete 4x4 matrix.
mat.r0 = _mm256_unpacklo_pd( t0,t2 ); // 00,10,30
mat.r1 = _mm256_unpackhi_pd( t0,t2 ); // 01,21,31
mat.r2 = _mm256_unpacklo_pd( t1,t3 ); // 02,12,32
mat.r3 = _mm256_unpackhi_pd( t1,t3 ); // 03,23,33
}
inline void store( const Matrix4x4& mat,double* rdi,size_t stride = 8 )
{
_mm256_storeu_pd( rdi,mat.r0 );
_mm256_storeu_pd( rdi + stride,mat.r1 );
_mm256_storeu_pd( rdi + stride * 2,mat.r2 );
_mm256_storeu_pd( rdi + stride * 3,mat.r3 );
}
// Transpose 8x8 matrix of double values
void transpose8x8( double* rdi,const double* rsi )
{
Matrix4x4 block;
// Top-left corner
loadTransposed( block,rsi );
store( block,rdi );
#if 1
// Using another instance of the block to support in-place transpose
Matrix4x4 block2;
loadTransposed( block,rsi + 4 ); // top right block
loadTransposed( block2,rsi + 8 * 4 ); // bottom left block
store( block2,rdi + 4 );
store( block,rdi + 8 * 4 );
#else
// Flip the #if if you can guarantee ( rsi != rdi )
// Performance is about the same,but this version uses 4 less vector registers,// slightly more efficient when some registers need to be backed up / restored.
assert( rsi != rdi );
loadTransposed( block,rsi + 4 );
store( block,rdi + 8 * 4 );
loadTransposed( block,rsi + 8 * 4 );
store( block,rdi + 4 );
#endif
// Bottom-right corner
loadTransposed( block,rsi + 8 * 4 + 4 );
store( block,rdi + 8 * 4 + 4 );
}
为了完整起见,这里有一个版本,它使用的代码与您的宏非常相似,加载次数是原来的两倍,存储的数量相同,并且洗牌次数更多。尚未进行基准测试,但我希望它会稍微慢一些。
struct Matrix4x4
{
__m256d r0,r3;
};
inline void load( Matrix4x4& mat,size_t stride = 8 )
{
mat.r0 = _mm256_loadu_pd( rsi );
mat.r1 = _mm256_loadu_pd( rsi + stride );
mat.r2 = _mm256_loadu_pd( rsi + stride * 2 );
mat.r3 = _mm256_loadu_pd( rsi + stride * 3 );
}
inline void store( const Matrix4x4& mat,mat.r3 );
}
inline void transpose( Matrix4x4& m4 )
{
// These unpack instructions transpose lanes within 2x2 blocks of the matrix
const __m256d t0 = _mm256_unpacklo_pd( m4.r0,m4.r1 );
const __m256d t1 = _mm256_unpacklo_pd( m4.r2,m4.r3 );
const __m256d t2 = _mm256_unpackhi_pd( m4.r0,m4.r1 );
const __m256d t3 = _mm256_unpackhi_pd( m4.r2,m4.r3 );
// Produce the transposed matrix by combining these blocks
m4.r0 = _mm256_permute2f128_pd( t0,t1,0x20 );
m4.r1 = _mm256_permute2f128_pd( t2,t3,0x20 );
m4.r2 = _mm256_permute2f128_pd( t0,0x31 );
m4.r3 = _mm256_permute2f128_pd( t2,0x31 );
}
// Transpose 8x8 matrix with double values
void transpose8x8( double* rdi,const double* rsi )
{
Matrix4x4 block;
// Top-left corner
load( block,rsi );
transpose( block );
store( block,rdi );
// Using another instance of the block to support in-place transpose,with very small overhead
Matrix4x4 block2;
load( block,rsi + 4 ); // top right block
load( block2,rsi + 8 * 4 ); // bottom left block
transpose( block2 );
store( block2,rdi + 4 );
transpose( block );
store( block,rdi + 8 * 4 );
// Bottom-right corner
load( block,rsi + 8 * 4 + 4 );
transpose( block );
store( block,rdi + 8 * 4 + 4 );
}
,
对于单个 SIMD 向量中可以容纳超过 1 行的小矩阵,AVX-512 具有非常好的 2 输入通道交叉洗牌,具有 32 位或 64 位粒度,并带有向量控制。 (与 _mm512_unpacklo_pd
不同,后者基本上是 4 个独立的 128 位随机播放。)
一个 4x4 double
矩阵“只有”128 个字节,两个 ZMM __m512d
向量,所以你只需要两个 vpermt2ps
(_mm512_permutex2var_pd
) 生成两个输出向量:每个输出向量一次洗牌,加载和存储都是全宽的。不过,您确实需要控制向量常量。
使用 512 位向量指令有一些缺点(时钟速度和执行端口吞吐量),但是如果您的程序可以在使用 512 位向量的代码中花费大量时间,那么使用更多指令可能会显着提高吞吐量数据与每条指令,并有更强大的洗牌。
对于 256 位向量,vpermt2pd ymm
可能不适用于 4x4,因为对于每个 __m256d 输出行,您想要的 4 个元素中的每一个都来自不同的输入行。所以一个 2-input shuffle 不能产生你想要的输出。
我认为小于 128 位粒度的车道交叉洗牌没有用,除非您的矩阵小到足以在一个 SIMD 向量中容纳多行。请参阅 How to transpose a 16x16 matrix using SIMD instructions?关于 32 位元素的算法复杂性推理 - 使用 AVX1 的 32 位元素的 8x8 xpose 与使用 AVX-512 的 64 位元素的 8x8 大致相同,其中每个 SIMD 向量恰好包含一整行。
因此不需要向量常量,只需立即对 128 位块进行混洗,以及 unpacklo/hi
用 512 位向量(8 个双精度)转置 8x8 会遇到同样的问题:8 个双精度的每个输出行需要来自 8 个输入向量中的每一个的 1 个双精度。 因此,最终我认为您需要一个类似于 Soonts 的 AVX 答案的策略,从 _mm512_insertf64x4(v,load,1)
开始,作为将 2 个输入行的前半部分转化为一个向量的第一步。
(如果您关心 KNL / Xeon Phi,@ZBoson 在 How to transpose a 16x16 matrix using SIMD instructions? 上的另一个答案展示了一些有趣的想法,使用合并掩码和 vpermpd
或 vpermq
等 1-input shuffle,而不是2-input shuffle,如 vunpcklpd
或 vpermt2pd
)
使用更宽的向量意味着更少的加载和存储,甚至可能更少的总混洗,因为每一个都结合了更多的数据。但是您还有更多的改组工作要做,将一行的所有 8 个元素放入一个向量中,而不仅仅是将一行大小的一半以块的形式加载和存储到不同的位置。不明显更好;如果我开始实际编写代码,我会更新这个答案。
请注意,Ice Lake(带有 AVX-512 的第一个消费者 CPU)每个时钟可以执行 2 次加载和 2 次存储。对于某些 shuffle,它比Skylake-X具有更好的shuffle吞吐量,但对于任何对这个或Sonts的答案有用的都没有。 (对于 ymm 和 zmm 版本,所有 vperm2f128
、vunpcklpd
和 vpermt2pd
仅在端口 5 上运行。https://uops.info/。vinsertf64x4 zmm,mem,1
是前面的 2 uop -end,并且需要一个加载端口和一个用于 p0/p5 的 uop。(不是 p1,因为它是 512 位 uop,另见 SIMD instructions lowering CPU frequency)。