Skip to content

perf: Avoid array copies and pass only declared callback arguments - #2

Merged
PresentKim merged 2 commits into
mainfrom
refactor/perf-improve
Oct 7, 2026
Merged

PresentKim merged 2 commits into
mainfrom
refactor/perf-improve

Conversation

@PresentKim

@PresentKim PresentKim commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

Follow-up to #1. Brings the library closer to native speed and adds the script that generates the method traits.

Changes

  • No array copy when building results. The userland constructor made ArrayObject copy the whole array, which doubled the cost of every *From() call. Results are now built by cloning an empty instance and calling exchangeArray() (ArrayUtils::blank()).
  • Callbacks receive only the arguments they declare (ArrayUtils::callbackArity()). Callbacks that take 1 or 2 arguments use array_map() / array_filter() / array_reduce() or a loop with fewer arguments; callbacks with 3 or more parameters, variadics, or an undeterminable signature keep the old behavior. As a side effect, internal functions such as strtoupper work as callbacks.
  • last() / keyLast() use array_key_last() on PHP 7.3+ (the old end() path remains for 7.1/7.2).
  • Trait generator: tools/generate-traits.py is now the single source of the traits (--check verifies they are up to date). Generated traits are marked @generated.
  • Benchmarks: added the missing @Warmup to FirstLastKeyExistsBench and UniqueReplaceBench. Without it the one-time autoload cost was counted, which inflated results of those subjects (including some numbers in refactor: Replace magic methods with explicit methods split into feature traits #1).
  • Tests: callback arity paths, non-closure callbacks, independent result instances, last/keyLast, and that pop() / shift() remove the element from the instance.

Behavior changes

  • Callbacks that declare fewer than 3 parameters no longer receive the extra arguments.

Benchmark: main vs this branch

PHPBench suite (1000-element arrays, arrayutils variant, mean per call), both sides with the @Warmup fix, run sequentially on the same machine with PHP 8.3. Not run on PHP 7.x.

main this branch
Geometric mean vs native 1.94x 1.51x
Benchmarks within 1.25x of native 23 / 56 41 / 56

Callback-heavy methods (μs per call; native array_map is 23.3, array_reduce-style loop for reduceRight is 25.4):

method main branch
map 40.6 24.1
reduceRight 41.6 25.5
reduce 34.7 24.5
every 29.8 22.3
filter 42.3 35.6
forEach 30.6 24.8

What is left

  • Wrapping the result in an ArrayObject costs about 0.25μs, or about 1μs when the source array is shared (e.g. values() on a list). This only matters where the native call takes under 0.1μs (values, keyRandom, flat, fill, slice, column). The *As variants return plain arrays and do not pay it.
  • On arrays with only a few elements, checking the callback arity (about 0.1μs) can cost more than it saves (flatMap 0.68 → 0.80μs).

Verification

  • Every method (instance, As, From, FromAs) was compared against the previous implementation on 8 inputs and about 1,100 calls; results and instance state matched.
  • vendor/bin/phpunit: 13 tests, 81 assertions pass (PHP 8.3).
  • python3 tools/generate-traits.py --check reports the traits are up to date.

- Build results by cloning an empty instance and calling exchangeArray(),
  so the userland constructor no longer makes ArrayObject copy the array
- Pass only the arguments a callback declares, delegating to array_map(),
  array_filter() and array_reduce() when possible
- Use array_key_last() on PHP 7.3+ for last()/keyLast()
- Add @WarmUp to benchmarks that lacked it, so the one-time autoload cost
  is not counted
- Add unit tests for the callback arity paths
- Add tools/generate-traits.py, the single source of the method traits
  (supports --check to verify the committed traits are up to date)
- Mark generated traits with @generated
- Test that pop() and shift() remove the element from the instance
@PresentKim
PresentKim merged commit ce80a9d into main Oct 7, 2026
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.

1 participant