Measuring#
You cannot tune what you have not measured, and on pperl the usual measuring tools split into three groups: the ones that work unchanged, the ones that work because they are plain Perl running on a faster interpreter, and the one everybody reaches for that does not work at all. This chapter sorts them out and shows how to read what the working ones tell you.
The tool that does not work: Devel::NYTProf#
Devel::NYTProf is the standard line-and-subroutine profiler on upstream Perl. It is an XS module - it hooks the interpreter through a C extension. pperl ships no XS layer, so Devel::NYTProf does not load. Neither does any other XS-based profiler (Devel::DProf, Devel::Profiler’s XS paths).
This is not a gap to be worked around with a pure-Perl reimplementation of NYTProf’s output format. What pperl offers instead is visibility into the layer that actually decides performance - the JIT - plus full transparency to the platform profiler. Use those.
Where the time goes: the JIT log#
pperl does not currently ship a line-or-sub profiler of its own (the one it once had belonged to a retired execution engine and never saw the current runtime). What it does ship is precise visibility into the optimization layer, which for pperl-specific tuning is usually the question you are actually asking: is my hot loop compiled, and if not, why not?
--jit-statsprints counters at exit: candidates found, loops compiled, entries run compiled, guard failures, deopts.PPERL_JIT_LOG=/path/fileappends one line per compiler event. Thecompile skip reason=...lines name the exact operation that kept a loop interpreted.
For symbol-level CPU attribution, use the platform profiler on a release build - perf record ./target/release/pperl script.pl then perf report. pperl is a native binary; perf sees straight through it, including JIT-compiled frames.
Whole-program timing: the benchmark runner#
When the question is “how fast is pperl on this, versus system perl, with and without JIT and parallelism”, the project benchmark runner answers it directly. bench/run-perlbench always runs /usr/bin/perl as the baseline (normalised to 100) and compares pperl in up to four configurations.
./bench/run-perlbench # no JIT, no parallel
./bench/run-perlbench +J # add the JIT-enabled run
./bench/run-perlbench +P # add the parallel run
./bench/run-perlbench +J+P # all four combinations
./bench/run-perlbench -t arith # only benchmarks matching "arith"
Every runner shares one iteration count, derived from the slowest runner’s empty-loop rate, so the reported ratios compare like with like. The +J / +P columns are exactly the comparison you want when deciding whether a piece of code benefits from the JIT or the parallelizer: if +J is no faster than the plain run, the loop is not being compiled, and Writing fast pperl explains why and what to change.
Plain-Perl tools that work: Benchmark and Time::HiRes#
The classic in-program measuring tools are plain Perl. They run on pperl through the normal module path - Benchmark is reachable on the standard include path and executes as Perl on the pperl interpreter; Time::HiRes is provided natively. Both behave exactly as a Perl programmer expects.
Benchmark for A/B comparison of two ways to write the same routine:
use Benchmark qw(cmpthese);
my @data = (1 .. 100_000);
cmpthese(-3, { # run each for ~3 CPU-seconds
grep_count => sub { my $n = grep { $_ % 2 == 0 } @data },
loop_count => sub { my $n = 0; $_ % 2 or $n++ for @data },
});
Time::HiRes for ad-hoc wall-clock timing of a single block:
use Time::HiRes qw(time);
my $t0 = time;
do_the_work();
printf "took %.3f s\n", time - $t0;
A caution that matters more on pperl than on upstream: Benchmark drives your code by calling a code reference over and over, and that per-call dispatch is Benchmark’s own overhead, not your code’s. A loop inside the callback still JIT-compiles (loops compile wherever they live, file scope or sub body), but for small bodies the harness call cost dominates and the comparison understates the real speed. When the thing you are timing is a loop that should JIT, prefer bench/run-perlbench or a Time::HiRes span around the loop in situ (see JIT compilation).
Turning the optimisers off to isolate a cause#
Two flags let you attribute a speedup - or a slowdown - to the right layer:
pperl --no-jit script.pl # interpreter only, no native compilation
pperl --no-parallel script.pl # single-threaded, no Rayon dispatch
Run a program three ways - default, --no-jit, --no-parallel - and the differences tell you which optimiser is carrying the work. If --no-jit barely changes the time, the JIT was not engaging on this code, and the loop shape is the thing to look at. If --no-parallel barely changes it, the parallelizer declined the loop - usually because of a side effect it could not rule out (see Parallel execution).
A measuring discipline#
Time the program default versus
--no-jitversus--no-parallel. The differences tell you which layer carries the work.If a hot routine is a loop you expected to be compiled, confirm with
run-perlbench +Jor by timing it--no-jitversus default. A loop that does not speed up under the JIT is shaped wrong, not slow.Only now change code. Re-measure the same way. Keep the change if the number moved and the program still produces the same output.
Performance claims are cheap; the JIT log and the benchmark ratio are not. Trust the measurement.
See also#
Writing fast pperl - once you have found the hot loop, how to shape it so the JIT and parallelizer take it.
Idioms - which classic optimisations are still worth applying before you reach for the measuring tools.
JIT compilation - what the JIT compiles and why interpreted op timings disappear for compiled loops.
Parallel execution - what the
+Pcolumn and--no-parallelflag are exercising.Regex performance - if the measurement points at a regexp, the cause and cure live there, not here.