typelevel / typelevel/cats

Improve `traverse` performance through increased specialization

Open
#4,408 5 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
5.5k
Forks
1.2k
Avg merge
2d 10h
Merged PRs (30d)
5

Description

This is a follow up on #3790, but breaking it out as a separate issue since we've realized that we can do better than what was originally suggested and discussed. Leverages the findings in #4403. Also relates to #3993.

Altogether, traverse has three distinct performance scenarios, depending on the nature of the underlying applicative G and the underlying traverse F:

  • If the applicative is secretly a StackSafeMonad, the fastest (by about 3x) strategy is to use the direct left-to-right approach with flatMap. There are no stack-safety issues, and any underlying short-circuiting is handled by the monad itself
  • If the applicative has a Defer, then we still need to do the tree branching strategy in order to avoid possible stack issues (just because an applicative forms a Defer does not mean its ap is stack safe!), but we no longer need to introduce the extra layer of Eval wrapping in order to guarantee short-circuiting
  • Otherwise, we need to do tree branching and we need to do Eval wrapping

The first scenario corresponds to doing a traverse with IO or Eval, which are both StackSafeMonads. In this case, the direct flatMap is the fastest thing you can do (verified by checking a hand-coded version). The second scenario corresponds to doing a parTraverse with IO, where the G applicative forms a Defer but does not form a Monad. In this case, we obviously can't use flatMap, but the tree branching strategy is optimal anyway because we want to balance the parallel scheduling. We can rely on the G itself to handle short-circuiting (to the extent that we get it within par) because of Defer. The third scenario corresponds to doing a traverse with something like Option, where we aren't stack safe and we can't form a Defer, so we just need to do the pessimistic tree branching (for stack safety) and Eval wrapping (for short circuiting).

Right now, all Seq-based traversals delegate to traverseViaChain, which strictly implements the third (most pessimistic) scenario. What we want here are two additional implementations corresponding to the first and second scenarios (the second is very close to the third, just without all the Eval.laters), and the swapping between them will be based on runtime type casing on the Applicative[G] instance.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the Seq-based traversals that delegate to traverseViaChain and review the Applicative, StackSafeMonad, Defer, and Eval cases described in the issue. Compare the three traversal strategies and their runtime type dispatch requirements. Done means Seq-based traverse selects the appropriate specialized implementation while preserving stack safety and short-circuiting behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
scala
Domain
performance
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.