将内存中的8个字符作为打包的单精度浮点数加载到__m256变量中
Loading 8 chars from memory into an __m256 variable as packed single precision floats
我正在优化图像上的高斯模糊算法,我想用__m256固有变量替换下面代码中浮点缓冲区[8]的使用。哪一系列的指令最适合这个任务?
// unsigned char *new_image is loaded with data
...
float buffer[8];
buffer[x ] = new_image[x];
buffer[x + 1] = new_image[x + 1];
buffer[x + 2] = new_image[x + 2];
buffer[x + 3] = new_image[x + 3];
buffer[x + 4] = new_image[x + 4];
buffer[x + 5] = new_image[x + 5];
buffer[x + 6] = new_image[x + 6];
buffer[x + 7] = new_image[x + 7];
// buffer is then used for further operations
...
//What I want instead in pseudocode:
__m256 b = [float(new_image[x+7]), float(new_image[x+6]), ... , float(new_image[x])];
如果您正在使用AVX2,您可以使用PMOVZX在256b寄存器中将字符零扩展为32位整数。从这里,可以就地转换为float。
; rsi = new_image
VPMOVZXBD ymm0, [rsi] ; or SX to sign-extend (Byte to DWord)
VCVTDQ2PS ymm0, ymm0 ; convert to packed foat
这是一个很好的策略,即使你想为多个向量这样做,但更好的可能是一个128位的广播负载来馈送vpmovzxbd ymm,xmm
和vpshufb ymm
(_mm256_shuffle_epi8
)的高64位,因为英特尔snb系列cpu不微融合vpmovzx ymm,mem
,只有vpmovzx xmm,mem
。(https://agner.org/optimize/)。广播负载是单向上的,不需要ALU端口,纯粹在负载端口中运行。因此,这是3个累计到bcast-load + vpmovzx + vpshufb。
(TODO:写一个内在的版本。它还避免了_mm_loadl_epi64
->_mm256_cvtepu8_epi32
错过优化的问题。
当然,这需要在另一个寄存器中有一个shuffle控制向量,所以只有当你可以多次使用它时才值得。
vpshufb
是可用的,因为每个通道所需的数据都来自广播,并且洗牌控制的高位将相应的元素归零。
这个广播+洗牌策略可能在Ryzen上很好;Agner Fog没有在上面列出vpmovsx/zx ymm
的计数。
<<BK_HR>做strong>不是做一些类似128位或256位的加载,然后将其打乱以提供进一步的vpmovzx
指令。总的shuffle吞吐量可能已经成为瓶颈,因为vpmovzx
是shuffle。英特尔Haswell/Skylake(最常见的AVX2搜索)每时钟有1次洗牌,但每时钟有2次加载。使用额外的shuffle指令而不是将单独的内存操作数折叠到vpmovzxbd
中是非常糟糕的。只有当你能减少总上限计数,就像我建议的broadcastload + vpmovzxbd + vpshufb,它是一个胜利。
我的回答缩放字节像素值(y=ax+b)与SSE2(作为浮点数)?可能与转换回uint8_t
有关。如果使用AVX2packssdw/packuswb
,那么打包到字节之后的部分是半棘手的,因为它们在通道内工作,不像vpmovzx
。
只有AVX1,没有AVX2,你应该这样做:
VPMOVZXBD xmm0, [rsi]
VPMOVZXBD xmm1, [rsi+4]
VINSERTF128 ymm0, ymm0, xmm1, 1 ; put the 2nd load of data into the high128 of ymm0
VCVTDQ2PS ymm0, ymm0 ; convert to packed float. Yes, works without AVX2
你当然不需要浮点数数组,只需要__m256
向量数组。
GCC/MSVC错过了VPMOVZXBD ymm,[mem]
与intrinsic的优化
GCC和MSVC不擅长将_mm_loadl_epi64
折叠成vpmovzx*
的内存操作数。(但至少有是加载固有的正确的宽度,不像pmovzxbq xmm, word [mem]
。)
我们得到一个vmovq
加载,然后是一个带有XMM输入的单独的vpmovzx
。(使用ICC和clang3.6+,我们可以从使用_mm_loadl_epi64
获得安全的最优代码,就像从gcc9+一样)
但是gcc8.3和更早的可以将_mm_loadu_si128
16字节的固有负载折叠到8字节的内存操作数中。这在GCC的-O3
上提供了最佳的asm,但在-O0
上是不安全的,因为它编译成实际的vmovdqu
加载,接触的数据比我们实际加载的要多,并且可能超出页面的末尾。
由于这个答案而提交了两个gcc bug:
- SSE/AVX movq load (_mm_cvtsi64_si128)不被折叠到pmovzx (修复gcc9,但是修复了128位加载的加载折叠,所以旧GCC的变通方法使gcc9做得更糟。)
- 32位模式下x86
MOVQ m64, %xmm
无固有特性。(在clang/LLVM中也要报告吗?)
没有内在的使用SSE4.1pmovsx
/pmovzx
作为负载,只有__m128i
源操作数。但是asm指令只读取它们实际使用的数据量,而不是16字节的__m128i
内存源操作数。与punpck*
不同,您可以在页面的最后8B中使用它而不会出现故障。(即使是非avx版本也可以使用未对齐的地址)。
所以这就是我想出的邪恶解决方案。不要使用这个,#ifdef __OPTIMIZE__
是坏的,可能会产生只发生在调试构建或只发生在优化构建中的错误!
#if !defined(__OPTIMIZE__)
// Making your code compile differently with/without optimization is a TERRIBLE idea
// great way to create Heisenbugs that disappear when you try to debug them.
// Even if you *plan* to always use -Og for debugging, instead of -O0, this is still evil
#define USE_MOVQ
#endif
__m256 load_bytes_to_m256(uint8_t *p)
{
#ifdef USE_MOVQ // compiles to an actual movq then movzx ymm, xmm with gcc8.3 -O3
__m128i small_load = _mm_loadl_epi64( (const __m128i*)p);
#else // USE_LOADU // compiles to a 128b load with gcc -O0, potentially segfaulting
__m128i small_load = _mm_loadu_si128( (const __m128i*)p );
#endif
__m256i intvec = _mm256_cvtepu8_epi32( small_load );
//__m256i intvec = _mm256_cvtepu8_epi32( *(__m128i*)p ); // compiles to an aligned load with -O0
return _mm256_cvtepi32_ps(intvec);
}
启用USE_MOVQ后,gcc -O3
(v5.3.0)发出。(MSVC也是)
load_bytes_to_m256(unsigned char*):
vmovq xmm0, QWORD PTR [rdi]
vpmovzxbd ymm0, xmm0
vcvtdq2ps ymm0, ymm0
ret
愚蠢的vmovq
是我们想要避免的。如果您让它使用不安全的loadu_si128
版本,它将生成良好的优化代码。
GCC9, clang和ICC emit:
load_bytes_to_m256(unsigned char*):
vpmovzxbd ymm0, qword ptr [rdi] # ymm0 = mem[0],zero,zero,zero,mem[1],zero,zero,zero,mem[2],zero,zero,zero,mem[3],zero,zero,zero,mem[4],zero,zero,zero,mem[5],zero,zero,zero,mem[6],zero,zero,zero,mem[7],zero,zero,zero
vcvtdq2ps ymm0, ymm0
ret
使用intrinsic编写avx1版本对于读者来说是一个无趣的练习。你问的是"指示",而不是"内在",这是内在的一个缺口。在我看来,使用_mm_cvtsi64_si128
来避免从越界地址加载是愚蠢的。我希望能够从它们所映射的指令的角度来考虑内在函数,通过加载/存储内在函数来通知编译器关于对齐保证或缺乏对齐保证。对于一个我不想要的指令,不得不使用内在指令是相当愚蠢的。
还需要注意的是,如果您查看intelininref手册,movq有两个单独的条目:
movd/movq,可以有一个整数寄存器作为src/dest操作数的版本(
66 REX.W 0F 6E
(或VEX.128.66.0F.W1 6E
) for (V) movq xmm, r/m64)。在这里,您将找到可以接受64位整数_mm_cvtsi64_si128
的内在函数。(有些编译器没有在32位模式下定义它。)movq:可以使用两个xmm寄存器作为操作数的版本。这是MMXreg -> MMXreg指令的扩展,它也可以像MOVDQU一样加载/存储。
MOVQ xmm, xmm/m64)
的操作码F3 0F 7E
(VEX.128.F3.0F.WIG 7E
)asm ISA ref手册只列出了在复制vector时将其高64b归零的
m128i _mm_mov_epi64(__m128i a)
固有函数。但是intrinsic指南确实列出了_mm_loadl_epi64(__m128i const* mem_addr)
它有一个愚蠢的原型(指针指向一个16字节的__m128i
类型,而实际上它只加载8字节)。它可以在所有4个主要的x86编译器上使用,并且实际上应该是安全的。请注意,__m128i*
只是传递给这个不透明的内部,而不是实际上被解引用。还列出了更合理的
_mm_loadu_si64 (void const* mem_addr)
,但是gcc缺少它。
- std::原子加载和存储都需要吗
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 如何加载(或映射)文件部分的最大大小,但适合在Windows上的RAM
- C++ 雷神库 - 使用资源加载器类时出现问题(不命名类型)
- 如何修复此错误:未定义对"距离(浮点数,浮点数,浮点数,浮点数,浮点数)"的引用
- C++浮点数据类型和字符串数据类型无法子到模板函数中
- 为什么加载SDF会导致Mobilizer创建闭环错误
- C++atioglxx.pdb未加载错误glBufferData OpenGL
- 如何使用tinyxml2从XML加载父实体和子实体
- 如何在C++中使用pybind11加载一个pickle python列表
- 系统.将数组移交给c#中动态加载的c++DLL时发生AccessViolationException
- IBM 单精度浮点数据转换为预期值
- 将浮点数保存在文本文件中,然后从文本文件加载回去(OpenGL和C++)
- C++ std::bad_alloc 加载 190 万行浮点值文件 - 提供源代码
- 在OpenCV中保存并加载浮点值
- 从新发行版和不同发行版加载LD_PRELOAD libstdc++.so.6和libc.so.6时出现浮点异常
- 打印最大的单精度浮点数
- 将内存中的8个字符作为打包的单精度浮点数加载到__m256变量中
- 加载常量的FLD浮点指令
- 如何判断双精度浮点数是否可以安全地存储为单精度浮点数