【问题标题】:Very verbose ASM code of MSVC /Os vs GCC -O2 for simple template codeMSVC /Os 的非常冗长的 ASM 代码与 GCC -O2 的简单模板代码
【发布时间】:2022-11-24 18:19:08
【问题描述】:

我正在寻找 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);
}

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

Here the code compiled with GCC

Here the code compiled with MSVC

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

【问题讨论】:

  • 我认为这里的默认输出具有误导性。尝试将对 std::visit 的调用移动到一个单独的函数(例如 visitor),关闭“编译为二进制”和“执行”,然后查看只是visitor 函数。两者对我来说大小大致相同:他们检查一个标签并发送到一个分支机构,然后由该分支机构进行实际打印。
  • 大多数 MSVC 代码是 iostream 的初始化,在 GCC 程序集中,它隐藏在几个函数调用中

标签: c++ assembly visual-c++ compiler-optimization


【解决方案1】:

MSVC /Os 单独不启用任何(?)优化,如果您要启用优化,只需更改调整。Code-gen 仍然像一个调试版本。显然它需要与其他选项结合使用?它不像 GCC -Os,因为它使用 MSVC -O1


如果您查看 asm 源代码而不是二进制反汇编,则更容易看出 MSVC 的 main 调用构造函数 std::variant&lt;...&gt;::variant&lt;...&gt;,清零一些内存,然后调用 std::visit。但是 GCC 显然已经将其内联为 cout&lt;&lt;

MSVC 还通过 std::visit 进行内联和常量传播,如果您告诉它要完全优化,使用 -O2-O1 而不是 /Os。 (https://godbolt.org/z/5MdcYh9xn)。

根据 MSVC's docs,目前还不清楚哪些选项实际上启用了(某些/任何)优化,而如果其他选项启用了某些优化,则只是偏向选择。

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

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

  • ...

  • /Os 告诉编译器支持大小优化而不是速度优化。

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

    [但请注意,优化一般默认情况下处于关闭状态,而这是默认设置并不会改变这一点。所以/Os/Ot似乎根本没有启用优化。]

  • /Ox 是一个组合选项,它选择了几个强调速度的优化。 /Ox 是 /O2 优化的严格子集。


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


更简单的例子

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

int foo(int a,int b){ return a+b*5;}
int bar(int x){
    return foo(3*x, 2*x);
}
; 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

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

与具有可用优化级别的 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-01
    • 2023-03-26
    • 2012-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多