3 ms·
That's because you are explicitly telling the compiler to create a copy of your shared pointer, that involves incrementing/decrementing the counter: static i
by UkrainianJew 4y ago
That's because you are explicitly telling the compiler to create a copy of your shared pointer, that involves incrementing/decrementing the counter:
static int add_numbers(std::shared_ptr<Data> p) {
return p->a + p->b;
}
Try this:
static int add_numbers(const std::shared_ptr<Data> &p) {
return p->a + p->b;
}
You'll get 4 instructions (don't forget to put __attribute__((noinline)) on add_numbers()):
add_numbers(std::shared_ptr<Data>&):
mov rdx, QWORD PTR [rdi]
mov eax, DWORD PTR [rdx+4]
add eax, DWORD PTR [rdx]
ret
The only difference is that Rust physically placed the structure in 2 registers (hence just 1 instruction), while C++ physically placed it into memory (hence the need to 2 extra instructions fetching the values).
- codedokode 4y ago> You'll get 4 instructions (don't forget to put __attribute__((noinline)) on add_numbers()): That's 4 instructions only for add_numbers(), but add_two_numbers_smart_way() still is unoptimized.
- UkrainianJew 4y agoWell, you can somewhat simplify it: int add_two_numbers_smart_way(int a, int b) { auto x = std::make_shared<Data>(a, b); return add_numbers(x); } But you still get what you ask the compiler to deliver. If you don't specifically ask it to count references, it won't: static int add_numbers(Data *p) { return p->a + p->b; } int add_two_numbers_smart_way(int a, int b) { Data x(a, b); return add_numbers(&x); } Here's the result: add_two_numbers_smart_way(int, int): lea eax, [rdi + rsi] ret To answer the original question, it won't optimize the smart pointers away because the smart pointers are not implemented by the compiler. They are a generic construct telling the compiler to allocate memory, increase reference counter, decrease reference counter and free memory. How does it now this is not a unit test for memory allocation API or atomic operations? A better question would be why C++ went for implementing certain things on the library level instead of the compiler level, and the answer here is probably "historical reasons" and someone's judgement call on flexibility vs performance under specific circumstances.
- gumby 4y agoIt’s not “C++’s” decision, it’s the compiler writer’s. Compilers are full of special handling of standard functions (memcpy is a common case) — those cases are called “compiler intrinsics”. They can also choose to call a function when you don’t expect it (e.g. when your hardware doesn’t have floating point support). Does rust try to follow the platform C ABI? C++ implementations typically do when it is semantically meaningful.
- steveklabnik 4y agoRust doesn’t follow the platform C ABI unless you ask for it specifically. If you do, it will, and if you don’t, the ABI is unspecified for a reason.
- gumby 4y agoI wish c++ could get away with this. But the two languages (c++ and rust) explore slightly different parts of the design space.
- bjourne 4y agoSTL lets you supply a custom allocator for all STL types. Afaik, Rust doesn't and this is probably the price you have to pay for that feature.
- steveklabnik 4y agoBox is parameterized by an allocator: https://doc.rust-lang.org/stable/std/boxed/struct.Box.html https://doc.rust-lang.org/stable/std/boxed/struct.Box.html Rust doesn’t let you utilize this fact on stable yet, which is what you’re thinking of, but the implementation in this case already exists.
- vasama 4y agoIf by "STL" you mean the standard library, it's not true that _all_ containers accept allocators. At least valarray and filesystem::path do not accept allocators.
- UkrainianJew 4y agoAlso, C++ allows you to be explicit about things. Like std::unique_ptr<> that does not have to count references and guarantees at compile time that: * There will be not more than 1 reference to the object at any time. * The object will be deleted once the reference dies. That's a zero-overhead GC that is enforced at compile time. And that is the main appeal of C++. You can get close-to-optimal instruction-level behavior while still describing things at reasonably high levels of abstraction. Yes, it has a hell of a learning curve, but once you master it, you can beat any other language out there.
- vvanders 4y agounique_ptr doesn't have any aliasing guarantees, unique_ptr::get() will happily hand out references to the interior pointer. It does give you non-ambiguous ownership semantics which is a good thing but there's still plenty of ways to misuse/abuse it. That said if you can make the aliasing guarantees(such as Rust's borrow checker does) that opens up the possibility of automatically applying restrict semantics[1] which enables further set of optimizations available to the compiler. [1] https://en.cppreference.com/w/c/language/restrict https://en.cppreference.com/w/c/language/restrict
- zamalek 4y agoThere's a lot of UB in that spec. Are compilers capable of warning/erroring UB in this case?
- maleldil 4y agoHow is std::unique_ptr different from Rust's Box? Everything you said applies to Rust. Also, I definitely would not call std::unique_ptr (or Box) a GC.
- steveklabnik 4y agoThey’re the same at a high level, but when you get into the details, there are different. A famous one is that uniq_ptr can’t be passed in registers (mentioned upthread) for ABI reasons, but Box doesn’t have that restriction. Another one is what happens when you move a value out, in C++ it’s in an “indeterminate state”, but in Rust you can’t access a value after move. Which means that you can get a null pointer exception in C++, but not in Rust, and means that destructors for uniq_ptr have to check for this case, whereas in Rust they don’t.
- millimeterman 4y agoWhile we're doing code review, in general there's no reason to pass a shared_ptr, even by const ref, unless you need to (e.g. if you might copy the shared_ptr within the function). It's better to pass a raw pointer to indicate that the function only requires a temporary non-owning view into the data. static int add_numbers(Data *p) { return p->a + p->b; }
- Kranar 4y agoIt's best to pass a reference to specify that the argument shall not be null. Also avoid writing static functions in C++, make use of anonymous namespaces. namespace { int add_numbers(const Data& p) { return p.a + p.b; } }
- millimeterman 4y agoAgreed, though at some point we run up against the inherent triviality of the example :P.
- vasama 4y agoThere's no reason to avoid static functions. It's just a better syntax and it's a shame it cannot be applied to class declarations as well.
- strager 4y ago> That's because you are explicitly telling the compiler to create a copy of your shared pointer This is also happening in the Rust code (x.clone()).