MSVC /Os 与 GCC -O2 的非常详细的 ASM 代码(用于简单的模板代码)

Jep*_*sen 0 c++ assembly compiler-optimization visual-c++

我正在寻找一些示例std::visit,并且我想探索一下以下常见示例代码:

#include <iostream>
#include <variant>

struct Fluid { };
struct LightItem { };
struct HeavyItem { };
struct FragileItem { };

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>; // line not needed in C++20...

int main() {
    std::variant<Fluid, LightItem, HeavyItem, FragileItem> package(HeavyItem{});

    std::visit(overload{
        [](Fluid& )       { std::cout << "fluid\n"; },
        [](LightItem& )   { std::cout << "light item\n"; },
        [](HeavyItem& )   { std::cout << "heavy item\n"; },
        [](FragileItem& ) { std::cout << "fragile\n"; }
    }, package);
}
Run Code Online (Sandbox Code Playgroud)

我已经使用 GCC 和 MSVC 编译了代码,并且我注意到在最后一种情况下生成的 ASM 代码量比 GCC 大一个数量级。

这里是用 GCC 编译的代码

这里是用 MSVC 编译的代码

有没有办法知道为什么会有如此大的差异?有没有一种方法可以使用 MSVC 进行优化以获得类似于 GCC 的 ASM?

Pet*_*des 6

MSVC/Os本身并不能启用任何(?)优化,只是在您要启用优化时更改调整。 Code-gen 仍然像调试版本。显然它需要与其他选项结合才能使用? 它不像 GCC-Os那样使用 MSVC -O1


如果您查看 asm 源代码而不是二进制反汇编,则更容易看到 MSVCmain调用构造函数 , std::variant<...>::variant<...>将一些内存归零,然后调用std::visit。但 GCC 显然已将其简化为cout<<

std::visit如果您告诉 MSVC 使用-O2-O1代替进行完全优化, MSVC 也会进行内联和常量传播/Oshttps://godbolt.org/z/5MdcYh9xn有一个main和 GCC 差不多的,只是用常量的地址调用cout's 。operator<<


MSVC 的文档没有明确说明哪些选项实际上启用了(某些/任何)优化,而如果其他选项启用了某些优化,则只是偏向选择。

  • /O1设置生成最小大小代码的优化组合。

  • /O2设置优化代码以获得最大速度的优化组合。

  • ...

  • /Os告诉编译器优先考虑大小优化而不是速度优化。

  • /Ot(默认设置)告诉编译器优先考虑速度优化而不是大小优化。

    [但请注意,优化通常默认关闭,并且这是默认设置并不会改变这一点。所以/Os似乎/Ot根本没有启用优化。]

  • /Ox是一个组合选项,选择多项优化,重点是速度。/Ox 是 /O2 优化的严格子集。

如果我没有进行测试,我会从该文档中假设至少-Os 可以实现一些优化。(MSVC 接受-/作为选项名称的开头;我-在这个答案中写了大部分内容,因为这就是 Unix/Linux 使用的,并且我知道 MSVC 接受它。)


(MSVC 总是在其 asm 源输出中打印大量内容,包括内联模板函数的独立定义。我认为这就是您使用编译为二进制来查看链接的可执行文件中实际最终结果的原因。由于某些原因/O1,在 Godbolt 上构建的版本可以运行,但不会显示反汇编:Cannot open compiler generated file [...]\output.s.obj。或者不,它对我来说只是间歇性地损坏,即使使用您的原始链接也是如此。)


更简单的例子

例如,bar()内联后这变得非常简单,但/Os即使对于这个微不足道的函数,MSVC 也不会这样做。事实上,code-gen 与没有选项的默认调试模式相同。

int foo(int a,int b){ return a+b*5;}
int bar(int x){
    return foo(3*x, 2*x);
}
Run Code Online (Sandbox Code Playgroud)
; MSVC 19.32 /Os
int foo(int,int) PROC                                  ; foo
        mov     DWORD PTR [rsp+16], edx
        mov     DWORD PTR [rsp+8], ecx
        imul    eax, DWORD PTR b$[rsp], 5
        mov     ecx, DWORD PTR a$[rsp]
        add     ecx, eax
        mov     eax, ecx
        ret     0
int foo(int,int) ENDP                                  ; foo

x$ = 48
int bar(int) PROC                                 ; bar
$LN3:
        mov     DWORD PTR [rsp+8], ecx
        sub     rsp, 40                             ; 00000028H
        mov     eax, DWORD PTR x$[rsp]
        shl     eax, 1
        imul    ecx, DWORD PTR x$[rsp], 3
        mov     edx, eax
        call    int foo(int,int)                     ; foo
        add     rsp, 40                             ; 00000028H
        ret     0
int bar(int) ENDP                                 ; bar
Run Code Online (Sandbox Code Playgroud)

不仅仅是缺乏内联;注意计算和时 和的溢出x和两次重新加载。与 相同,溢出其参数并重新加载,就像调试构建一样。起初我认为它不完全是一个调试版本,因为没有使用 RBP 作为帧指针,但 MSVC 生成相同的 asm,没有选项。x*2x*3foo

与可用的优化级别 MSVC 相比-O1,其中代码生成与 GCC-O2-Os

; MSVC 19.32 -O1
x$ = 8
int bar(int) PROC                                 ; bar, COMDAT
        imul    eax, ecx, 13
        ret     0
int bar(int) ENDP                                 ; bar

a$ = 8
b$ = 16
int foo(int,int) PROC                                  ; foo, COMDAT
        lea     eax, DWORD PTR [rcx+rdx*4]
        add     eax, edx
        ret     0
int foo(int,int) ENDP                                  ; foo
Run Code Online (Sandbox Code Playgroud)