Skip to content

perf(array): pre-size Ap and Flatten results#190

Merged
CarstenLeue merged 2 commits into
IBM:mainfrom
nishantmehta:pr/array-presize
Jun 28, 2026
Merged

perf(array): pre-size Ap and Flatten results#190
CarstenLeue merged 2 commits into
IBM:mainfrom
nishantmehta:pr/array-presize

Conversation

@nishantmehta

Copy link
Copy Markdown
Contributor

Problem

array.Ap and array.Flatten both route through MonadChain, which grows the
result from nil; Ap additionally allocates an intermediate slice per function
(via MonadMap).

Change

Both output lengths are known up front — len(fab)*len(fa) for the applicative
cartesian product, and the sum of the inner slice lengths for flatten — so the
result is built in a single pre-sized allocation. Element order and values are
unchanged (the change is in array/generic, which the public array.Ap /
array.Flatten delegate to).

Result

Benchmarks (added), -benchmem -count=6:

benchmark allocs/op B/op ns/op
Ap before 6 800 155
Ap after 1 240 66
Flatten before 3 216 51
Flatten after 1 128 24

−83% / −67% allocations respectively.

Testing

go test -race ./array/... passes (existing tests cover element order/values).
BenchmarkAp and BenchmarkFlatten were added to demonstrate and guard the win.

array.Ap and array.Flatten both routed through MonadChain, which grows the
result from nil and (for Ap) allocates an intermediate slice per function.
Their output lengths are known up front -- len(fab)*len(fa) for the
applicative cartesian product and the sum of inner lengths for flatten -- so
build the result in a single pre-sized allocation.

Benchmarks (added):

  BenchmarkAp        6 -> 1 allocs/op (-83%), 155 -> 66 ns/op
  BenchmarkFlatten   3 -> 1 allocs/op (-67%),  51 -> 24 ns/op

Signed-off-by: Nishant Mehta <nishantmehta.n@gmail.com>
@nishantmehta nishantmehta marked this pull request as ready for review June 26, 2026 13:49
Comment thread v2/array/generic/array.go Outdated
Comment thread v2/array/generic/array.go Outdated
// each a in fa, so its length is known up front. Pre-sizing avoids the
// per-function intermediate slice (one MonadMap allocation each) and the
// append-growth reallocations of the previous MonadChain-based formulation.
result := make(BS, 0, len(fab)*len(fa))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

in the same spirit as the slices.Concat function we can use slices.Grow to have a common way of allocating the slice, and accept nil for the result of an empty result.

Address review feedback by replacing the hand-rolled pre-sizing with the
idiomatic standard library helpers (Go 1.21+, compatible with the module's
1.24 requirement):

- Flatten delegates to slices.Concat, which sizes the result in a single
  allocation.
- MonadAp allocates its result once via slices.Grow, the same idiom
  slices.Concat uses internally.

Both return a nil slice for an empty result, which is a valid representation
of the empty array. The doc comments now state this, and the TestNilSlice_*
assertions are relaxed to check only that the result is empty.

Benchmarks are unchanged at a single allocation per call:

  BenchmarkAp        240 B/op, 1 allocs/op
  BenchmarkFlatten   128 B/op, 1 allocs/op

Signed-off-by: Nishant Mehta <nishantmehta.n@gmail.com>
@CarstenLeue CarstenLeue self-requested a review June 28, 2026 20:33
@CarstenLeue CarstenLeue merged commit dfc0c4b into IBM:main Jun 28, 2026
13 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.3.69 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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.

3 participants