3 ms·
Every time I see "use ranges and algorithms!" examples, I am baffled that apparently, I am supposed to find inline double algorithm_call(std::span<double c
by Joker_vD 4mo ago
Every time I see "use ranges and algorithms!" examples, I am baffled that apparently, I am supposed to find
inline double algorithm_call(std::span<double const> xs) noexcept {
return std::accumulate(
xs.begin(),
xs.end(),
0.0,
[](double acc, double volts) {
auto mv = calibrated_mv(volts);
auto err = residual(mv);
return weighted_square(err) + acc;
});
}
more readable, concise, and easier on my eyes than
inline double raw_loop(std::span<double const> xs) noexcept {
double sum = 0.0;
for (double volts : xs) {
auto mv = calibrated_mv(volts);
auto err = residual(mv);
sum += weighted_square(err);
}
return sum;
}
Sure, there are some algorithms in <algorithms> that I'm rather not reimplement myself, but this one is not it.
- rzzzt 4mo agoThe first form is easier to send to 32 beefy cores or 1024 small CPUs or a Beowulf cluster or a GPU or people sitting in a room.
- xyzzyz 4mo agoBoth of them have to be completely rewritten to make use of multiprocessing, so what exactly is the advantage?
- rzzzt 4mo agoThe first one too? Isn't that the map-reduce fork-join golden example of multiprocessing?
- cwzwarich 4mo ago`std::accumulate` is defined to have sequential semantics, so the analysis required to make it parallel is probably not that different than starting from the loop version. I guess you could have an alternate `accumulate_associative` that uses the same interface but assumes the reduction is associative and has unspecified evaluation order?
- mpyne 4mo agoC++ has std::reduce for that, which is std::accumulate except it's defined to operate without any specific ordering.
- rzzzt 4mo agoThanks everyone, my C++ knowledge has been greatly expanded today.
- Joker_vD 4mo agoAnd now you should probably also stop and consider whether adding elements one-by-one as opposed to recursively adding together sums of smaller subarrays has better or worse numerical behaviour in regards to e.g. rounding and stability.
- tcfhgj 4mo ago1) afaik accumulate cannot be parallelized 2) the map part is included in the accumulate lambda, so the map part cannot be parallelized either -> you'd have to split it out into a transform step (iirc)
- CITIZENDOT 4mo agostd::accumulate is sequential and guarantes in order traversal. std::reduce is parallel version of it
- mpyne 4mo agoThe original example isn't really using ranges except to emulate C++98 iterator work though. The actual equivalent might be something closer to: inline double algorithm_call(std::span<double const> xs) noexcept { return std::accumulate( xs, 0.0, [](double acc, double volts) { auto mv = calibrated_mv(volts); auto err = residual(mv); return weighted_square(err) + acc; }); } (that is, without the boilerplate .begin and .end). Even that is enough to make ranges useful in my mind, but in a codebase which has started to integrate some functional programming techniques, there are also applications for things like views and transforms. This can make it easier to reason about iteration pipelines in ways you might already be familiar with from POSIX. That all said, it's C++ so sometimes the error messages get a lot more 'interesting' than they would have with STL-style iterators, especially when mixed with constexpr expressions as you might do with std::format or fmt libs.
- Joker_vD 4mo agoIt's been 15 years since I've last touched OpenMP, but the second form is trivially parallelizable as well. Besides, this parallelization can only ever properly work with arrays/vectors or, at the very worst, std::deque as its usually implemented (a vector of fixed-length arrays), not with e.g. linked lists or red-black trees, so why even bother with generic spans and algorithms?
- never_inline 4mo agoFor compilation?
- fooker 4mo agoGreat, now use some functions. From the library or your own, and see this complexity become manageable. That's what abstraction is about.
- Erlangen 4mo agoYou said "ranges and algorithms", but you didn't copy the third function which actually uses <range> library. inline double ranges_pipeline(std::span<double const> xs) noexcept { auto costs = xs | std::views::transform(calibrated_mv) | std::views::transform(residual) | std::views::transform(weighted_square); return std::ranges::fold_left(costs, 0.0, std::plus<double>{}); } It's still a bit verbose, because C++ doesn't allow universal function call syntax. It will be even more concise in other languages like D.
- Joker_vD 4mo agoThat version was so much more opaque that I didn't bother copying that. Again, I'm not entirely sure why people are so enamored with splitting iteration itself from the contents of one iteration step, especially since the loops are language built-ins.