https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105260
Bug ID: 105260 Summary: Union with user-defined empty destructor leads to worse code-gen Product: gcc Version: 12.0 Status: UNCONFIRMED Severity: normal Priority: P3 Component: c++ Assignee: unassigned at gcc dot gnu.org Reporter: m.cencora at gmail dot com Target Milestone: --- Following code leads to unnecessary stack spill of deserialized Foo variable: g++ -std=c++17 -O2 #include <new> inline unsigned deserializeUInt(const unsigned char* &in) { unsigned out; __builtin_memcpy(&out, in, sizeof(out)); in += sizeof(out); out = __builtin_bswap32(out); return out; } struct Foo { unsigned a; unsigned b; static Foo deserialize(const unsigned char* &in) { return Foo{ deserializeUInt(in), deserializeUInt(in) }; } }; struct Result { unsigned idx; union { unsigned a; const void* ptr; }; }; Result dummyFunc(Foo); void deserializeAndInvoke(const unsigned char* it) { #ifndef WORKAROUND union NoDestroy { ~NoDestroy() {} Foo value; }; NoDestroy un{ Foo::deserialize(it) }; auto& arg = un.value; #elif WORKAROUND == 1 union NoDestroy { Foo value; }; NoDestroy un{ Foo::deserialize(it) }; auto& arg = un.value; #elif WORKAROUND == 2 alignas(Foo) char rawStorage[sizeof(Foo)]; auto& arg = *new (&rawStorage[0]) Foo{ Foo::deserialize(it) }; #endif dummyFunc(arg); } deserializeAndInvoke(unsigned char const*): mov edx, DWORD PTR [rdi] mov eax, DWORD PTR [rdi+4] bswap edx bswap eax mov DWORD PTR [rsp-16], edx mov DWORD PTR [rsp-12], eax mov rdi, QWORD PTR [rsp-16] jmp dummyFunc(Foo) The spill can be avoided, when we remove user-defined destructor of NoDestroy union, or when we construct Foo via placement new in raw buffer. Generated code with either of the workarounds: g++ -std=c++17 -O2 -DWORKAROUND=1 or g++ -std=c++17 -O2 -DWORKAROUND=2 deserializeAndInvoke(unsigned char const*): mov rax, rdi mov edi, DWORD PTR [rdi] mov eax, DWORD PTR [rax+4] bswap edi mov edi, edi bswap eax sal rax, 32 or rdi, rax jmp dummyFunc(Foo)