5 ms·
You can also achieve the fix using the arguably more natural template<typename T> T const& clamp(T const& v, T const& lo, T const& hi) { T cons
by pbsd 5y ago
You can also achieve the fix using the arguably more natural
template<typename T>
T const& clamp(T const& v, T const& lo, T const& hi) {
T const& a = v < lo ? lo : v;
T const& b = a > hi ? hi : a;
return b;
}
Either way, the commitee's idea of returning references for the C++ max/min/clamp functions was terrible, and it's a constant source of problems with temporaries and such. Like this: https://godbolt.org/z/j593Mebd6 https://godbolt.org/z/j593Mebd6
- secondcoming 5y agoBut it's not unreasonable for a programmer to expect no temporaries in code that just uses references. The rules are too complex, some of this is down to the language itself and some is the fault of compiler writers. I find myself using Compiler Explorer more and more for short code snippets to (obviously) see what the compiler is doing because I don't really trust my intuition that much any more. And the annoying thing is that clang and gcc can differ wildly in their codegen. I don't think anyone can call themselves a C++, or possibly Rust, programmer unless they can also understand assembly code. Just getting code to compile isn't enough. It seems like a step backwards to me.
- josephg 5y ago> I don't think anyone can call themselves a C++, or possibly Rust, programmer unless they can also understand assembly code. Slow down on the gate keeping there. It really depends what you’re trying to achieve. I’ve spent the last few days optimising a btree implementation in rust. The code runs fast, but the performance varies +/- 15% or so seemingly arbitrarily based on how I structure my code. I could hop into compiler explorer and figure out what the compiler is actually doing. But the code is long and complex, and my mostly-allocation free approach is already about 80x faster than the javascript implementation this code is trying to replace. Optimising it further is not worth my time. The fact that I don’t have the skills or the time to squeeze a few more % out of my algorithm doesn’t make me a bad rust programmer. It makes me good at planning - I’m optimising my time. There will always be a line in the sand where further optimizations based on even deeper understanding will stop being worth your effort. Where that point is depends on the project. Is it worth running benchmarks? Doing profile based hotspot optimization? Is it worth looking at the compiler’s assembly output? Are you going to hardcode asm routines? How about per-cpu variants of core algorithms for popular CPUs? No. We all stop somewhere, depending on the needs of the project at hand, depending on how our time is best spent. That is the only reasonable answer if you want to ever finish projects. You don’t get to tell me I’m not a real rust programmer because I’m not microoptimizing this project as much as you would. That’s silly.
- secondcoming 5y agoActually, I don't think it's silly at all. Beating JS isn't hard to achieve, it doesn't tell you much about the quality of your code. Me beating a toddler in a 100m sprint doesn't make me an athlete. Benchmark against a C or C++ implementation. You'll be examining things on Compiler Explorer soon enough.
- josephg 5y agoBeating JS by 5x is easy. Beating a good JS implementation by 80x is much harder. > Benchmark against a C or C++ implementation Alas no; I'm not going to do that. First because there aren't any implementations in C/C++ for what I'm doing. But also, because as much fun as it is to wring every last cycle out of my CPU, I've met myself. Spending every moment trying to make my code go faster is a trap. If I spend too much time optimizing for performance, I'll never finish what I'm writing. I've done it before. Performance is fun because its measurable. You get a score, and that feels great. But beyond some point, documentation, testing and features all become much more important. I could write the fastest library in the world but if its buggy, missing features or has no documentation, nobody will use it. Worse, I won't be able to build things on top of it because I'll be too distracted trying to make my benchmark numbers go up. Performance is fun, but its far from the only measure of software quality.
- eek04_ 5y agoI spent a decade or so of my life writing assembly, and a few years as a performance specialist for a programming team. What you're describing is a specialist skill. There's nothing wrong with it - it's a good skill to have - but there is no need to have everybody in the team have it, and you can expect better results by having some people develop that skill and other people develop other skills.
- sesuximo 5y agoWhat would you have them use instead of constant references? If you take values and T is bigint, then clamp becomes unusable. Even if it’s 128 bit ints... that’s worse. I think const ref makes perfect sense. It has the logic you want but no copying etc. When clamp is inlined, the compiler should be able to deal with const refs as easily as values for small Ts. (And if it’s not, let’s fix the compilers... not stdlib)
- pbsd 5y agoHaving everything be const references is in a sense optimal, but is so prone to misuse that I wouldn't want it in the standard library. One option is to pass everything by value. Move semantics does the rest. Like: template<typename T> T clamp(T v, T lo, T hi) { if(v < lo) v = std::move(lo); if(v > hi) v = std::move(hi); return std::move(v); } For fundamental types, this is the same or better than before (including __int128_t and such). For bigint types with heap allocation, i.e., GMP wrappers and such, moving is fairly inexpensive (but for constant lo, hi, they need to be created/copied each time...). For fixed-width bigint types, moving is no different from copying and the cost is higher. In any case, the 99-percentile usage of these functions is with fundamental types, where pass-by-value is clearly the better choice. Alternatively, you could have template<typename T> T& clamp(T& v, T const& lo, T const& hi) { if(v < lo) v = lo; if(v > hi) v = hi; return v; } This forces the returned reference to be an l-value, and so returning dangling temporaries becomes more difficult. There's the added cost for the assignments, though.
- sesuximo 5y agoMoving into clamp is problematic since you’d make clamp destructive. You couldn’t use any of the arguments after clamp. Non const refs seems like a weird idea (what if I got a const ref from my callee? Why can’t I clamp?).
- pbsd 5y agoTrue, but it's entirely voluntary: - clamp(a, b, c), where a, b, c are all l-values would copy everything. Not destructive by default (but wasteful on "big" types). - clamp(a, std::move(b), c) would potentially destroy b, but that's something the caller explicitly opted into. - clamp(a, T{ something }, T{ something else }) would be an implicit move, but nobody is going to accidentally use those r-values anywhere else. The non-const ref version returning is not something I quite like either; it would make more sense to me to have `void clamp(T& x, T const& l, T const& h)`, where the semantics would be more like `x.clamp(l, h);`. But to retain the same API I returned the reference.