8000 Optimize Promise by hearnadam · Pull Request #9569 · zio/zio · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Optimize Promise #9569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
May 15, 2025
Merged

Optimize Promise #9569

merged 40 commits into from
May 15, 2025

Conversation

hearnadam
Copy link
Collaborator
@hearnadam hearnadam commented Feb 10, 2025
  • Merge UnsafeAPI with AtomicRef
  • State: cache Empty and use custom linked list to minimize allocation footprint
  • Avoid ZIO.asyncInterrupt - it introduces a minimum of 3 suspensions + Atomic allocation
  • Never interrupt waiters. If the callback is invoked after interruption, the fiber won't be resumed
  • Merge ZIO.asyncInterrupt AtomicRef and function allocation
  • Use Exit in make to avoid suspension (we should later add makeWith)
  • Complete waiters in order

Marking this as a draft as I want feedback on: 1) never removing waiters, 2) state design

The benchmarks are extremely noisy as the ZScheduler constantly waking Threads dominates. I think I've removed most of the hotspots, but I will make some changes to my measurements to make sure.

@@ -39,9 +39,12 @@ object BenchmarkUtil extends Runtime[Any] { self =>
Unsafe.unsafe(implicit unsafe => rt.unsafe.run(zio).getOrThrowFiberFailure())
}

override val unsafe = super.unsafe
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed some benchmarks allocate new unsafe apis.

8000
@hearnadam
Copy link
Collaborator Author

Will fix the CI tomorrow.

@hearnadam hearnadam requested a review from guizmaii March 13, 2025 22:48
@hearnadam hearnadam requested a review from ghostdogpr March 24, 2025 15:29
hearnadam and others added 4 commits March 30, 2025 16:25
ZIO.asyncInterrupt[Any, E, A](
case Done(value) => value
case pending =>
ZIO.async[Any, E, A]( // intentionally never remove the callback, interrupted fibers won't be resumed
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kyri and I concluded that this will likely end up retaining references to Fibers, which is problematic in a highly awaited Promise, where Fibers are interrupted.

Unfortunately, we will need to use asyncInterrupt here, though it's heavier weight. We can keep the Link structure as the removals should be rare, and it's optimized for the single waiter case.

retry = !state.compareAndSet(oldState, newState)
}
object Promise {
private[zio] object internal {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I could tell, there isn't a great way to test this without it being package private...

@hearnadam
Copy link
Collaborator Author

@kyri-petrou this is ready for review.

@guizmaii
Copy link
Member

@hearnadam Your CI is failing

@hearnadam
Copy link
Collaborator Author

Discussed the PR offline with Kyri today.

  • Will keep the existing behavior for completing in LIFO. We can followup to make it configurable.
  • Avoid using tailrec when building/reading the arrays. Mostly for stylistic reasons. Kept the cas loops recursive as they are much simpler this way.

def loop(): Boolean =
state.get match {
case pending: Pending[?, ?] =>
if (state.compareAndSet(pending, Done(io))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the Done(io) instanciation be put outside of the loop to avoid more than 1 instanciation? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible, though passing it to the loop may have more overhead than just allocating. Since it only has a single frame, it should be okay.

Additionally, that would allocate when the promise was already completed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Thanks :)

if (joiners ne null) joiners.foreach(_(io))
def remove(waiter: IO[Nothing, Nothing] => Any): Pending[Nothing, Nothing] = self
}
private sealed abstract class Link[E, A](final val waiter: IO[E, A] => Any, final val ws: Pending[E, A])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does ws means? waiters? Can we use a more explicit name maybe?

@jdegoes jdegoes merged commit fba034a into zio:series/2.x May 15, 2025
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0