如何使用x86-32的MSVC++获得高效的asm来调零微小结构
How to get efficient asm for zeroing a tiny struct with MSVC++ for x86-32?
我的项目在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_t
或unsigned 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字节的结构。
现代编译器知道一些大量使用/重要的标准库函数(memset
、memcpy
等)的作用,并将它们视为内部函数。就优化而言,如果a = b
和memcpy(&a, &b, sizeof(a))
具有相同的类型,则它们之间的差异非常小。
在调试模式下,您可能会得到对实际库实现的函数调用,但无论如何,调试模式都非常缓慢。如果您有调试模式性能要求,这是不寻常的。(但对于需要跟上其他东西的代码确实会发生…)
- 如何循环打印顶点结构
- 通过方法访问结构
- 使用不带参数的函数访问结构元素
- 预处理器:插入结构名称中的前一个行号
- 为什么在没有显式默认构造函数的情况下,将另一个结构封装在联合中作为成员的结构不能编译
- 孤立代码块在结构中引发异常
- 有什么方法可以遍历结构吗
- 如何在 C# 中映射双 C 结构指针?
- 如何在C++中使用结构生成映射
- 无法将结构注册为增强几何体3D点
- 多成员Constexpr结构初始化
- C++将文本文件中的数据读取到结构数组中
- 如何重构类层次结构以避免菱形问题
- 如何在C++中序列化结构数据
- std::vector的包装器,使数组的结构看起来像结构的数组
- 没有为自己的结构调用列表推回方法
- 奇怪的结构&GCC&clang(void*返回类型)
- 在 c++ 中拥有一组结构的正确方法是什么?
- 未使用的 asm() 在不受支持的体系结构上的行为
- 如何使用x86-32的MSVC++获得高效的asm来调零微小结构