Performance tuning
AI assistant
Build this with Claude Code, Cursor or Copilot
Copy a Tessero-tuned prompt for Performance tuning, grounded in 16 real API signatures , into your IDE's AI. No chatbot, just exact context.
Measure first: vendor/bin/tessero bench runs the benchmark suite
(bench/run.php) with its regression gates on your hardware.
bench/numpy_bench.py runs the NumPy counterparts for comparison.
1. Cross the PHP boundary as rarely as possible
Most slow Tessero code spends its time converting between PHP arrays and native arrays, or making many calls on small arrays.
| Instead of | Do |
|---|---|
foreach over elements calling item() |
one vectorised expression |
building a PHP array in a loop, then arr() |
build it once, convert once; or fromBytes() from a binary source |
json_encode($a->toList()) |
$a->toJson() or response()->ndarray($a) |
| storing arrays as JSON columns and decoding per request | the binary cast (no float parsing) |
| thousands of calls on 10-element arrays through FFI | the extension (0.3 µs per call against 9 µs), or batch the small arrays into one larger array |
Per-call overhead, measured on a 2-core VM with PHP 8.4:
| 10-element add | 1M-element add | |
|---|---|---|
| plain PHP loop | 0.28 µs | ~25 ms |
| extension | 0.28 µs | ~1 ms |
| FFI | 8.7 µs | ~1 ms |
For large arrays the backend does not matter. For small ones it dominates.
2. Avoid temporaries in hot loops
$a->add($b)->mul(2) allocates two arrays. On the FFI package pass out::
$tmp = zeros($shape); $a->add($b, out: $tmp); $tmp->mul(2, out: $tmp);
The allocator does not zero memory (except for zeros()), and blocks of
32 MiB or more are aligned for transparent huge pages. Large temporaries are
cheap to allocate, but still cost a pass over memory.
3. Keep data contiguous for repeated work
Views with unusual strides (transposed, reversed, step > 1) work everywhere,
but repeated heavy operations on them are faster after one copy(). Reductions
along the last axis of a C-contiguous array are the fastest case.
4. Pick the right solver settings
| Workload | Setting |
|---|---|
| MDP with more than a few thousand states | policyIteration(evalSweeps: 10-50) (modified PI) rather than exact PI (O(S³)) or plain VI |
| MDP with γ close to 1 | VI needs about log(ε)/log(γ) sweeps: prefer (modified) policy iteration |
| MILP taking too long | set mipRelGap (e.g. 0.01) and nodeLimit; tighten variable bounds; add valid inequalities |
| LP larger than a few thousand rows/columns | the dense tableau grows as m·(n+m): use a sparse external solver (see [Roadmap(https://github.com/DMJ-CV-91913/tessero/blob/main/docs/project/roadmap.md)) |
| Sparse linear systems | CG for symmetric positive definite matrices, BiCGSTAB otherwise; scale rows to improve conditioning |
5. Threads for batch work
On dedicated workers, threads = cores per worker speeds up element-wise
operations on large arrays and MDP sweeps. It has no effect on arrays below
131 072 elements. See [Memory and threads(https://github.com/DMJ-CV-91913/tessero/blob/main/docs/operations/memory-and-threads.md).
6. Linear algebra
- Make sure the FFI package found OpenBLAS (
tessero doctor). Without itmatmulfalls back to the C kernel andLinalgis unavailable. - For CLI jobs dominated by large matrix products, raise
TESSERO_BLAS_THREADS. - Solve with
solve()rather thaninv()followed bymatmul: it is faster and more accurate.
7. Enable the JIT (FFI package)
The PHP code around FFI calls (shape and stride arithmetic, argument
marshalling) benefits from opcache.jit=tracing. All test suites pass
under the tracing and function JITs. The upstream JIT defects found during
development have workarounds in the code; see [Upstream bugs(https://github.com/DMJ-CV-91913/tessero/blob/main/docs/project/upstream-bugs.md).
Known slow spots
| Operation | vs NumPy | Why |
|---|---|---|
exp, log, trig on 1M values |
4-8× slower | scalar libm; NumPy uses AVX-512 SVML-style kernels |
sort 1M float64 |
3-6× slower | radix sort vs NumPy's AVX-512 quicksort (argsort is on par) |
| FFT 2^20 | 1.5-2× slower | straightforward mixed radix vs pocketfft; rfft does a full complex transform |
toList() 1M |
~2× slower (FFI), faster (ext) | PHP array construction |
These are the top items on the [roadmap(https://github.com/DMJ-CV-91913/tessero/blob/main/docs/project/roadmap.md).