But I was helping the compiler
pankajraghav.com
pankajraghav.com
- For std::array of uint8_t – an in-place container of a plain-old-data type – the move constructor and the copy constructor, if not optimized away, will do the exact same thing! Both boil down to copying the bytes from the old object to the new object. So there is never any benefit to calling std::move on such an object. This is different from out-of-line containers like std::vector, where the std::vector object itself consists only of a pointer and some lengths. In that case, the move constructor just copies the pointer and length values themselves and nulls them out in the original object, without copying the underlying data. But the copy constructor has to allocate a whole new heap buffer, copy the underlying data, and initialize the new vector object with that pointer.
- The godbolt links are building without optimization enabled (without -O settings)! GCC apparently still performs (non-mandatory) NRVO in this case, probably because that optimization is implemented on the frontend rather than the backend, and the frontend tends to care less about -O settings. But GCC does not perform the usual suite of backend optimizations, including inlining and memcpy elimination. If optimizations were enabled, GCC might be able to optimize both versions into the same thing, depending on what actually happens in “// Do some computation”. It doesn’t make sense to make performance comparisons with optimization disabled, unless you’re trying to improve the performance of debug builds themselves.
The purpose of move is to "steal" the underlying memory, but if you do that, you leave the gfx variable from the class without backing. So basically you would need to reallocate it after every call to get_display_pixels. So the code only works because the author uses array, which is statically allocated and there is nothing to steal. It would break were they to switch to vector.
Either the author doesn't understand what the purpose of move is, or if this is really their intent, it's a terrible design, since get_display_pixels looks like an idempotent getter function, but it's not.
In the example in the article, the compiler may be able to perform all kinds of optimizations for example because the result is assigned to a const lvalue, and depending on the rest of the code in that function (which is not shown) it may be able to infer the assigned variable can never be aliased, so that’s one possible reason why the disassembly may be surprising. The C++ specification only prescribes how code should behave but it explicitly does not put restrictions how the compiler achieves that effect, to the point whole functions might be completely absent in the produced binary and replaced by a constant if it's possible to calculate the result of every invocation at compile time.
No, because get_display_pixels returns by value and not by reference. So the move constructor will move from a newly constructed value, not the instance gfx variable.
If get_display_pixels returned by mutable reference and the author used std::vector, yes, the code might break.
Nit: array is not statically allocated; it's stack/automatically allocated.
I strictly use std::move in case of explicit transfer of ownership, and not to try to outsmart the compiler to ‘optimize’ something. As the article shows the compiler is smart enough to do all this by itself, it doesn’t need to be told to try to elide copies. But maybe I’m missing some valid use case where it would make sense to add the std::move in these kinds of situations?
Btw, that's what I meant by statically allocated, I wasn't sure what's the proper terminology.
I also checked move and copy assignment operators – is it UB to move-assign an object to itself? Based on this very confusing Stack Overflow thread [1], I think the answer is: no, not at a language level, although it might be bad to do it with standard library types. If that's correct, a compiler-generated default move assignment operator would have to assume aliasing is possible and use memmove, same as with copy assignments.
[1] https://stackoverflow.com/questions/13127455/what-does-the-s...
I don't think aliasing the this pointer matters, since it's the pointed-to memory that could be aliased.
But I'm not sure if that's actually faster or if any compiler utilizes the semantic difference.
2) If you care that much about performance, you can avoid all copies by returning by reference in this case.
3) The author seems to misunderstand std::move — in this case, std::move can only pessimize performance and never optimize because the move constructor of std::array is the exact same as the copy constructor.
For example, move constructors help performance for std::vector because the move constructor will copy the backing buffer's pointer instead of copying the entire backing buffer itself.
Since std::array<T> is a thin wrapper around T[], the entire array is stored on the stack, and there is no possible optimization that a move could make — stack values must be copied (or otherwise initialized) in move constructors. In fact, since std::array is an aggregate type, there isn't even a user-defined move constructor (or copy constructor)! https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-...
Just curious as I've wanted to get better at being able to understanding assembly output by gcc/clang but don't think I've come across this advice explicitly before.
Unoptimized assembly IS useful if you just want to learn or understand assembly itself.
The straightforward naive code is also the best code, here. So it's not an example of what you describe. In fact at least on stl containers, and your own classes with such members, move semantics "just works". Rebuild the same c++98 code with c++11 and it just skips needless copies as part of the language, not implementation.
And it's not about second-guessing. The confusion here is not about the compiler implementation. It's about the language.
Many times straightforward code is the best code. Value returns in C++ are already optimised, no need to add anything fancy.
Edit: I just took a look at the actual godbolt links. And it looks like the std::move version actually puts a call to std::move in the generated assembly. I’m kind of surprised this is actually a literal function call, I would have assumed std::move would be always inlined.
The godbolt links are without any optimization.
In the version with std::move, it first copy-elides the function call into a temporary, then move-constructs the disp_pixels object from the temporary. ie as if
auto tmp = emulator.get_display_pixels();
const auto disp_pixels = std::move(tmp); // move constructor called here
// lifetime of tmp ends
So the version with std::move is invoking an extra move-constructor and destructor call.That being said, I've definitely heard of std::move in the wrong places preventing optimizations (e.g., std::move on a return statement can (will?) inhibit copy elison), but I'm not familiar enough with the finer details of C++ be able to tell you why.
std::move doesn't move. It should maybe have been called std::lvalue_to_rvalue.
FWIW, this is also one of Rob Pike's rules of programming:
> Rule 1. You can't tell where a program is going to spend its time. Bottlenecks occur in surprising places, so don't try to second guess and put in a speed hack until you've proven that's where the bottleneck is.
It's implicit, but the "proven" part implies use of a profiler.
Also, bottlenecks happen in surprising places, and allowing slow/bad code just because the profiler doesn't scream about it on the development machine is a recipe for surprising performance issues in the future. The slow code might become a problem when a user has slightly different task for the program.
But I think the aphorism covers the case you’re describing too.
It’s possible that your loop is slow, but if you’re working on a program of sufficient size and complexity, you simply won’t know if it’s the bottleneck without using a profiler.
The purpose of the statement is to save you the trouble of 10xing performance of a function, so that it takes .1% of all execution time, instead of 1%. Do sufficiently complex programs, and without a profiler, you won’t really know if a loop is taking 1%, 10%, or 80%.
Types are a really good example of where you want to help the compiler. Type signatures let you define how the type can be used. By adding as much information about how the type should be constrained, you can help the compiler prevent you or someone else from doing something stupid and on occasion, you can earn some optimisations in the process.
And there are also the 'thousand cuts' issues for which profilers are "useless"..