Files
coorl-lost-cities/experiments/julia_safe_heuristic/bench_threaded.jl

139 lines
3.9 KiB
Julia
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Pkg
const PROJECT_DIR = @__DIR__
Pkg.activate(PROJECT_DIR; io=devnull)
using JSON
using Printf
using Statistics
using Base.Threads
include(joinpath(PROJECT_DIR, "src", "SafeHeuristic.jl"))
using .SafeHeuristic
const DEFAULT_SNAPSHOT_PATH = joinpath(
dirname(dirname(PROJECT_DIR)),
"runs",
"tmp",
"safe_heuristic_snapshots_3seeds.jsonl",
)
const RUNS_PER_CASE = 5
function load_records(path::AbstractString)
return [JSON.parse(line) for line in eachline(path) if !isempty(strip(line))]
end
function chunk_ranges(length::Int, chunks::Int)::Vector{UnitRange{Int}}
ranges = Vector{UnitRange{Int}}(undef, chunks)
base = length ÷ chunks
extra = length % chunks
start = 1
for chunk in 1:chunks
width = base + (chunk <= extra ? 1 : 0)
stop = start + width - 1
ranges[chunk] = start:stop
start = stop + 1
end
return ranges
end
function run_sequential(records)::Vector{Int}
actions = Vector{Int}(undef, length(records))
@inbounds for idx in eachindex(records)
actions[idx] = safe_heuristic_action(records[idx])
end
return actions
end
function run_threaded(records, chunks::Int)::Vector{Int}
actions = Vector{Int}(undef, length(records))
ranges = chunk_ranges(length(records), chunks)
@threads for chunk in eachindex(ranges)
@inbounds for idx in ranges[chunk]
actions[idx] = safe_heuristic_action(records[idx])
end
end
return actions
end
function run_case(records, chunks::Int)::Vector{Int}
if chunks == 1
return run_sequential(records)
end
return run_threaded(records, chunks)
end
function measure_case(records, chunks::Int, baseline::Vector{Int})::Float64
warmup_actions = run_case(records, chunks)
if warmup_actions != baseline
error("action parity failed during warmup for $(chunks) thread case")
end
times = Vector{Float64}(undef, RUNS_PER_CASE)
for run in 1:RUNS_PER_CASE
actions = Vector{Int}()
elapsed = @elapsed begin
actions = run_case(records, chunks)
end
if actions != baseline
error("action parity failed on run $(run) for $(chunks) thread case")
end
times[run] = elapsed * 1000.0
end
return median(times)
end
function scaling_label(efficiency::Float64)::String
if efficiency >= 0.80
return "near-linear"
elseif efficiency >= 0.50
return "partial scale"
end
return "contention"
end
function main()
if length(ARGS) > 1
println(stderr, "usage: julia --threads=8 experiments/julia_safe_heuristic/bench_threaded.jl [snapshots.jsonl]")
exit(2)
end
path = length(ARGS) == 1 ? ARGS[1] : DEFAULT_SNAPSHOT_PATH
if !isfile(path)
println(stderr, "snapshot file not found: $path")
exit(2)
end
records = load_records(path)
available = Threads.nthreads()
grid = [threads for threads in (1, 2, 4, 8) if threads <= available && threads <= length(records)]
if isempty(grid)
println(stderr, "no thread counts are available")
exit(2)
end
baseline = run_sequential(records)
results = Dict{Int,Float64}()
for threads in grid
results[threads] = measure_case(records, threads, baseline)
end
one_thread_ms = results[1]
println("threads total_ms μs/call speedup vs 1T efficiency")
for threads in grid
total_ms = results[threads]
us_per_call = total_ms * 1000.0 / length(records)
speedup = one_thread_ms / total_ms
efficiency = speedup / threads
@printf("%-7d %8.1f %8.1f %13.2f× %10.0f%%\n", threads, total_ms, us_per_call, speedup, efficiency * 100.0)
end
if 8 in grid
speedup = one_thread_ms / results[8]
efficiency = speedup / 8
@printf("8T efficiency %.0f%%%s\n", efficiency * 100.0, scaling_label(efficiency))
end
end
main()