MSVC对含字节数组的结构体生成不必要拷贝的问题咨询
关于MSVC消除不必要拷贝的问题
给定代码
#include <memory> struct Foo { alignas(__STDCPP_DEFAULT_NEW_ALIGNMENT__) std::byte _value[16]; }; Foo fByRef(const Foo&); Foo gByRef(const Foo& input) { const Foo intermediate1 = fByRef(input); const Foo intermediate2 = fByRef(intermediate1); return fByRef(intermediate2); } Foo fByVal(Foo); Foo gByVal(Foo input) { const Foo intermediate1 = fByVal(input); const Foo intermediate2 = fByVal(intermediate1); return fByVal(intermediate2); }
不同编译器的汇编输出
Clang(-O2 -DNDEBUG -std=c++17)生成的gByRef代码
sub rsp, 40 call fByRef(Foo const&)@PLT mov qword ptr [rsp + 16], rax mov qword ptr [rsp + 24], rdx lea rdi, [rsp + 16] call fByRef(Foo const&)@PLT mov qword ptr [rsp], rax mov qword ptr [rsp + 8], rdx mov rdi, rsp call fByRef(Foo const&)@PLT add rsp, 40 ret
MSVC 19.33(/O2 /DNDEBUG /std:c++17)生成的gByRef代码
$T1 = 32 $T2 = 32 intermediate1$ = 48 intermediate2$ = 64 __$ArrayPad$ = 80 __$ReturnAddress$ = 112 input$ = 120 Foo gByRef(Foo const &) PROC ; gByRef, COMDAT $LN4: push rbx sub rsp, 96 ; 00000060H mov rax, QWORD PTR __security_cookie xor rax, rsp mov QWORD PTR __$ArrayPad$[rsp], rax mov rbx, rcx lea rcx, QWORD PTR $T2[rsp] call Foo fByRef(Foo const &) ; fByRef lea rdx, QWORD PTR intermediate1$[rsp] lea rcx, QWORD PTR $T1[rsp] movaps xmm0, XMMWORD PTR [rax] movdqa XMMWORD PTR intermediate1$[rsp], xmm0 call Foo fByRef(Foo const &) ; fByRef lea rdx, QWORD PTR intermediate2$[rsp] mov rcx, rbx movaps xmm0, XMMWORD PTR [rax] movdqa XMMWORD PTR intermediate2$[rsp], xmm0 call Foo fByRef(Foo const &) ; fByRef mov rax, rbx mov rcx, QWORD PTR __$ArrayPad$[rsp] xor rcx, rsp call __security_check_cookie add rsp, 96 ; 00000060H pop rbx ret 0 Foo gByRef(Foo const &) ENDP
MSVC并未直接将intermediate1的地址传入第二次调用,而是在栈上创建临时变量,通过xmm0寄存器拷贝结果。
Clang生成的gByVal代码
gByVal(Foo): # @gByVal(Foo) push rax call fByVal(Foo)@PLT mov rdi, rax mov rsi, rdx call fByVal(Foo)@PLT mov rdi, rax mov rsi, rdx pop rax jmp fByVal(Foo)@PLT
MSVC生成的gByVal代码
$T1 = 32 $T2 = 32 $T3 = 32 $T4 = 48 $T5 = 48 __$ReturnAddress$ = 80 input$ = 88 Foo gByVal(Foo) PROC ; gByVal, COMDAT $LN4: push rbx sub rsp, 64 ; 00000040H movaps xmm0, XMMWORD PTR [rdx] mov rbx, rcx lea rdx, QWORD PTR $T3[rsp] movdqa XMMWORD PTR $T3[rsp], xmm0 lea rcx, QWORD PTR $T5[rsp] call Foo fByVal(Foo) ; fByVal lea rdx, QWORD PTR $T2[rsp] lea rcx, QWORD PTR $T4[rsp] movaps xmm0, XMMWORD PTR [rax] movdqa XMMWORD PTR $T2[rsp], xmm0 call Foo fByVal(Foo) ; fByVal lea rdx, QWORD PTR $T1[rsp] mov rcx, rbx movaps xmm0, XMMWORD PTR [rax] movdqa XMMWORD PTR $T1[rsp], xmm0 call Foo fByVal(Foo) ; fByVal mov rax, rbx add rsp, 64 ; 00000040H pop rbx ret 0 Foo gByVal(Foo) ENDP ; gByVal
可以看到,Clang能借助Itanium ABI做优化,但MSVC仍插入了不必要的临时变量与拷贝操作。
问题解答
1. 如何让MSVC消除这些不必要的拷贝?
- 启用更激进的优化选项:尝试添加
/Ob3(最大化内联优化)或/Oxs(优化代码大小,部分场景下会顺带消除冗余拷贝)选项,注意这类选项可能增加编译时间、降低调试友好性。 - 切换编译前端:使用
/clang:-O2让MSVC调用Clang前端编译,直接复用Clang的优化逻辑。 - 手动调整代码结构:
- 将
gByRef改为链式调用:return fByRef(fByRef(fByRef(input)));,减少命名中间变量,帮助优化器识别可省略的拷贝。 - 对于
gByVal,将参数改为引用传递,或利用C++17复制消除规则,让中间变量以临时对象形式传递,避免显式构造命名变量。
- 将
- 禁用栈保护(谨慎使用):添加
/GS-选项禁用栈安全检查,简化生成的代码,帮助优化器识别冗余操作,该操作存在安全风险,生产环境需评估后使用。
2. C++标准是否强制要求MSVC生成这些拷贝?
C++标准不强制要求生成这些拷贝。标准中的**复制消除(Copy Elision)**规则允许编译器省略不必要的拷贝/移动操作,即使拷贝/移动构造函数有副作用。示例代码中的中间变量intermediate1、intermediate2完全符合复制消除的条件,编译器有权省略相关拷贝。
MSVC生成冗余拷贝的原因是其默认Microsoft x64 ABI的规则与优化器实现策略:该ABI对大于8字节的对象,要求调用者分配栈空间并传递地址,而优化器在部分场景下未识别到可直接复用这些栈空间,导致额外拷贝。这是编译器实现选择,而非标准强制要求。
内容的提问来源于stack exchange,提问作者user1083696
相关产品推荐
相关产品推荐

