如何使用x86-32的MSVC++获得高效的asm来调零微小结构

How to get efficient asm for zeroing a tiny struct with MSVC++ for x86-32?

本文关键字:asm 结构 高效 x86-32 何使用 MSVC++      更新时间:2023-10-16

我的项目在Windows和Linux中都是为32位编译的。我有一个8字节的结构,几乎到处都在使用:

struct Value {
unsigned char type;
union {  // 4 bytes
unsigned long ref;
float num;
}
};

在很多地方,我需要将结构清零,这是这样做的:

#define NULL_VALUE_LITERAL {0, {0L}};
static const Value NULL_VALUE = NULL_VALUE_LITERAL;
// example of clearing a value
var = NULL_VALUE;

然而,即使启用了所有优化,这也不能编译成Visual Studio 2013中最高效的代码。我在程序集中看到的是,NULL_VALUE的内存位置正在被读取,然后被写入变量。这导致两次从内存读取,两次写入内存。然而,这种清除经常发生,即使是在时间敏感的例程中,我也在寻求优化。

如果我将值设置为NULL_value_LITERAL,情况会更糟。文字数据(同样都是零)被复制到临时堆栈值中,然后被复制到变量中——即使变量也在堆栈中。这太荒谬了。

还有一种常见的情况:

*pd->v1 = NULL_VALUE;

它的汇编代码与上面的var=NULL_VALUE类似,但如果我选择采用这种方法,我就无法使用内联汇编对其进行优化。

根据我的研究,清除记忆的最快方法是这样的:

xor eax, eax
mov byte ptr [var], al
mov dword ptr [var+4], eax

或者更好的是,因为结构对齐意味着在数据类型之后只有3个字节的垃圾

xor eax, eax
mov dword ptr [var], eax
mov dword ptr [var+4], eax

你能想出任何方法让我得到类似的代码,优化以避免完全不必要的内存读取吗?

我尝试了一些其他方法,最终创建了我觉得过于臃肿的代码,将32位0字面值写入两个地址,但IIRC将字面值写入内存仍然不如将寄存器写入内存快。我希望能获得任何额外的表现。

理想情况下,我也希望结果可读性强。非常感谢你的帮助。

对于与float的并集,我建议使用uint32_tunsigned int。Linux x86-64上的long是一种64位类型,这可能不是您想要的。


我可以在用于x86-32和x86-64的Godbolt编译器资源管理器上使用MSVC CL19-Ox重现错过的优化。适用于CL19:的解决方案

  • type设为unsigned int,而不是char,因此结构中没有填充,然后从文字{0, {0L}}而不是从static const Value对象进行赋值。(然后得到两个mov立即存储:mov DWORD PTR [eax], 0/mov DWORD PTR [eax+4], 0)。

    gcc也有在结构中填充的结构清零遗漏优化,但没有MSVC那么糟糕(Bug 82142)。它只是挫败了合并到更广泛的商店;它不会让gcc在堆栈上创建一个对象并从中复制。

  • std::memset:可能是最好的选择,MSVC使用SSE2将其编译为单个64位存储。xorps xmm0, xmm0/movq QWORD PTR [mem], xmm0。(gcc -m32 -O3将此memset编译为两个mov即时存储。)

void arg_memset(Value *vp) {
memset(vp, 0, sizeof(gvar));
}
;; x86 (32-bit) MSVC -Ox
mov      eax, DWORD PTR _vp$[esp-4]
xorps    xmm0, xmm0
movq     QWORD PTR [eax], xmm0
ret      0

这是我为现代CPU(英特尔和AMD)选择的。跨越缓存线的惩罚很低,如果指令不是一直发生,那么值得保存它。xor归零非常便宜(尤其是在英特尔SnB系列上)。


IIRC将文字写入内存仍然不如将寄存器写入内存快

在asm中,嵌入指令中的常量称为即时数据。mov-直接到内存在x86上基本上是可以的,但它的代码大小有点臃肿。

(仅限x86-64):具有RIP相对寻址模式和即时地址的存储无法在英特尔CPU上进行微熔断,因此它是2个融合的域uop。(请参阅Agner Fog的微阵列pdf,以及x86标签wiki中的其他链接。)这意味着,如果你对RIP相对地址进行了多个存储,那么(对于前端带宽)将寄存器清零是值得的。不过,其他寻址模式确实会融合,所以这只是一个代码大小的问题。

相关:微融合和寻址模式(索引寻址模式在Sandybridge/Ivybridge上不分层,但Haswell和更高版本可以保持索引存储微融合。)这不依赖于即时与寄存器源。


我认为memset的适合性非常差,因为它只是一个8字节的结构。

现代编译器知道一些大量使用/重要的标准库函数(memsetmemcpy等)的作用,并将它们视为内部函数。就优化而言,如果a = bmemcpy(&a, &b, sizeof(a))具有相同的类型,则它们之间的差异非常小。

在调试模式下,您可能会得到对实际库实现的函数调用,但无论如何,调试模式都非常缓慢。如果您有调试模式性能要求,这是不寻常的。(但对于需要跟上其他东西的代码确实会发生…)