Skip to content

Conversation

@thevilledev
Copy link
Contributor

@thevilledev thevilledev commented Jan 6, 2026

Motivation

When checking if a collection has at least one matching element, users commonly write count(arr, pred) > 0 or count(arr, pred) >= 1. However, count iterates through the entire array even when a match is found early. The any builtin provides early termination. This optimization transforms matching count comparisons into any calls, providing nice performance improvements.

Changes

Add countAny optimizer that transforms:

  • count(arr, pred) > 0 -> any(arr, pred)
  • count(arr, pred) >= 1 -> any(arr, pred)

Tests and benchmarks included. For benchmarks, run:

go test ./optimizer/... -bench='BenchmarkCount' -benchmem -run=^$ -count=10

Benchstat results:

cpu: Apple M1 Pro
                             │  master.out   │               fix.out                │
                             │    sec/op     │    sec/op     vs base                │
CountGtZero-8                  36.809µ ± 34%   1.878µ ± 11%  -94.90% (p=0.000 n=10)
CountGtZeroLargeEarlyMatch-8   371.08µ ± 15%   11.38µ ±  5%  -96.93% (p=0.000 n=10)
CountGtZeroNoMatch-8            36.88µ ±  7%   35.93µ ± 61%        ~ (p=0.247 n=10)
CountGteOneEarlyMatch-8        41.480µ ± 53%   1.911µ ± 22%  -95.39% (p=0.000 n=10)
CountGteOneNoMatch-8            36.44µ ±  6%   35.81µ ±  5%        ~ (p=0.289 n=10)
geomean                         59.75µ         8.792µ        -85.28%

                             │  master.out   │                fix.out                 │
                             │     B/op      │     B/op      vs base                  │
CountGtZero-8                  15.938Ki ± 0%   8.133Ki ± 0%  -48.97% (p=0.000 n=10)
CountGtZeroLargeEarlyMatch-8   158.25Ki ± 0%   80.13Ki ± 0%  -49.36% (p=0.000 n=10)
CountGtZeroNoMatch-8            15.94Ki ± 0%   15.94Ki ± 0%        ~ (p=1.000 n=10) ¹
CountGteOneEarlyMatch-8        15.938Ki ± 0%   8.133Ki ± 0%  -48.97% (p=0.000 n=10)
CountGteOneNoMatch-8            15.94Ki ± 0%   15.94Ki ± 0%        ~ (p=1.000 n=10) ¹
geomean                         25.22Ki        16.82Ki       -33.32%
¹ all samples are equal

                             │   master.out   │                fix.out                │
                             │   allocs/op    │  allocs/op   vs base                  │
CountGtZero-8                   1005.000 ± 0%    6.000 ± 0%  -99.40% (p=0.000 n=10)
CountGtZeroLargeEarlyMatch-8   10005.000 ± 0%    6.000 ± 0%  -99.94% (p=0.000 n=10)
CountGtZeroNoMatch-8              1.005k ± 0%   1.005k ± 0%        ~ (p=1.000 n=10) ¹
CountGteOneEarlyMatch-8         1005.000 ± 0%    6.000 ± 0%  -99.40% (p=0.000 n=10)
CountGteOneNoMatch-8              1.005k ± 0%   1.005k ± 0%        ~ (p=1.000 n=10) ¹
geomean                           1.591k         46.53       -97.08%
¹ all samples are equal

Further comments

These patterns map directly to the existing any builtin which has early termination. Other thresholds like count > 100 cannot be optimized because there's no equivalent builtin.

I also looked into optimising == 0, < 1 and <= 0 with the none builtin. It showed regression in no-match scenarios which I guess comes from the extra OpNot instruction per iteration in none bytecode. Seemed like an optimisation not worth doing at this time.

A potential language enhancement could be adding an atLeast(array, predicate, n) builtin. So if any is "stop at 1st match" then atLeast would "stop at nth match". Basically the optimizer could then do:

  • count(arr, pred) > 100 -> atLeast(arr, pred, 101)
  • count(arr, pred) >= 50 -> atLeast(arr, pred, 50)

And while writing this I actually realised any could be represented as atleast(arr, pred, 1). But let me know what you think @antonmedv!

Optimize count(...) comparisons to use any/none builtins which support
early exit on first matching element:

- count(arr, pred) > 0   → any(arr, pred)
- count(arr, pred) >= 1  → any(arr, pred)

Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
@thevilledev thevilledev marked this pull request as ready for review January 6, 2026 11:11
@antonmedv
Copy link
Member

Thanks for the PR @thevilledev! Awesome optimizations!

atLeast(array, predicate, n)

I think I'm against adding atLeast builtin to the language. Lets not bloat the std lib. But such optimizations can be done on bytecode level.

@thevilledev
Copy link
Contributor Author

Np, it's been fun!

And bytecode level optimisation in that case probably makes more sense. I can look into it next 👍

@antonmedv antonmedv merged commit 13c5b65 into expr-lang:master Jan 6, 2026
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.

2 participants