Current section

Files

Jump to
erlang_python examples bench_owngil_pool.erl
Raw

examples/bench_owngil_pool.erl

#!/usr/bin/env escript
%% -*- erlang -*-
%%! -pa _build/default/lib/erlang_python/ebin
%%% @doc Benchmark for OWN_GIL event loop pool.
%%%
%%% Compares regular event loop pool vs OWN_GIL mode for parallel execution.
%%% OWN_GIL mode creates separate GIL per worker, enabling true parallelism.
%%%
%%% Run with:
%%% rebar3 compile && escript examples/bench_owngil_pool.erl
-mode(compile).
main(_Args) ->
io:format("~n"),
io:format("================================================================~n"),
io:format(" OWN_GIL Event Loop Pool Benchmark~n"),
io:format("================================================================~n~n"),
case py_nif:subinterp_supported() of
false ->
io:format("[ERROR] OWN_GIL requires Python 3.12+~n"),
halt(1);
true ->
ok
end,
%% Run regular pool benchmark first
io:format("Phase 1: Regular Event Loop Pool (shared GIL)~n"),
io:format("----------------------------------------------~n"),
application:set_env(erlang_python, event_loop_pool_owngil, false),
{ok, _} = application:ensure_all_started(erlang_python),
timer:sleep(500),
print_system_info(),
RegularStats = py_event_loop_pool:get_stats(),
io:format("Pool config: ~p~n~n", [RegularStats]),
RegularResults = run_benchmarks("Regular"),
%% Stop and restart with OWN_GIL enabled
ok = application:stop(erlang_python),
timer:sleep(200),
io:format("~nPhase 2: OWN_GIL Event Loop Pool (separate GILs)~n"),
io:format("-------------------------------------------------~n"),
application:set_env(erlang_python, event_loop_pool_owngil, true),
{ok, _} = application:ensure_all_started(erlang_python),
timer:sleep(500),
OwngilStats = py_event_loop_pool:get_stats(),
io:format("Pool config: ~p~n~n", [OwngilStats]),
OwngilResults = run_benchmarks("OWN_GIL"),
%% Print comparison
print_comparison(RegularResults, OwngilResults),
halt(0).
print_system_info() ->
io:format("System Information:~n"),
io:format(" Erlang/OTP: ~s~n", [erlang:system_info(otp_release)]),
io:format(" Schedulers: ~p~n", [erlang:system_info(schedulers)]),
{ok, PyVer} = py:version(),
io:format(" Python: ~s~n", [PyVer]),
io:format("~n").
run_benchmarks(Label) ->
#{
sequential => bench_sequential(Label, 500),
concurrent_light => bench_concurrent(Label, 4, 100, light),
concurrent_medium => bench_concurrent(Label, 8, 50, light),
parallel_cpu => bench_parallel_cpu(Label, 4)
}.
%% Sequential calls from single process
bench_sequential(Label, N) ->
io:format(" ~s Sequential (~p calls): ", [Label, N]),
{Time, _} = timer:tc(fun() ->
lists:foreach(fun(I) ->
Ref = py_event_loop_pool:create_task(math, sqrt, [float(I)]),
{ok, _} = py_event_loop_pool:await(Ref)
end, lists:seq(1, N))
end),
Rate = round(N / (Time / 1000000)),
io:format("~.1f ms (~p calls/sec)~n", [Time/1000, Rate]),
#{time_ms => Time/1000, rate => Rate}.
%% Concurrent callers (each process gets own worker in OWN_GIL mode)
bench_concurrent(Label, NumProcs, TasksPerProc, _Complexity) ->
TotalTasks = NumProcs * TasksPerProc,
io:format(" ~s Concurrent (~p procs x ~p tasks): ", [Label, NumProcs, TasksPerProc]),
Parent = self(),
{Time, _} = timer:tc(fun() ->
Pids = [spawn_link(fun() ->
lists:foreach(fun(I) ->
Ref = py_event_loop_pool:create_task(math, sqrt, [float(I)]),
{ok, _} = py_event_loop_pool:await(Ref)
end, lists:seq(1, TasksPerProc)),
Parent ! {done, self()}
end) || _ <- lists:seq(1, NumProcs)],
[receive {done, Pid} -> ok end || Pid <- Pids]
end),
Rate = round(TotalTasks / (Time / 1000000)),
io:format("~.1f ms (~p calls/sec)~n", [Time/1000, Rate]),
#{time_ms => Time/1000, rate => Rate, total => TotalTasks}.
%% CPU-bound parallel tasks (shows true GIL parallelism)
bench_parallel_cpu(Label, NumProcs) ->
io:format(" ~s CPU-bound (~p parallel time.sleep(0.1)): ", [Label, NumProcs]),
Parent = self(),
{Time, _} = timer:tc(fun() ->
Pids = [spawn_link(fun() ->
%% Use time.sleep as a proxy for CPU work
%% In OWN_GIL mode, these run truly in parallel
Ref = py_event_loop_pool:create_task(time, sleep, [0.1]),
Result = py_event_loop_pool:await(Ref, 5000),
Parent ! {done, self(), Result}
end) || _ <- lists:seq(1, NumProcs)],
Results = [receive {done, Pid, R} -> R after 5000 -> timeout end || Pid <- Pids],
%% Count successes
length([X || {ok, _} = X <- Results])
end),
io:format("~.1f ms~n", [Time/1000]),
#{time_ms => Time/1000, num_procs => NumProcs}.
print_comparison(Regular, Owngil) ->
io:format("~n"),
io:format("================================================================~n"),
io:format(" Comparison Summary~n"),
io:format("================================================================~n~n"),
io:format("~-30s ~12s ~12s ~10s~n", ["Benchmark", "Regular", "OWN_GIL", "Speedup"]),
io:format("~-30s ~12s ~12s ~10s~n", [string:copies("-", 30),
string:copies("-", 12),
string:copies("-", 12),
string:copies("-", 10)]),
%% Sequential (should be similar)
SeqR = maps:get(rate, maps:get(sequential, Regular)),
SeqO = maps:get(rate, maps:get(sequential, Owngil)),
SeqSpeedup = SeqO / max(1, SeqR),
io:format("~-30s ~10w/s ~10w/s ~8.2fx~n",
["Sequential (single caller)", SeqR, SeqO, SeqSpeedup]),
%% Concurrent light
CL_R = maps:get(rate, maps:get(concurrent_light, Regular)),
CL_O = maps:get(rate, maps:get(concurrent_light, Owngil)),
CL_Speedup = CL_O / max(1, CL_R),
io:format("~-30s ~10w/s ~10w/s ~8.2fx~n",
["Concurrent (4 procs x 100)", CL_R, CL_O, CL_Speedup]),
%% Concurrent medium
CM_R = maps:get(rate, maps:get(concurrent_medium, Regular)),
CM_O = maps:get(rate, maps:get(concurrent_medium, Owngil)),
CM_Speedup = CM_O / max(1, CM_R),
io:format("~-30s ~10w/s ~10w/s ~8.2fx~n",
["Concurrent (8 procs x 50)", CM_R, CM_O, CM_Speedup]),
%% CPU-bound
CPU_R = maps:get(time_ms, maps:get(parallel_cpu, Regular)),
CPU_O = maps:get(time_ms, maps:get(parallel_cpu, Owngil)),
CPU_Speedup = CPU_R / max(1, CPU_O),
io:format("~-30s ~10.1f ms ~10.1f ms ~8.2fx~n",
["CPU-bound (4 parallel)", CPU_R, CPU_O, CPU_Speedup]),
io:format("~n"),
io:format("Notes:~n"),
io:format(" - Regular pool: all workers share Python's main GIL~n"),
io:format(" - OWN_GIL pool: each worker has independent GIL~n"),
io:format(" - OWN_GIL enables true parallelism for CPU-bound tasks~n"),
io:format(" - Speedup for CPU-bound tasks approaches number of workers~n"),
io:format("~n").