我的项目在 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 的内存位置,然后写入 var。这导致两次从内存读取和两次写入内存。然而,这种清理经常发生,即使在对时间敏感的例程中也是如此,我正在寻求优化。
如果我将该值设置为 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 将文字写入内存仍然不如写入寄存器快内存。我正在寻找我能获得的任何额外性能。
理想情况下,我还希望结果具有很高的可读性。感谢您的帮助。
最佳答案
我建议使用 uint32_t
或 unsigned int
与 float
union 。 long
在 Linux x86-64 上是 64 位类型,这可能不是您想要的。
我可以使用 MSVC CL19 -Ox
重现优化失误 on the Godbolt compiler explorer适用于 x86-32 和 x86-64。适用于 CL19 的解决方法:
将
type
设为unsigned int
而不是char
,因此结构中没有填充,然后从文字{0, {0L}}
而不是static const Value
对象。 (然后你得到两个 mov-immediate 存储: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)选择的。跨越高速缓存线的惩罚非常低,如果它不是一直发生,则值得保存一条指令。异或归零非常便宜(尤其是在 Intel SnB 系列上)。
IIRC writing a literal to memory still isn't as fast as writing a register to memory
在asm中,嵌入在指令中的常量称为立即数。 mov
- 立即进入内存在 x86 上基本没问题,但对于代码大小来说有点臃肿。
(仅限 x86-64):具有 RIP 相对寻址模式和立即数的存储不能在 Intel CPU 上进行微融合,因此它是 2 个融合域微指令。 (参见 Agner Fog's microarch pdf 和 x86 标签 wiki 中的其他链接。)这意味着如果您要对一个 RIP 相关地址执行多个存储操作,那么将寄存器清零(对于前端带宽)是值得的。不过,其他寻址模式确实会融合,所以这只是一个代码大小问题。
相关:Micro fusion and addressing modes (索引寻址模式在 Sandybridge/Ivybridge 上未层压,但 Haswell 和更高版本可以保持索引存储微融合。)这不依赖于立即源与寄存器源。
I think memset would be a very poor fit since this is just an 8-byte struct.
现代编译器知道一些频繁使用/重要的标准库函数的作用(memset
、memcpy
等),并将它们视为内部函数。就优化而言,a = b
和 memcpy(&a, &b, sizeof(a))
如果它们具有相同的类型,则它们之间的差异很小。
您可能会在 Debug模式下获得对实际库实现的函数调用,但无论如何 Debug模式都非常慢。如果您有 Debug模式性能要求,那是不寻常的。 (但是对于需要跟上其他东西的代码确实会发生......)
关于c++ - 如何使用 MSVC++ for x86-32 获得有效的 asm 来归零一个微小的结构?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47381520/