CPU parallelism in practice
Process independent images with the same task function, compare an ordinary loop with local CPU workers, and inspect measured timing and exact result checks.
Implemented in developmentProcess a batch of camera images concurrently by changing the outer loop. Keep the image processing function, its parameters and its results the same. Aner's parallel.repeat assigns independent images to local CPU workers and collects numeric result rows in iteration order.
This guide belongs to Native parallel computing. For exact task signatures, supported operations, input copying, resource limits and errors, see Independent simulation repetitions.
Start with independent image work
A camera preprocessing pipeline often applies the same transformation to many images. Each image can be processed independently, so several CPU workers can handle different images at once.
The complete example processes 64 synthetic grayscale images, each 64 × 64 pixels. For each image, process_image generates a lighting gradient, a bright central object and reproducible texture, applies three 3 × 3 box blur passes, and returns three numeric metrics: mean gray level, RMS gray level and center pixel gray level. Values use the 0 to 255 grayscale range. The original one pixel border stays fixed during every pass.
These synthetic images make the program runnable without input files, an image library, network access or an accelerator. They illustrate a camera preprocessing workload; no camera capture, image decoding, file reading or image saving is measured. The returned metrics describe the processed images. The program does not write image files.
Start with the compact runnable image example or download its source. The complete comparison adds the ordinary sequential loop, a one worker control, timers and equality checks.
The small change
Both versions below use the same process_image(parameters: List, iteration: Int64, seed: Int64) -> List function from the complete source, with parameters = List(64, 64, 3), jobs = 64 and columns = 3.
The ordinary loop finishes one image before starting the next and copies each metric into an output array:
let values = Array<Float64>(jobs * columns, 0.0)
for iteration in Range(jobs) {
let row = process_image(parameters, iteration, 2026)
for column in Range(columns) {
values[iteration * columns + column] = cast<Float64>(row.get(column))
}
}Replace that loop and its manual result collection with one call:
let results = parallel.repeat(
iterations: 64, task: process_image,
parameters: List(64, 64, 3),
execution: parallel.local(workers: 4), seed: 2026)Both downloadable programs include the required aner.parallel and aner.math imports and the complete process_image function. The task function does not change. parallel.repeat passes each task its copied parameters, iteration index and derived seed. Four workers process independent images concurrently. Results retain iteration order even if images finish in another order. Read one metric with results.value(iteration, column) or a column mean with results.mean(column).
The explicit workers: 4 request requires at least four admitted local CPU slots. Use workers: 0, or omit the worker count, for the automatic admitted CPU allocation. An automatic allocation of one slot is valid, but cannot demonstrate a multicore speedup. Inspect the actual count with results.workers().
Why this loop can run concurrently
Each image owns its two mutable pixel buffers. The shared parameters describe dimensions and pass count; each task receives its own copied inputs. Workers return their own numeric rows, so the caller does not coordinate shared output appends or counters.
Keep the three blur passes inside each task sequential: a pass must finish before the next pass reads its output. Within a pass, the function reads the source buffer and writes a separate destination buffer, then swaps buffers after all interior pixels are updated. Parallelizing the independent outer batch preserves those dependencies.
Fixture pixels depend deterministically on image identity through iteration. This particular task does not consume the injected seed parameter, so calling it directly in the ordinary loop generates exactly the same image as calling it through parallel.repeat. For stochastic tasks, follow the repetition seed and RNG contract when constructing a comparable sequential control.
For a real image pipeline, prepare inputs using supported task values and preserve private working buffers. Task bodies must stay within the checked operation and effect contract documented in the repetition API. Small jobs, shared dependencies and memory traffic can limit the benefit of concurrency.
Run the programs
Use a compatible current native Aner development engine with aner.parallel, checked parallel.repeat tasks, SimulationResults and Duration.monotonic. The compact program explicitly requests four admitted CPU slots. The full comparison uses automatic allocation by default and also runs the one worker control. Download a source file, preserve the paths below beneath your project directory, and run with aner on your command path:
aner check examples/parallel/cpu_image_blur_intro.aner
aner run examples/parallel/cpu_image_blur_intro.aner --max-steps 500000000
aner check examples/parallel/cpu_image_blur.aner
aner run examples/parallel/cpu_image_blur.aner --max-steps 500000000The explicit step budget accommodates the full comparison's ordinary loop and cumulative task calls. Each checked parallel task has its own interpreter step budget. Both programs print their results and create no files. Checking validates source without processing images.
The compact program prints 64 followed by 104.45688468195286, the mean gray level across its 64 processed images.
The full comparison prints verified=true only after every returned metric matches exactly across the ordinary loop, the one worker executor and the concurrent executor. It also verifies the derived seeds between executor runs and checks the grayscale metric ranges. These checks cover the returned metrics; the example does not compare or export every intermediate pixel.
Measured CPU comparison
A recorded comparison on October 16, 2026 in America/Denver used an Apple M2 Max, 12 visible and effective CPU slots, an optimized Release native engine identifying itself as Aner 0.0.2-dev.1, and four requested CPU workers. Three measured trials followed one excluded full program warmup. Each trial processed the complete 64 image batch with all three methods and rotated which method ran first.
| Method | Median caller elapsed time |
|---|---|
| Ordinary sequential loop | 2.6234 s |
parallel.repeat with one worker | 2.6163 s |
parallel.repeat with four workers | 0.7858 s |
The median paired speedup was 3.330× relative to the ordinary sequential loop. This statistic is the median of each trial's sequential time divided by its parallel time. Dividing the independent median durations produces a different statistic, retained separately in the evidence. All three trials passed exact equality checks for every returned metric; both executor runs also produced identical derived seeds. Native summaries record CPU Float64 results, four workers and four peak active tasks.
These caller elapsed times include the complete batch operation, result allocation and collection. The executor paths include context creation, thread startup, parameter snapshots and result publication. Synthetic image generation and all three blur passes are timed inside each task. Verification, aggregate statistics and printing happen after the timers stop. The separate executor_speedup in the evidence uses SimulationResults.seconds() and excludes some setup and publication costs.
This measures checked Aner task bodies interpreted by the current native CPU executor. It does not measure ahead of time compilation. Hardware, admitted CPU allocation, task size, memory traffic and background activity affect the result; four workers do not guarantee a fourfold speedup. The original record reports no concurrent agent builds, tests or benchmarks, while ordinary user and operating system activity was uncontrolled.
Download the complete image benchmark evidence. It preserves all three trial timings and execution orders, output metrics, native CPU summaries, CPU inventory, exact source and configured variant hashes, and native executable identity. The download includes only this image case and omits workstation file paths. The unchanged full comparison source defaults to automatic workers; the recorded variants explicitly set requested_workers = 4 and rotate benchmark_order through 0, 1 and 2. The recorded runner executes each variant with --prod --max-steps 500000000.