Compare sequential and parallel image processing
Measure the same 64 image preprocessing tasks in an ordinary loop, with one CPU worker and with concurrent workers, then verify every returned metric exactly.
// CPU comparison: ordinary sequential loop, one worker, and multiple workers.
// Run: aner run examples/parallel/cpu_image_blur.aner --max-steps 500000000
// Standalone timings are one trial; run_cpu_cases.py repeats and varies execution order.
import aner.parallel
// Batch preprocessing: one independent grayscale camera image per task.
// The fixture is synthetic: a lighting gradient, a bright central object and
// reproducible sensor texture. Each image varies exposure; no file I/O is timed.
import aner.math
fn process_image(parameters: List, iteration: Int64, seed: Int64) -> List {
let width = cast<Int64>(parameters.get(0))
let height = cast<Int64>(parameters.get(1))
let passes = cast<Int64>(parameters.get(2))
let pixels = width * height
var source = Array<Float64>(pixels, 0.0)
var destination = Array<Float64>(pixels, 0.0)
let exposure = 0.85 + Float64(iteration % 15) * 0.02
// Both buffers start with the same boundary pixels. The blur updates only
// interior pixels, so every pass preserves the original one-pixel border.
var y = 0
while y < height {
var x = 0
while x < width {
let texture = Float64((x * 17 + y * 13 + iteration * 3) % 23) - 11.0
var intensity = 20.0 + 140.0 * Float64(x) / Float64(width - 1) + texture
if x >= width / 4 && x < 3 * width / 4 &&
y >= height / 4 && y < 3 * height / 4 {
intensity = intensity + 65.0
}
intensity = intensity * exposure
if intensity < 0.0 { intensity = 0.0 }
if intensity > 255.0 { intensity = 255.0 }
let index = y * width + x
source.set(index, intensity)
destination.set(index, intensity)
x = x + 1
}
y = y + 1
}
var pass = 0
while pass < passes {
y = 1
while y < height - 1 {
var x = 1
while x < width - 1 {
let index = y * width + x
let neighborhood = source.get(index - width - 1) + source.get(index - width) +
source.get(index - width + 1) + source.get(index - 1) + source.get(index) +
source.get(index + 1) + source.get(index + width - 1) +
source.get(index + width) + source.get(index + width + 1)
destination.set(index, neighborhood / 9.0)
x = x + 1
}
y = y + 1
}
// Read the old image throughout one pass; swap after the full pass.
let previous = source
source = destination
destination = previous
pass = pass + 1
}
var pixel_sum = 0.0
var square_sum = 0.0
var index = 0
while index < pixels {
let value = source.get(index)
pixel_sum = pixel_sum + value
square_sum = square_sum + value * value
index = index + 1
}
// Deterministic output columns: mean gray level, RMS gray level, center pixel.
return List(pixel_sum / Float64(pixels), math.sqrt(square_sum / Float64(pixels)),
source.get((height / 2) * width + width / 2))
}
// Change only these settings to try a larger batch or another CPU allocation.
let jobs = 64
let columns = 3
// 0 = automatic admitted CPU allocation; 1 = serial control.
let requested_workers = 0
// Runner rotates 0/1/2 to vary which method executes first.
let benchmark_order = 0
let parameters = List(64, 64, 3)
fn require(condition: Bool, message: String) -> Unit {
if !condition {
let failure: Result<Bool, String> = Result.err(message)
print(failure.unwrap())
}
}
// NO PARALLELISM: finish one job before calling the same function for the next.
fn measure_sequential(parameters: List, jobs: Int64, columns: Int64) -> List {
let started = Duration.monotonic()
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))
}
}
let seconds = Duration.monotonic().subtract(started).total_seconds()
return List(values, seconds)
}
// PARALLELISM: the task body is unchanged. The executor collects ordered rows.
// workers=1 is the control; workers=0 selects the machine's admitted allocation.
fn measure_repeat(parameters: List, jobs: Int64, workers: Int64) -> List {
let started = Duration.monotonic()
let results = parallel.repeat(iterations: jobs, task: process_image,
parameters: parameters, execution: parallel.local(workers: workers), seed: 2026)
let seconds = Duration.monotonic().subtract(started).total_seconds()
return List(results, seconds)
}
// One untimed task exercises the model before the comparison.
let warmup = process_image(parameters, 0, 2026)
var sequential_run = List()
var one_worker_run = List()
var parallel_run = List()
for phase in Range(3) {
let mode = (benchmark_order + phase) % 3
if mode == 0 { sequential_run = measure_sequential(parameters, jobs, columns) }
if mode == 1 { one_worker_run = measure_repeat(parameters, jobs, 1) }
if mode == 2 { parallel_run = measure_repeat(parameters, jobs, requested_workers) }
}
let sequential_values = cast<Array<Float64>>(sequential_run.get(0))
let one_worker = cast<SimulationResults>(one_worker_run.get(0))
let concurrent = cast<SimulationResults>(parallel_run.get(0))
let sequential_seconds = cast<Float64>(sequential_run.get(1))
let one_worker_seconds = cast<Float64>(one_worker_run.get(1))
let parallel_seconds = cast<Float64>(parallel_run.get(1))
// Verification and printing happen after all timers stop.
require(one_worker.rows() == jobs && concurrent.rows() == jobs &&
one_worker.cols() == columns && concurrent.cols() == columns &&
one_worker.workers() == 1, "Incomplete results or incorrect serial allocation")
require(sequential_seconds > 0.0 && one_worker_seconds > 0.0 && parallel_seconds > 0.0,
"Clock resolution is insufficient for this batch")
for iteration in Range(jobs) {
require(one_worker.seed(iteration) == concurrent.seed(iteration), "Task seed mismatch")
for column in Range(columns) {
let expected = sequential_values[iteration * columns + column]
require(expected == one_worker.value(iteration, column) &&
expected == concurrent.value(iteration, column), "Output differs across execution methods")
}
require(concurrent.value(iteration, 0) >= 0.0 && concurrent.value(iteration, 0) <= 255.0 &&
concurrent.value(iteration, 1) >= concurrent.value(iteration, 0) &&
concurrent.value(iteration, 2) >= 0.0 && concurrent.value(iteration, 2) <= 255.0, "Invalid grayscale metrics")
}
print("case=cpu_image_blur")
print("jobs=" + String(jobs))
print("columns=" + String(columns))
print("workers=" + String(concurrent.workers()))
print("verified=true")
// Caller elapsed times include allocation, CPU context startup and result collection.
print("sequential_seconds=" + String(sequential_seconds))
print("one_worker_seconds=" + String(one_worker_seconds))
print("parallel_seconds=" + String(parallel_seconds))
print("speedup=" + String(sequential_seconds / parallel_seconds))
print("executor_speedup=" + String(one_worker.seconds() / concurrent.seconds()))
for column in Range(columns) {
print("output_" + String(column) + "_mean=" + String(concurrent.mean(column)))
}
print("summary_json=" + concurrent.summary())
print("one_worker_summary_json=" + one_worker.summary())
Make it your experiment.
With a compatible development runtime installed, save this program as examples/parallel/cpu_image_blur.aner inside a folder for your experiment. Open a terminal in that folder, then check and run the program.
aner check examples/parallel/cpu_image_blur.aner
aner run examples/parallel/cpu_image_blur.aner --max-steps 500000000The aner command must be on your PATH. Follow the installation guide if your terminal cannot find it.
Use a compatible current native Aner development engine with aner.parallel, checked parallel.repeat tasks, SimulationResults and Duration.monotonic. This synthetic CPU image workload needs no input files, image library, accelerator, network, service or credential. The published command sets --max-steps 500000000 as an explicit interpreter budget. Checking processes no images; running prints numeric results and creates no file. The default concurrent comparison uses automatic admitted CPU allocation; a one slot allocation runs successfully without a multicore speedup. For the recorded four worker configuration, set requested_workers to 4 and provide at least four admitted CPU slots.
This example uses synthetic images. Measured timings describe the documented workload, runtime build and CPU allocation.
Read the comparison and measurement method