c++ - 如何使用 MSVC++ for x86-32 获得有效的 asm 来归零一个微小的结构?

标签 c++ assembly optimization x86 compiler-optimization

我的项目在 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_tunsigned intfloat 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 标签 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.

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

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

关于c++ - 如何使用 MSVC++ for x86-32 获得有效的 asm 来归零一个微小的结构?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47381520/

相关文章:

c++ - 是否可以将cin与cout并行使用?

c++ - 为什么二维数组上的指针算法有效?

assembly - x86 程序集直接写入 VGA 简单操作系统

r - 为什么 mean() 这么慢?

php - 从 Amazon S3 自动打包 Javascript/CSS?

c++ - 在 QQuickItem 引用上发出信号

c++ - wxFrame 不处理 Tab 按钮

java - 从头开始创建 .txt 文件

delphi - 将字节值广播到 Delphi ASM 中的所有 16 个 XMM 插槽

python - 为什么首先分配给变量时 pandas.dataframe.groupby 更快?