强制编译器使用 Intrinsics 中的内存操作数

Posted

技术标签:

【中文标题】强制编译器使用 Intrinsics 中的内存操作数【英文标题】:Force compiler to use memory operand from Intrinsics 【发布时间】:2022-01-12 14:03:32 【问题描述】:

是否存在强制 C 编译器直接使用内存操作数的语法?

在过去的 asm 时代,我们只需在指令中写入操作数的位置 - “真实”寄存器或内存指针(地址指向的位置)。

但是在 CI 的内在伪 asm 中看不到强制编译器在指令中使用内存指针的方法(拒绝将数据从内存(缓存)加载到“寄存器”,即垃圾寄存器文件加载的内容到缓存和导致重新加载并受到惩罚)。

我知道程序员很容易将“变量”操作数简单地写入 instinsic 并让编译器决定是先从内存加载还是直接使用它(如果可能的话)。

当前任务:我想在 AVX2 CPU 上使用 512 字节寄存器文件(每个 32 字节的 16 个 ymm '寄存器')计算 8x8 8 位块序列的 SAD。因此它可以加载 8 个 8x8 8bit 源块来完全填满可用的 AVX2 寄存器文件。

我想在 all 寄存器文件中加载源块,并针对这些源块和每个 ref 位置从内存中测试不同的 'ref' 位置一次。所以我想防止 CPU 将 ref 块从缓存加载到寄存器文件并在悲伤指令中使用“内存操作数”。

使用 asm,我们只需编写类似的东西

(load all 16 ymm registers with src)
vpsadbw ymm0, ymm0, [ref_base_address_register + some_offset...]

但在 C 文本中,它是内在的

__m256i src = load_src(src_pointer);
__m256i ref = load_ref(ref_pointer); 
__m256i sad_result= _mm256_sad_epu8(src, ref)

它没有办法让编译器使用像这样的有效内存操作数

__m256i src = load_src(src_pointer);
__m256i sad_result= _mm256_sad_epu8(src, *ref_pointer)

或者取决于“任务大小”,如果编译器将用完可用寄存器,它将自动切换到内存操作数版本并且程序员可以编写

__m256i sad_result=_mm256_sad_epu8(*(__m256i*)src_pointer, *(__m256i*)ref_pointer)

并期望编译器将 2 个操作数之一加载到寄存器文件并从内存中使用下一个?

【问题讨论】:

【参考方案1】:

不,没有,除了一些具有指针操作数的特定内在函数,即使它们不是纯加载或纯存储1

内在函数的部分目的是抽象出寄存器分配细节,就像它对intdouble 所做的那样,所以当这是一件好事时,由编译器将内容保存在寄存器中。这通常会发生,如果您担心优化器无法将负载固有的折叠到内存源操作数中,请检查 asm 输出(例如,在 https://godbolt.org/ 或本地)。 AVX(VEX 编码)允许折叠甚至未对齐的负载,因为与传统 SSE 不同,默认情况下不需要对齐。

当编译器失败时,这可能会很糟糕,就像许多用于 _mm256_cvtepu8_epi32( _mm_loadl_epi64(p) ) 的 - GCC 曾经发出实际的 movq 负载和 reg-reg vpmovzxbd。只有在 GCC9 及更高版本中,我们才能获得内存源 vpmovzxbd。 (Loading 8 chars from memory into an __m256 variable as packed single precision floats)

或者对于您的情况,如果编译器溢出了错误的东西,唯一的解决方法是提交错过优化的错误报告并等待新的编译器版本。或者用 asm 编写一个版本(内联或独立)。


内在函数模型的设计者还希望提供 load/loadustore/storeu 内在函数来将对齐信息传达给编译器。 (对于浮点数/双精度,在float*__m128* 之间进行转换或其他。)_mm_load_si128((__m128i*)foo) 完全*(__m128i*)foo 相同,并且与访问数组元素几乎相同__m128i,如果编译器无法看穿数组并将其保存在寄存器中。见Is `reinterpret_cast`ing between hardware SIMD vector pointer and the corresponding type an undefined behavior?

令人困惑的加载内在函数看起来像 asm 加载/存储,但启用优化后它们实际上根本不同。


脚注 1:AVX-512 有一些特殊指令,它们具有相应有趣的内在函数,例如 VPMOVDB mem128 k, zmm2 - void _mm512_mask_cvtepi32_storeu_epi8(void * d, __mmask16 k, __m512i a);。能够存储到内存中,Xeon Phi (Knight's Landing) 可以在不使用 AVX-512BW 的情况下为 vmovdqu8 进行字节屏蔽存储。

【讨论】:

以上是关于强制编译器使用 Intrinsics 中的内存操作数的主要内容,如果未能解决你的问题,请参考以下文章

如何在x86上使用gcc强制执行内存排序

Java中 = 和 += 的区别

Neon Intrinsics各函数介绍

Neon Intrinsics各函数介绍

无符号字符图像上的快速高斯模糊 - ARM Neon Intrinsics - iOS Dev

强制向操作系统释放内存