The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#!/usr/bin/perl -w
require 5.002;
use strict;
$| = 1;
=encoding utf8
=head1 NAME
perlbench - compare the relative speed of different versions of Perl
=head1 SYNOPSIS
% perlbench-run [-s] [-t filter] [-c cpu-factor] [-d results_dir]
=head1 DESCRIPTION
This is a Perl benchmark suite. It can be used to compare the
relative speed of different versions of Perl. You run the benchmark
by starting the 'perlbench-run' script giving the path-name of various
Perls you want to test as argument. The perlbench-run program takes
the following optional parameters:
-s don't scale numbers (so that first perl is always 100)
-t <filter> only tests that match <filter> regex are timed
-c <cpu-factor> use this factor to scale tests instead of running the
'cpu_factor' program to determine it.
-d <dirname> what directory to save results in
=head2 Creating new tests
The individual tests are found in a directory called "benchmarks".
They expect to be started with at least a single number as argument.
This number is the CPU speed factor as calculated by the 'cpu_factor'
program. This factor is used to scale the number of iterations that
the test must run to give measurable timing.
A new test is created by making a new file under the "benchmarks"
directory. The filename should end with the F<*.t> suffix. A test
should look like this:
# Name: Regexp matching
# Require: 4
require 'benchlib.pl';
# YOUR SETUP CODE HERE
$a = 0;
&runtest(100, <<'ENDTEST');
# YOUR TESTING CODE HERE
$a++; # for instance
ENDTEST
The first part of the test declares some properties of the test. The
'require' property means that you need a perl with version number
greater or equal to this to run the test (same as the 'require NUM'
does for perl5). You are advised to write the tests so that they can
run under perl4 as well as perl5.
You should then load the F<benchlib.pl> library. This will take care
of the command line arguments and also provide the function
C<&main::runtest()> which will perform the testing. The first argument
to C<runtest()> is the test scale factor. It should be set to some
number that makes the test run for about about 10 seconds when given a
proper CPU factor command line argument. The second argument to
C<runtest()> is the code you want to test. The code should be suitable
as the body inside a loop.
If you want to contribute some additional tests, please make a pull
=head1 AUTHOR
Gisle Aas originally wrote the code, and it's still pretty much what
he created.
Maintained by brian d foy, C<< <bdfoy@cpan.org> >>.
=cut
use vars qw($opt_c $opt_t $opt_d $opt_s $opt_v);
getopts("c:t:d:sv") or usage();
use Sys::Hostname qw(hostname);
use File::Basename qw(basename dirname);
use File::Path qw(mkpath);
use Cwd qw(abs_path);
my @perls;
for (@ARGV) {
eval {
push(@perls, Perl->new($_));
};
if ($@) {
$@ =~ s/ at (.*) line (\d+).*\n//;
warn "$@, skipping...\n";
}
}
usage() unless @perls;
my $HOSTNAME_HTML = htmlesc(hostname());
# result directory
my $dir = $opt_d;
unless ($dir) {
my $cnt = 1;
while (1) {
$dir = sprintf "benchres-%03d", $cnt;
last unless -e $dir;
$cnt++;
}
}
mkdir($dir, 0755) || die "Can't mkdir(\"$dir\"): $!";
open(INDEX, ">$dir/index.html") || die "Can't create $dir/index.html: $!";
print INDEX "<html>\n";
print INDEX "<head>\n";
print INDEX " <title>PerlBench $HOSTNAME_HTML " . time2iso() . "</title>\n";
print INDEX qq( <link rel="stylesheet" href="style.css" type="text/css">\n);
link_or_copy("style.css");
my $use_overlib;
if (link_or_copy("overlib.js")) {
$use_overlib++;
print INDEX qq( <script type="text/javascript" src="overlib.js"></script>\n);
}
print INDEX "</head>\n";
print INDEX "<body>\n";
print INDEX qq(<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>\n);
print INDEX qq(<h1>PerlBench results from $HOSTNAME_HTML at ) . time2iso() . qq(</h1>\n);
# Show perl configurations
my %config_summary;
{
my %cnf;
my $keymax = length("version");
for my $p (@perls) {
while (my($k,$v) = each %{$p->{config} || {}}) {
$cnf{$k}{$v}++;
$keymax = length($k) if length($k) > $keymax;
}
}
for my $p (@perls) {
my $label = $p->{label} || '';
my $name = $p->{name} || basename($p->{path});
print "$label) $name\n";
printf "\t%-*s = %s\n", $keymax, "version", $p->{version};
printf "\t%-*s = %s\n", $keymax, "git-version", $p->{git_version} if $p->{git_version};
printf "\t%-*s = %s\n", $keymax, "path", $p->{path};
for my $k (sort keys %{$p->{config} || {}}) {
next if $cnf{$k}{$p->{config}{$k}} == @perls;
printf "\t%-*s = %s\n", $keymax, $k, $p->{config}{$k};
$config_summary{$k}{$label} = $p->{config}{$k};
}
print "\n";
open(RES, ">$dir/CONFIG-$p->{label}.txt") || die;
$p->run_cmd(*V, "-V") || die "Can't run $p->{path}: $?";
while (<V>) {
print RES $_;
}
close(V);
close(RES) || die "Can't write: $!";
}
}
my $factor = $opt_c;
unless ($factor) {
$factor = -e 'cpu_factor' ? `$^X cpu_factor` : `$^X -S cpu_factor`;
chomp($factor);
die "Can't calculate cpu speed factor" unless $factor;
}
file("$dir/CPU_FACTOR", "$factor\n");
# Try to run tests
die "No test directory found" unless -d 't';
my @tests;
find(sub { /\.t$/ && push(@tests, $File::Find::name) }, "benchmarks");
if ($opt_t) {
@tests = grep /$opt_t/o, @tests;
}
@tests = sort @tests;
# Try to run the empty test in order to time the loop
my %empty_cycles;
for my $p (@perls) {
$p->{empty_cycles} = ($empty_cycles{$p->{path}} ||= do {
my $empty_cycles;
$p->run_cmd(*P, "empty.t", $factor);
while (<P>) {
next unless /^Cycles-Per-Sec:\s*(\S+)/;
$empty_cycles = int($1);
last;
}
close(P);
die "Could not determine empty test speed for $p->{path}"
unless $empty_cycles;
$empty_cycles;
});
$p->{point_sum} = 0;
}
# heading
print INDEX "<table border=1>\n";
print INDEX " <tr>\n <th>&nbsp;</th>\n";
print "\n";
print " " x 20;
for my $p (@perls) {
printf "%8s", $p->{label};
my $h = htmlesc($p->{label});
my $overlib_attr = "";
if ($use_overlib && $p->{name}) {
$overlib_attr = qq( onmouseover="return overlib('$p->{name}');" onmouseout="return nd();");
}
print INDEX qq( <th><a href="CONFIG-$h.txt"$overlib_attr>$h</a></th>\n);
}
print "\n";
print INDEX " </tr>\n";
print " " x 20;
for my $p (@perls) {
printf "%8s", ("-" x max(3, length($p->{label})));
}
print "\n";
my $test;
for $test (@tests) {
unless (open(T, $test)) {
warn "Can't open $test: $!";
next;
}
my $name = $test;
$name =~ s,^benchmarks/,,;
$name =~ s,\.t$,,;
my $save_file = "$dir/$name/test.txt";
mkpath(dirname($save_file), 0, 0755);
open(SAVE, ">$save_file") || die "Can't create $save_file: $!";
(my $save_file_link = $save_file) =~ s,^\Q$dir\E/,,;
$save_file_link = htmlesc($save_file_link);
my %prop;
while (<T>) {
print SAVE $_;
next unless /^\#\s*(\w+)\s*:\s*(.*)/;
my($k,$v) = (lc($1), $2);
if (defined $prop{$k}) {
$prop{$k} .= "\n$v";
} else {
$prop{$k} = $v;
}
}
close(T);
close(SAVE) || die "Can't write $save_file: $!";
printf "%-20s", $name;
my $overlib_attr = "";
if ($use_overlib && $prop{name}) {
$overlib_attr = qq( onmouseover="return overlib('$prop{name}');" onmouseout="return nd();");
}
print INDEX qq( <tr>\n <th align=left><a href="$save_file_link"$overlib_attr>) . htmlesc($name) . "</a></th>\n";
my $scale;
my $p;
for my $p (@perls) {
if ($p->{version} < $prop{'require'}) {
# Can't run test
printf "%8s", "N/A";
print INDEX " <td>N/A</td>\n";
next;
}
my $res_file = "$dir/$name/" . $p->{label} . ".txt";
mkpath(dirname($res_file), 0, 0755);
open(RES, ">$res_file") || die "Can't create $res_file: $!";
(my $res_file_link = $res_file) =~ s,^\Q$dir\E/,,;
$res_file_link = htmlesc($res_file_link);
my $points;
my $popup_text = "";
$p->run_cmd(*P, $test, $factor, $p->{empty_cycles});
while (<P>) {
print RES $_;
if (/^Bench-Points:\s+(\S+)/) {
$points = $1;
}
if (/^(?:\w+-Time|CPU|Cycles-Per-Sec|Loop-Overhead):/) {
$popup_text .= "<br>" if length($popup_text);
$popup_text .= $_;
chomp($popup_text);
}
}
close(P);
close(RES);
my $overlib_attr = "";
if ($use_overlib) {
$overlib_attr = qq( onmouseover="return overlib('$popup_text');" onmouseout="return nd();");
}
# present results
unless (defined $points) {
printf "%8s", "-";
print INDEX qq( <td><a href="$res_file_link"$overlib_attr>??</a></td>\n);
next;
}
unless ($opt_s) {
unless (defined $scale) {
$scale = 100 / $points;
}
$points *= $scale;
}
printf "%8.0f", $points;
printf INDEX qq( <td align=right><a href="%s"%s>%.0f</a></td>\n), $res_file_link, $overlib_attr, $points;
$p->{point_sum} += $points;
$p->{no_tests}++;
}
print INDEX " </tr>\n";
print "\n";
}
print "\n";
printf "%-20s", "AVERAGE";
for my $p (@perls) {
printf "%8.0f", $p->{point_sum} / $p->{no_tests};
}
print INDEX " <tr>\n";
print INDEX " <th align=left>Average</th>\n";
for my $p (@perls) {
printf INDEX qq( <td align=right>%.0f</td>\n), $p->{point_sum} / $p->{no_tests};
}
print INDEX " </tr>\n";
print INDEX "</table>\n";
print INDEX "<p><small>Higher numbers are better. 200 is twice as fast as 100.</small></p>\n";
print INDEX "<h2>Configuration summary</h2>\n";
print INDEX "<p>Test ran on a $^O machine";
if ($^O ne "MSWin32") {
my $uname = `uname -a`;
if ($uname) {
print INDEX qq( that reports its uname as ") . htmlesc($uname) . qq(");
}
}
print INDEX ".\n";
print INDEX " Test run completed at " . substr(time2iso(), 11) . ".\n";
print INDEX "</p>\n";
print INDEX "<table border=1>\n";
print INDEX " <tr>\n <th>&nbsp</th>\n";
for my $p (@perls) {
my $h = htmlesc($p->{label});
print INDEX qq( <th><a href="CONFIG-$h.txt">$h</a></th>\n);
}
for my $k ("name", "version", "path") {
print INDEX " <tr>\n <th>$k</th>\n";
for my $p (@perls) {
print INDEX " <td>" . htmlesc($p->{$k}) . "</td>\n";
}
print INDEX " </tr>\n";
}
print INDEX " </tr>\n";
for my $k (sort keys %config_summary) {
print INDEX " <tr>\n <th>" . htmlesc($k) . "</th>\n";
for my $lab (map $_->{label}, @perls) {
my $v = $config_summary{$k}{$lab};
$v = "" unless defined($v);
my $len = length($v);
$v = $len ? htmlesc($v) : "&nbsp;";
$v = "<small>$v</small>" if $len > 40;
print INDEX " <td align=left>$v</td>\n";
}
print INDEX " </tr>\n";
}
print INDEX "</table>\n";
print INDEX "</body>\n</html>\n";
close(INDEX) || die "Can't write $dir/index.html\n";
my $index_url = abs_path($dir);
if ($^O eq "MSWin32") {
$index_url =~ s,\\,/,g;
$index_url =~ s,^([A-Za-z]):,/$1|,;
}
$index_url = "file://$index_url/index.html";
print "\n\nResults saved in $index_url\n";
sub usage
{
$0 =~ s,.*/,,;
die "Usage: $0 [options] [lab1=]<perl1> [lab2=]<perl2>...
if an arg is a directory, 'perl' is appended to it
Recognized options:
-s don't scale numbers (so that first perl is always 100)
-t <filter> only tests that match <filter> regex are timed
-c <cpu-factor> use this factor to scale tests instead of running the
'cpu_factor' program to determine it.
-d <dirname> where to save results
-v verbose - a bit of debug
";
}
sub max
{
my $max = shift;
while (@_) {
my $n = shift;
$max = $n if $n > $max;
}
return $max;
}
sub file {
my $name = shift;
if (@_) {
my $content = shift;
open(my $f, ">", $name) || die "Can't create '$name': $!";
binmode($f);
print $f $content;
close($f) || die "Can't write to '$name': $!";
if (@_) {
my $mode = shift;
change_mode($mode, $name);
}
}
else {
open(my $f, "<", $name) || return undef;
binmode($f);
local $/;
return scalar <$f>;
}
}
sub link_or_copy {
my $f = shift;
link($f, "$dir/$f") || do {
require File::Copy;
File::Copy::copy($f, $f);
}
}
sub htmlesc {
my $str = shift || '';
$str =~ s/&/&amp;/g;
$str =~ s/</&lt;/g;
$str;
}
sub time2iso
{
my $time = shift;
$time = time unless defined $time;
my($sec,$min,$hour,$mday,$mon,$year) = localtime($time);
return sprintf("%04d-%02d-%02d %02d:%02d:%02d",
$year+1900, $mon+1, $mday, $hour, $min, $sec);
}
BEGIN {
package Perl;
use constant OS => $^O;
my $NEXT_LABEL = "A";
sub new
{
my($class, $path) = @_;
my $label;
if ($path =~ s/^(\S+)=//) {
$label = $1;
}
else {
$label = $NEXT_LABEL++;
}
unless (-x $path) {
die "$path is not executable";
next;
}
if (-d $path and -x "$path/perl") {
$path = "$path/perl";
print "updating given dir path to $path\n" if $::opt_v;
}
my $self = bless { path => $path, label => $label }, $class;
$self->run_cmd(*V, '-e', 'print qq(This is perl ), $]+0, qq(\n)');
my $version = <V>;
close V or die "closing pipe from perl: exit code $?";
chomp $version;
unless ($version =~ /^This is perl (\d+.\d+)/) {
die "$path does not appear to be a working perl";
}
$self->{version} = $1;
$self->run_cmd(*V, '-v');
while (<V>) {
if (/^This is perl, v(\S+)/) { # old format
$self->{name} = "perl-$1";
}
if (/^This is perl (\d), version (\d+), subversion (\d+) \((\S+) (?:\((\S+)\){2})?/) {
print "new format: $4 $5\n" if $::opt_v;
$self->{name} = "perl-$1";
$self->{git_version} = $5
}
if (/^Binary build (\d+.*) provided by ActiveState/) {
$self->{name} .= " build $1";
$self->{name} =~ s/^perl/ActivePerl/;
}
}
close(V);
if ($self->{version} >= 5) {
# The perl should have Configure support. Try to extract
# some key settings
my $prog = 'use Config; Config::config_vars(qw(cc ccversion gccversion optimize ccflags usethreads use64bitint use64bitall usemymalloc))';
$self->run_cmd(*CONFIG, '-e', $prog);
while (<CONFIG>) {
next unless /^(\w+)='([^']+)'/; #' #
$self->{config}{$1} = $2;
}
close(CONFIG);
}
return $self;
}
my $ld_path = Cwd::extLibpath() if $^O eq 'os2';
$ld_path .= ';' if $ld_path and $^O eq 'os2';
sub cmd
{
my $self = shift;
my $path = $self->{path};
(my $pdir = $path) =~ s,[/\\][^/\\]+$,/,;
if (-d "$pdir/lib") {
# uninstalled perl
Cwd::extLibpath_set("$ld_path$pdir") if $^O eq 'os2'; # Find DLL
($path, '-I', "$pdir/lib");
} else {
$path;
}
}
sub run_cmd
{
my $self = shift;
my @cmd = $self->cmd;
my $fh = shift;
my @args = map {/\s/ ? OS ne 'MSWin32' ? "'$_'" : "\"$_\"" : $_} @_;
open($fh, "@cmd @args |") or die "Cannot pipe from '@cmd @args': $!";
}
}