Examples of Parallel Algorithms from C++17
bfilipek.com
bfilipek.com
Further, I suspect that the extremely regular pattern of the space boundaries is probably giving the branch predictor a leg up. It almost certainly would pick up the 'every 5' pattern if not the 'every 17' pattern. So a realistic example would be far more branch miss heavy.
This is still fine work, as the effort of writing a nice C++ function and getting some parallel help on it is tiny compared to writing SIMD code, and some problems don't play nice with SIMD at all.
Skimming the trip reports just now, it sounds like some Google engineers tried to use them, didn't like them, wrote a whole new proposal, and threw the schedule into disarray. Sigh. I'm sure they had good reasons to do this, but c'mon guys... at some point you have to decide that done is better than perfect.
The TS heap-allocates all coroutine frames by default and hopes that can be optimized out; the Google proposal treats the coroutine frame as an anonymously-typed object much like a lambda and leaves allocation (or not) up to the surrounding library.
The TS has a huge pile of extension points to control behavior; the Google proposal provides the same functionality via a single operator overload (which plays a role similar to the argument passed to call-with-current-continuation).
The TS has co_yield, co_return, and co_await; the Google proposal has a single operator that takes the place of co_await, while co_yield and co_await are made unnecessary by its interface.
The Google proposal does have some things that need to be solved, but (again IMO) it's a much more promising direction than the TS.
I remember watching Gor Nishanov's CppCon talk from 2015(!), learning a lot, seeing a working implementation, and getting really fired up about this feature. Now it's 2018 and it's back to the drawing board? What happened? Was Google the smart kid in high school who skips class and waits until the last second to do their homework? I know the committee has only the best intentions, but with stuff like this they're their own worst enemy.
I believe that even at the cost of speed of progress, getting this kind of impactful feature right is worth the trouble, especially since therr are already other possibilities for using coroutines in C right now, although they're not nicely fitted into the language.
So average Joe Users like me have to wait and wait while it's made better and better. If you'd put this decision to a vote of 10,000 MSVC users instead of a bunch of compiler vendors and Google engineers, I bet coroutines would've been merged, warts and all. Instead, the years pass, attention drifts, modern languages beckon, and C++ becomes even more niche.
The history of standards is littered with such attempts, w/ and wo/ a dictator, and it never ends well. Reasons include that bad ideas/implementations become very hard to eradicate (they will have been used in legacy code); some coding standards mandate use of standard functionality even if a better alternative exists (for good reasons such as portability, but still) and they increase the activation energy to do something well.
Another value of a standards organization is a drive for solid (not necessarily fanatic) orthogonality and comparability.
C++ has suffered from some legacy mistakes (e.g. << io operations) and hasty mistakes (std::auto_ptr) and appears pretty reluctant not to get it wrong again.
Better to let some ideas get worked out in detail with some use experience before sticking them in, especially for features that can be implemented in a library.
It works beautifully (of course, putting the fact that it's slightly "magical" aside). It's much more comfortable to write Javascript-style programs in C than it is in Javascript (where you need to chain callbacks) this way :-)
But are they an abstraction that will be useful in the future? Worth it for the language?
Normally it's good to have pleasant abstractions and generic functions. But execution policies are there for speed and speed alone: if they don't make things faster, they've failed. So in this case it's worth tolerating a more awkward API to achieve better performance.
The C++17 parallelism interface is more or less to declare "this loop can be parallelized," which does not provide enough information to the scheduler. You need to express properties of the algorithm, such as its optimal chunk size. (In the future perhaps those properties will enter into the execution policy as e.g. constructor parameters.)
Second, passing in callbacks which receive one or two elements at a time is too granular. You are at the mercy of the compiler to optimize these. A better approach would be for the callbacks to receive a pair of iterators and allow the user to drive the loop. This makes it easier to avoid accidental indirections, failed alias analysis or inlining, etc.
Intel's TBB will continue to be more useful here.
If you are going to have a lot of temporary vectors as part of a larger algorithm, it is usually beneficial to copy the inputs once, do all the computations on the gpu, and copy them back.
That’s a lot more work than this, though.
Here's the public API of mine[1], which is pretty blah, because it takes a closure (per-thread initialization) that must return a closure (executed on each entry in the tree). The most interesting part of the implementation is here[2], along with a suspicious termination argument.
Outside of that, ucg (written in C++) also has a recursive directory iterator.[3]
I also know of two written in Go, one in sift[4] and another in the platinum searcher[5]. The ones in Go seem considerably simpler as they fall back to goroutines.
I haven't carefully benchmarked all of these, but if memory serves, they're all faster than the single threaded variants. In my experience, the hardest part about implementing this is coming up with a solid termination argument because your consumers are also your producers. (I documented this a bit in `get_work` in my Rust code.)
[1] - https://docs.rs/ignore/0.4.2/ignore/struct.WalkParallel.html
[2] - https://github.com/BurntSushi/ripgrep/blob/b38b101c77003fb94...
[3] - https://github.com/gvansickle/ucg/blob/master/src/libext/Dir...
[4] - https://github.com/svent/sift/blob/2ca94717ef0bbf43068f217c8...
[5] - https://github.com/monochromegane/the_platinum_searcher/blob...
I was more curious if there was a cleaner C++ way of doing it with all of the recent or pending languages changes.
There will hardly be any project that needs to know the "recursive file size" that is also too small to be able to afford this extra work.
Ok, but maybe I'm missing something here: Why parallel? Is there any benefit?
... in addition to the tasks behaving like streams.
The language describing each policy is awful, but okay.
"so concise code" for 10 lines consisting mostly of ceremony to get the results of one std function into a vector?
Does the first example with std::transform::reduce really need `std::uintmax_t{ 0 }` twice instead of `0`?
2x-3x speed up for specifying a parallel execution policy on reasonably large examples seems deeply disappointing. Author didn't specify how many cores the examples were run on, but ouch.
Best of all is the volume of comments here predicting that these annotations will eventually be deprecated ..
I really want to like C++.
Also what is exactly the difference between parallel_execution_policy and parallel_vector_execution_policy ?
template<class T>
void foo(T &&);That’s incorrect. Addition of int isn’t associative in C++, either. For example
INT_MAX + (1 + -1) = INT_MAX
but (INT_MAX + 1) + -1
is undefined.