From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: by sourceware.org (Postfix, from userid 48) id E19483858D39; Fri, 3 Nov 2023 09:10:37 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org E19483858D39 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gcc.gnu.org; s=default; t=1699002637; bh=5axNL2s1OfE0Yrezjb9khc3u5R6izpvnLSSrYlVjIEU=; h=From:To:Subject:Date:In-Reply-To:References:From; b=LxyEYvQeIPiOvS1/+ZvtKiBagk6XDeDtwwY16GcW7G2GgrL0y8LOarnTFk1TGXjz5 SCdDWGDh2mU8UufhxwobkDPp4gY5myXdOiAclMP3gDsMtxlyQejwbEDbaqnU6m6pR3 PfxnVEFD/38oSzRd22bPsG7w2IVFA/1IksHmdUDk= From: "redi at gcc dot gnu.org" To: gcc-bugs@gcc.gnu.org Subject: [Bug c++/112335] missed optimization on reset and assign unique_ptr Date: Fri, 03 Nov 2023 09:10:37 +0000 X-Bugzilla-Reason: CC X-Bugzilla-Type: changed X-Bugzilla-Watch-Reason: None X-Bugzilla-Product: gcc X-Bugzilla-Component: c++ X-Bugzilla-Version: 14.0 X-Bugzilla-Keywords: missed-optimization X-Bugzilla-Severity: normal X-Bugzilla-Who: redi at gcc dot gnu.org X-Bugzilla-Status: RESOLVED X-Bugzilla-Resolution: INVALID X-Bugzilla-Priority: P3 X-Bugzilla-Assigned-To: unassigned at gcc dot gnu.org X-Bugzilla-Target-Milestone: --- X-Bugzilla-Flags: X-Bugzilla-Changed-Fields: Message-ID: In-Reply-To: References: Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable X-Bugzilla-URL: http://gcc.gnu.org/bugzilla/ Auto-Submitted: auto-generated MIME-Version: 1.0 List-Id: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=3D112335 --- Comment #8 from Jonathan Wakely --- (In reply to Federico Kircheis from comment #7) > > Demo: https://godbolt.org/z/PWd96j4fz >=20 > Thank you for the example, but honestly, I think I am still missing > something. >=20 > If you modify the main this way >=20 > ---- > int main() > { > global =3D nullptr; > std::unique_ptr local1(new s); > bar1(global, local1); > global =3D nullptr; > std::unique_ptr local2(new s); > bar1(global, local2); > } > ---- >=20 >=20 > or this way >=20 >=20 > ---- > int main() > { > global =3D nullptr; > std::unique_ptr local1(new s); > bar2(global, local1); > global =3D nullptr; > std::unique_ptr local2(new s); > bar2(global, local2); > } > ---- >=20 > the output is the same. > First time it prints 0, the second time a non-0 address. You're right, my example failed to show the difference I was trying to show, which is that the value of ps1.get() observed by the destructor is differen= t. To show that, global.get() must be non-null initially, but it was always nu= l in my example (the addresses being printed out where when the destructors ran = in main, not in bar1 and bar2). Try this one: https://godbolt.org/z/KEbnPxEdq This shows that in bar1 the value of ps1.get() is null when the destructor runs. In bar2 the value of ps2.get() is not null when the destructor runs. >=20 > > But a much simpler example where the difference is observable is when t= he two arguments to bar1 and bar2 are the same object: > > https://godbolt.org/z/z8fsTan8K >=20 > And this is absolutely correct, did not think of it. >=20 > > I'm not sure what you mean here. The postcondition holds unless the del= eter *also* destroys the unique_ptr itself. >=20 > As written, I was not sure if this plays a role; this is what the standard > says (and I#ve misunderstood the example of Andrew). > A similar effect can be observed in the following example >=20 > ---- > #include >=20 > using s =3D int; // for simplicity >=20 > void bar1(std::unique_ptr& ps1, std::unique_ptr& ps2){ > ps1.reset(); > ps1 =3D std::move(ps2); > } > void bar2(std::unique_ptr& ps1, std::unique_ptr& ps2){ > ps1 =3D std::move(ps2); > } >=20 > void bar3(std::unique_ptr& ps1, std::unique_ptr& ps2){ > ps1.reset(); > ps1.reset(); > ps1 =3D std::move(ps2); > } > ---- >=20 > While you are right that bar1 and bar2 have different behavior if they po= int > to the same object, bar1 and bar3 are fundamentally the same function, ev= en > in case of aliasing. > Unless the post-condition of reset is not hold; otherwise the second > "ps1.reset()" could be completely removed, as ps1 is nullptr. The postcondition has to hold, by definition, that's what it means. It only doesn't hold if the unique_ptr object itself ceases to exist, and that does= n't happen with std::default_delete. It's irrelevant whether it happens with so= me other deleters, because the code you're compiling doesn't use those other deleters (if it did, it would be compiled differently). This example is more interesting, bar1 does: test r12, r12 je .L11 mov rdi, r12 call s::~s() [complete object destructor] mov rdi, r12 mov esi, 1 call operator delete(void*, unsigned long) But bar3 has extra instructions: test r12, r12 je .L11 mov rdi, r12 call s::~s() [complete object destructor] mov rdi, r12 mov esi, 1 call operator delete(void*, unsigned long) mov r12, QWORD PTR [rbx] mov QWORD PTR [rbx], 0 test r12, r12 je .L11 mov rdi, r12 call s::~s() [complete object destructor] mov esi, 1 mov rdi, r12 call operator delete(void*, unsigned long) I think the reason the redundant ps1.reset() isn't removed is that the comp= iler doesn't know what operator delete(void*, size_t) does. In theory, the user could replace that function with one which modifies a global unique_ptr, and ps1 could alias that global. https://godbolt.org/z/WGPTesEb3 shows that bar3 is not the same as bar1, because it runs an additional destructor if operator delete() plays silly games. If we had PR 110137 then I think the redundant ps1.reset() would be optimiz= ed away. Without it, the compiler assumes operator delete() might be silly. Luckily, that bar3 code seems unlikely to be written. The second ps1.reset() can obviously be removed by the programmer.=