Parallel performance improvement - #2159
Conversation
|
Unfortunately I don't think we have a |
No worries! I was thinking of just doing it as part of this PR actually. @manufacturist are you comfortable working with JMH? Would you like to give it a try? Otherwise I (or Vasil) can get to it and push here. |
|
Feel free to ask and to look into the |
|
Missed the questions! Answers incoming…
The problem here is a little more existential. If it was just a bit of type juggling I would be a lot more worried about it! Here's the issue: F.uncancelable(_ => F.both(F.canceled.as(42), F.unit))What should this produce? The problem here is the type signature of
Precisely as you said. :-) Also I'm realizing that there's actually a leak here: it's possible that the parent fiber is canceled prior to the first
I believe you're correct! I'm pretty sure we don't need |
I'll give it a try 😄 |
|
@manufacturist If it helps, I literally always copy/paste from something pre-existing for JMH. I recommend just copying something like the |
|
Maintainers caught admitting copy/pasting code all over the place. |
My favorite Sublime key combo is Cmd-Shift-D: it duplicates the current line. :-P |
|
Updated the Executing MachineAll benchmarks executed with variations of CPU Time Tokens = 200CPU Time Tokens = 2000 -
|
|
|
||
| override def mapBoth[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => F[C]): F[C] = { | ||
| implicit val F: GenConcurrent[F, E] = this | ||
|
|
||
| uncancelable { poll => | ||
| for { | ||
| fiberA <- start(fa) | ||
| fiberB <- start(fb) | ||
|
|
||
| outcomeA <- poll(fiberA.join).onCancel(fiberB.cancel) | ||
| outcomeB <- poll(fiberB.join) | ||
|
|
||
| resultC <- (outcomeA, outcomeB) match { | ||
| case (Outcome.Succeeded(fa), Outcome.Succeeded(fb)) => | ||
| fa.flatMap(a => fb.flatMap(b => f(a, b))) | ||
|
|
||
| case (Outcome.Errored(e), _) => raiseError(e) | ||
| case (_, Outcome.Errored(e)) => raiseError(e) | ||
| case (Outcome.Canceled(), _) => F.canceled *> never | ||
| case (_, Outcome.Canceled()) => F.canceled *> never | ||
| } | ||
| } yield resultC | ||
| } | ||
| } |
There was a problem hiding this comment.
Now that there are no GenConcurrent constructs used, we should try to move this code in GenSpawn. This was an observation by @TimWSpence, thank you!
There was a problem hiding this comment.
I feel this block of code needs a good review from more pairs of eyes.
There was a problem hiding this comment.
That being said, if the logic does end up changing, this code may indeed need to live in GenConcurrent.
|
@manufacturist Are the benchmark results you posted only for the current branch which includes the changes in this PR? |
They are. The first batch ( |
|
I would like to split the benchmark code out of this PR, open it as a separate PR and then when we merge it, rebase this PR and do a proper comparison for every case between |
|
@manufacturist The original issue has been changed in light of new information. I suggest that we split out the benchmarks into a separate PR and push that through, and then we can work together on tackling this problem again and be a bit more focused this time round. |
|
I think it's okay to keep updating this PR in light of the revised requirements. It's good history. :-) Agree that splitting the benchmarks out into a separate PR (#2167) is a good move. |
79838aa to
c599b30
Compare
|
Can you drop the commit that adds the |
Will do so in ~12 hours. |
|
@manufacturist New plan! Let's skip the @vasilmkd gets all the credit for this realization btw. |
|
Plan of attack that @manufacturist can follow tomorrow:
|
c599b30 to
006a0e4
Compare
|
Will rerun the benchmarks later today to be able to compare them with the ones from #2167. |
|
Finished running the benchmarks. They can be compared with the ones from the other PR: [info] Benchmark (cpuTokens) (size) Mode Cnt Score Error Units
[info] ParallelBenchmark.parTraverse 100 100 thrpt 20 9944.533 ± 211.074 ops/s
[info] ParallelBenchmark.parTraverse 100 1000 thrpt 20 1428.851 ± 9.781 ops/s
[info] ParallelBenchmark.parTraverse 100 10000 thrpt 20 139.946 ± 1.383 ops/s
[info] ParallelBenchmark.parTraverse 1000 100 thrpt 20 8184.030 ± 39.369 ops/s
[info] ParallelBenchmark.parTraverse 1000 1000 thrpt 20 1008.832 ± 2.568 ops/s
[info] ParallelBenchmark.parTraverse 1000 10000 thrpt 20 98.693 ± 0.729 ops/s
[info] ParallelBenchmark.parTraverse 10000 100 thrpt 20 2526.056 ± 29.223 ops/s
[info] ParallelBenchmark.parTraverse 10000 1000 thrpt 20 257.168 ± 2.535 ops/s
[info] ParallelBenchmark.parTraverse 10000 10000 thrpt 20 25.164 ± 0.491 ops/s
[info] ParallelBenchmark.parTraverse 100000 100 thrpt 20 305.078 ± 1.061 ops/s
[info] ParallelBenchmark.parTraverse 100000 1000 thrpt 20 30.497 ± 0.589 ops/s
[info] ParallelBenchmark.parTraverse 100000 10000 thrpt 20 3.006 ± 0.084 ops/s
[info] ParallelBenchmark.parTraverse 1000000 100 thrpt 20 31.570 ± 0.133 ops/s
[info] ParallelBenchmark.parTraverse 1000000 1000 thrpt 20 3.164 ± 0.017 ops/s
[info] ParallelBenchmark.parTraverse 1000000 10000 thrpt 20 0.330 ± 0.034 ops/s
[info] ParallelBenchmark.traverse 100 100 thrpt 20 61728.601 ± 694.063 ops/s
[info] ParallelBenchmark.traverse 100 1000 thrpt 20 7589.424 ± 84.756 ops/s
[info] ParallelBenchmark.traverse 100 10000 thrpt 20 873.365 ± 2.606 ops/s
[info] ParallelBenchmark.traverse 1000 100 thrpt 20 15077.902 ± 89.639 ops/s
[info] ParallelBenchmark.traverse 1000 1000 thrpt 20 1622.568 ± 10.988 ops/s
[info] ParallelBenchmark.traverse 1000 10000 thrpt 20 165.695 ± 1.015 ops/s
[info] ParallelBenchmark.traverse 10000 100 thrpt 20 1770.700 ± 0.867 ops/s
[info] ParallelBenchmark.traverse 10000 1000 thrpt 20 177.990 ± 0.240 ops/s
[info] ParallelBenchmark.traverse 10000 10000 thrpt 20 17.709 ± 0.125 ops/s
[info] ParallelBenchmark.traverse 100000 100 thrpt 20 179.605 ± 0.153 ops/s
[info] ParallelBenchmark.traverse 100000 1000 thrpt 20 17.886 ± 0.097 ops/s
[info] ParallelBenchmark.traverse 100000 10000 thrpt 20 1.737 ± 0.067 ops/s
[info] ParallelBenchmark.traverse 1000000 100 thrpt 20 17.896 ± 0.069 ops/s
[info] ParallelBenchmark.traverse 1000000 1000 thrpt 20 1.729 ± 0.075 ops/s
[info] ParallelBenchmark.traverse 1000000 10000 thrpt 20 0.176 ± 0.008 ops/sThe For my machine, the performance gain for |
|
Here are the runs from a 16 core cloud instance:
I think this is a spectacular improvement and well worth the code duplication. Amazing work @manufacturist! You should be proud. |
|
I will do another pass over the code and give my final approval later. |
vasilmkd
left a comment
There was a problem hiding this comment.
@djspiewak Please take a close look when reviewing the changes. I'd love an explanation on a couple of confusion points I encountered in the review. Thanks.
| fiberA <- F.start(ParallelF.value(fa)) | ||
| fiberB <- F.start(ParallelF.value(fb)) | ||
|
|
||
| a <- F.onCancel(poll(fiberA.join), fiberB.cancel).flatMap[A] { |
There was a problem hiding this comment.
Here's a scenario, we get canceled while trying to join the fiberA. However, this does not cancel fiberA.
Is this a valid concern or am I missing something?
There was a problem hiding this comment.
This is a valid concern! We need to cancel both of the fibers if either join is canceled. And in fact, we need to ensure that this cancelation is itself run in parallel, so the correct finalizer here would be F.onCancel(poll(fiberA.join), F.both(fiberB.cancel, fiberA.cancel)).
| a <- F.onCancel(poll(fiberA.join), fiberB.cancel).flatMap[A] { | ||
| case Outcome.Succeeded(fa) => fa | ||
| case Outcome.Errored(e) => fiberB.cancel *> F.raiseError(e) | ||
| case Outcome.Canceled() => fiberB.cancel *> F.never |
There was a problem hiding this comment.
I don't quite understand this interaction. I fear this will enter F.never here.
Should this line be: fiberB.cancel *> F.canceled *> F.never instead? I also don't quite understand how this all interacts with the outer uncancelable.
There was a problem hiding this comment.
This should indeed be poll(fiberB.cancel *> F.canceled *> F.never) (and analogously in the b case)
vasilmkd
left a comment
There was a problem hiding this comment.
Very impactful change @manufacturist! Amazing contribution. Thank you for your patience while we figured things out along the way. It's usually how great PRs come together. 😄
| @@ -53,7 +53,53 @@ trait GenSpawnInstances { | |||
| final override def map2[A, B, Z](fa: ParallelF[F, A], fb: ParallelF[F, B])( | |||
There was a problem hiding this comment.
this code is exactly the same as below, except for the Eval wrapping, no?
Why not have one private method that returns Now[ParallelF[F, Z]] and in the map2 case pass fa, Now(fb)? I think the extra now wrap unwrap is likely trivial.
There was a problem hiding this comment.
I think the extra now wrap unwrap is likely trivial.
The unwrap is pretty trivial but the allocation is… less so. Particularly since you pay that penalty on a per-item basis.
First PR. Be gentle 🙏 🎉 PR covering #2155:
mapBothmap2 in terms of mapBothGenConcurrent.mapBoth overridemap2&map2Evaloverrides (withmapBothsolution)unit tests(No longer needed due to overridingmap2&map2Eval)In abstract, I need to ensure that two effects run in parallel, obtain their results and map over them.
Qs:
cancelled *> nevertrick ensures thatneverwill never run, therefore it is only used throughout the cats-effect code to help with matching the type signature where needed. Is this correct?outcome <- poll(workFiber.join), is there any need for callingonCancelorguarantee? Is there the possibility of having the main fiber cancelled while theworkFiberhasn't joined yet? And if yes, then the mentioned constructs guards us against such scenarios?Deferred? Why isn't fibers enough, e.g.