In basic principle, go arenas give an allocation path where the allocations can be quickly discarded on request. It sort of presents a way to do memory accounting on allocation types, and presents an optimisation path that would bring phpscript closer to how php works.
In php, each worker request gets a memory_limit buffer for allocations. Go arenas produce a similar mechanism for allocation control, even if not a complete one. As some grok based research notes beyond the break, it is impossible to cover all allocation paths, and it has a tendency to "infect" code by passing along arena APIs for allocation and free. Can't really hint encoding/json to use the arena all of a sudden, or database/sql for that matter.
The Go GC is a bit of a bottleneck for this type of application, and I can only wonder how efficient a rust implementation could be. In practice reimplementing even a small part of the go standard library to support arenas would be misguided, a lot of the restrictions are engrained in the toolchain (e.g. the way interfaces are implemented, etc.).
Either way, even if only a certain 20-30% digit allocation rate gets cut, memory usage for the requests becomes predictable, arenas could be provided over a sync.Pool or a similar pool with a max size to prevent resource exhaustion and factor in some overhead by the go process itself.
A significant dependency that enables phpscript is expr-lang. It would be nearly required to cover it with arenas to enable phpscript benefit. In practice, reimplementing it isn't trivial, but not every statement needs to be evaluated in expr lang. I would like phpscript to have allocation control, which requires arena backed expressions (e.g. __set). A lot of the inline VM allocations tend to center around the stack, for which the naive arena is a sync.Pool, a .Reset call, and some leetcode style stack management to reuse the slice allocations.
Memory arenas in Go are an experimental feature (introduced in Go 1.20, behind GOEXPERIMENT=arenas) that lets you allocate many related objects from a contiguous block of memory and free them all at once, largely bypassing the garbage collector for those objects.
How they work
- You create an arena with
arena.NewArena().
- You allocate values or slices from it with
arena.New[T](a) or arena.MakeSlice[T](a, len, cap).
- When you’re done, you call
a.Free() (or rely on the arena becoming unreachable). The entire region is released together.
This is a classic bump-pointer / region allocator: a large chunk is obtained up front, a pointer is advanced for each allocation, and everything is discarded in one shot. There is little or no per-object bookkeeping and no individual GC tracking while the arena is live.
Benefits (when used appropriately):
- Lower GC CPU time and fewer GC cycles.
- Reduced heap memory pressure.
- Better locality (objects live close together).
Google reported up to ~15% CPU/memory savings in some large internal workloads that allocated many short-lived objects.
Limitations and caveats:
- Completely unsupported; the Go team has made no compatibility guarantees and has effectively put the public API on indefinite hold because of design concerns (it tends to “infect” APIs).
- An arena must not be used concurrently by multiple goroutines.
- After
Free(), any remaining pointers into the arena become invalid (use-after-free is a hard error; on 64-bit the memory is typically unmapped so accesses fault).
- Best for bulk allocations on the order of MiB, not tiny one-off objects.
- Strings, interfaces, and some other values may still escape to the regular heap.
- You normally need separate code paths for arena vs non-arena builds.
Third-party pure-Go arena libraries also exist if you want something that doesn’t depend on the experimental flag.
How github.com/titpetric/phpscript could take advantage of them
phpscript is a pure-Go experimental PHP syntax VM / interpreter (no CGO). It parses a limited PHP subset into an AST, then runs it. Typical workloads involve many short-lived allocations that share a clear lifetime:
- AST nodes and parse trees while parsing a script.
- Runtime values (PHP variables, arrays, objects, temporary results) during execution of a request/script.
- Intermediate structures for template rendering, database result handling, callbacks, etc.
- Per-request or per-script execution context.
These map very well to arena-style allocation:
-
Per-request / per-script arena
Create an arena at the start of handling a PHP script or HTTP request. Allocate the AST (if not cached), the execution frame, all temporary PHP values, array elements, etc. from that arena. When the script finishes, free the arena. This eliminates the GC work that would otherwise be needed for the many short-lived objects a PHP-like language creates.
-
Parser / AST construction
The parser package builds a tree of nodes. Allocating those nodes (and any associated slices/maps) from an arena that lives only for the duration of parsing (or for the lifetime of a cached compiled form) would reduce allocation pressure during compilation of scripts.
-
Runner / VM execution
The runner evaluates expressions and statements, creating many intermediate values. A per-execution arena would keep those allocations cheap and allow a single free at the end of the script.
-
Template / stdlib helpers
Any temporary buffers, result sets, or intermediate structures created while executing templates or the limited standard-library shims could also live in the same arena.
Because arenas are experimental and require the build tag / GOEXPERIMENT, a practical approach would be:
- Keep the normal heap path as the default.
- Optionally enable an arena-backed allocator (behind a build tag or runtime flag) for the hot paths that allocate many short-lived objects.
- Ensure that any values that must outlive the script (e.g., results returned to Go callers, cached compiled scripts) are copied out of the arena before it is freed.
In short, arenas are a natural fit for a PHP VM’s request-scoped or script-scoped temporary objects, exactly the pattern where Go’s GC tends to spend the most time. The experimental status means it would be an optional optimisation rather than a core dependency.
In basic principle, go arenas give an allocation path where the allocations can be quickly discarded on request. It sort of presents a way to do memory accounting on allocation types, and presents an optimisation path that would bring phpscript closer to how php works.
In php, each worker request gets a memory_limit buffer for allocations. Go arenas produce a similar mechanism for allocation control, even if not a complete one. As some grok based research notes beyond the break, it is impossible to cover all allocation paths, and it has a tendency to "infect" code by passing along arena APIs for allocation and free. Can't really hint encoding/json to use the arena all of a sudden, or database/sql for that matter.
The Go GC is a bit of a bottleneck for this type of application, and I can only wonder how efficient a rust implementation could be. In practice reimplementing even a small part of the go standard library to support arenas would be misguided, a lot of the restrictions are engrained in the toolchain (e.g. the way interfaces are implemented, etc.).
Either way, even if only a certain 20-30% digit allocation rate gets cut, memory usage for the requests becomes predictable, arenas could be provided over a sync.Pool or a similar pool with a max size to prevent resource exhaustion and factor in some overhead by the go process itself.
A significant dependency that enables phpscript is expr-lang. It would be nearly required to cover it with arenas to enable phpscript benefit. In practice, reimplementing it isn't trivial, but not every statement needs to be evaluated in expr lang. I would like phpscript to have allocation control, which requires arena backed expressions (e.g. __set). A lot of the inline VM allocations tend to center around the stack, for which the naive arena is a sync.Pool, a .Reset call, and some leetcode style stack management to reuse the slice allocations.
Memory arenas in Go are an experimental feature (introduced in Go 1.20, behind
GOEXPERIMENT=arenas) that lets you allocate many related objects from a contiguous block of memory and free them all at once, largely bypassing the garbage collector for those objects.How they work
arena.NewArena().arena.New[T](a)orarena.MakeSlice[T](a, len, cap).a.Free()(or rely on the arena becoming unreachable). The entire region is released together.This is a classic bump-pointer / region allocator: a large chunk is obtained up front, a pointer is advanced for each allocation, and everything is discarded in one shot. There is little or no per-object bookkeeping and no individual GC tracking while the arena is live.
Benefits (when used appropriately):
Google reported up to ~15% CPU/memory savings in some large internal workloads that allocated many short-lived objects.
Limitations and caveats:
Free(), any remaining pointers into the arena become invalid (use-after-free is a hard error; on 64-bit the memory is typically unmapped so accesses fault).Third-party pure-Go arena libraries also exist if you want something that doesn’t depend on the experimental flag.
How
github.com/titpetric/phpscriptcould take advantage of themphpscriptis a pure-Go experimental PHP syntax VM / interpreter (no CGO). It parses a limited PHP subset into an AST, then runs it. Typical workloads involve many short-lived allocations that share a clear lifetime:These map very well to arena-style allocation:
Per-request / per-script arena
Create an arena at the start of handling a PHP script or HTTP request. Allocate the AST (if not cached), the execution frame, all temporary PHP values, array elements, etc. from that arena. When the script finishes, free the arena. This eliminates the GC work that would otherwise be needed for the many short-lived objects a PHP-like language creates.
Parser / AST construction
The parser package builds a tree of nodes. Allocating those nodes (and any associated slices/maps) from an arena that lives only for the duration of parsing (or for the lifetime of a cached compiled form) would reduce allocation pressure during compilation of scripts.
Runner / VM execution
The runner evaluates expressions and statements, creating many intermediate values. A per-execution arena would keep those allocations cheap and allow a single free at the end of the script.
Template / stdlib helpers
Any temporary buffers, result sets, or intermediate structures created while executing templates or the limited standard-library shims could also live in the same arena.
Because arenas are experimental and require the build tag /
GOEXPERIMENT, a practical approach would be:In short, arenas are a natural fit for a PHP VM’s request-scoped or script-scoped temporary objects, exactly the pattern where Go’s GC tends to spend the most time. The experimental status means it would be an optional optimisation rather than a core dependency.