Acton v0.31.0: Faster Programs, Better Tools

This release brings a broad performance boost, from iteration and arithmetic to actor scheduling and timers. It also introduces our own fork of the Boehm garbage collector, lazy generator expressions, new immutable and compact collections, and better tools for measuring performance. There are improvements to project builds and fixes throughout the standard library.

Less work in everyday operations

Iteration now finishes through ordinary control flow. next() returns a maybe value, using just(value) for an item and nothing() for exhaustion, instead of throwing an exception when the iterator runs out. The compiler can often eliminate the allocation of that result. Fewer allocations reduce pressure on the memory subsystem and leave less garbage for the collector to reclaim later. Some iteration-heavy applications now run more than twice as fast.

There are savings in other common operations, too. Direct loops over range() avoid heap allocations, fixed-width numeric conversions avoid unnecessary boxing, and hashing strings, bytes, and integers creates fewer temporary objects.

The runtime also spends less time moving actors through queues and coordinating workers. On Linux and macOS, timed messages now use kernel timers instead of spinning while they wait. In timer-heavy workloads, that reduces CPU time by more than 90%.

Lazy generator expressions

Generator expressions bring comprehension syntax to lazy iteration. They produce values as a consumer asks for them, without first building a list:

total = sum(n * n for n in range(1000) if n % 2 == 0)

A generator is a single-pass iterator: once consumed, it is exhausted. The outermost iterable is evaluated when the generator is created; result expressions, filters, and nested iterables are evaluated as iteration advances and must be pure.

The compiler can fuse a generator with a consumer such as sum() or a for loop, turning the whole operation into direct loops. It can also do this when the generator is stored in a local variable and consumed once. Fusion avoids allocating the intermediate iterator pipeline while preserving the lazy evaluation rules.

The new flatmap() builtin adds lazy flattening: a pure callback returns an iterator for each input, and its elements are consumed before moving to the next input.

Our own garbage collector fork

Acton now uses a maintained fork of the Boehm garbage collector. This gives us room to improve collection for Acton's workloads and expose settings that applications can measure and tune.

Several improvements are enabled by default, including parallel mark-stack range stealing and immediate use of thread-local free lists. Other choices are opt-in. Applications can adjust mark layout, dirty-page tracking, heap growth, and allocation budgets through build_options in Build.act. acton.rts.get_gc_info exposes the active mode and settings, making it easier to check which configuration a program is actually using.

The useful setting depends on the workload. The performance tools described below let you compare configurations on the same program and input, so you can choose settings for the work your application actually does.

Linux and macOS applications can also select mimalloc for ordinary C and native-library allocations. This is a separate choice: Acton objects continue to use garbage collection.

Measuring performance and how it scales

Performance and scaling studies use ordinary Acton test definitions. Benchmarks mark their measured body with t.loop(), so setup and cleanup outside that loop do not contribute to operation time or allocation totals. Process peak memory still includes setup, which matters when a benchmark prepares a large input.

Compare a benchmark with acton test perf

acton test perf has been reworked throughout. It builds optimized code by default, calibrates a workload size when needed, warms up, and then measures repeated invocations. Reports show wall time, allocation volume, peak memory use, and GC statistics.

The old runner forced collections in an attempt to improve memory statistics. The new runner leaves GC on its natural schedule, with no forced collections between invocations. Collections that happen during the measured work remain part of the reported wall time.

Where supported, we now collect retired instructions and CPU cycles across all threads in the test process, alongside CPU time and instructions per cycle (IPC). These counters give another view of the work behind a timing result.

The measurement techniques and terminal UI take inspiration from poop. We've also adopted its lightning (⚡) and poop (💩) markers for clear improvements and regressions.

Measurements can be compared with a saved report or another Git revision:

acton test perf --name my_test --compare git:main

This measures main and the current working tree, including uncommitted changes, at the same workload size. Fresh processes and interleaved pairs help reduce noise, and the report includes an estimate of uncertainty. Both project versions use the running compiler and runtime; utils/perf-compare supports comparisons of changes to Acton's compiler, runtime, or builtins themselves.

acton test perf comparing two list-building implementations, with timing, instruction and cycle counts, allocations, and lightning markers

Comparing appends with repeated list concatenation at 4,096 elements, using a saved baseline. Both implementations use the same compiler and runtime.

Explore growth with acton test scale

acton test scale studies how a benchmark's cost grows with its workload. The benchmark defines what scale means, such as the number of elements to sort, and reads the selected size with t.scale().

The study increases that size, usually doubling it, and measures each size in fresh processes. It continues until observed growth settles over a broad range or a time or memory limit stops it. Results include a table of growth between sizes and terminal charts of time, time per unit of scale, and allocated bytes. Ghostty and Kitty display those charts as inline images.

Scaling studies can also compare saved results or Git revisions:

acton test scale --name my_test --compare git:main

With the default settings, both versions are measured at the same completed workload sizes and their curves are overlaid. That shows where a change saves work and whether the savings hold as the input grows.

acton test scale comparing repeated list concatenation with appends across increasing input sizes, using native image charts and logarithmic axes

Repeated concatenation (orange baseline) versus appends (solid curves), from 1,024 to 65,536 elements. Logarithmic axes keep both visible and show the cost of repeatedly copying the growing list. Both implementations use the same compiler and runtime.

New collections for different workloads

The new ilist, idict, and iset types provide immutable collection APIs. freeze() turns mutable collections into their immutable forms, and hashable iset values can be used as dictionary keys or nested inside other sets.

For numeric and Boolean workloads, array, bitarray, and bitset provide compact storage. The new matrix module adds numeric matrices with row, column, transposed, and reshaped views that share the underlying data. You can work with a different view of a matrix without copying all its elements.

Mutable collections also gain in-place augmented assignment. Operations such as += on lists and bytearrays and |= on sets update the existing collection, avoiding copies and making those changes visible through aliases.

Cleaner builds and a more reliable standard library

Generated Zig build files now live under out/zig/, keeping them out of the project root. Dependencies can select a subdirectory of a monorepo archive, so several packages can share one repository. Focused test builds and watch mode also handle dependency refreshes and cancellation more reliably.

The standard library has fixes across string and byte operations, Unicode handling, regular expressions, dictionaries, and file I/O. File writes now preserve the correct position and complete partial writes, and TCP servers drain queued data before closing a connection.

Before upgrading

A few changes deserve a look when updating an existing project:

The full changelog has the complete list of changes and compatibility notes. The installation guide covers getting Acton on your system.