bench: Add JSON parsing benchmarks + nicer formatting
This shows I got some optimizing to do. I was expecting integer parsing to be slower, though, but it looks like it can compete with JSON::XS's specialized small-int parsing code anyway.
This commit is contained in:
parent
7cdc02e399
commit
ca8d1b72be
2 changed files with 263 additions and 158 deletions
134
bench.PL
134
bench.PL
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Can be invoked as:
|
||||
# ./bench.PL # generates FU/Benchmarks.pod, running new benchmarks as necessary
|
||||
# ./bench.PL id func # invalidate cache for the (regex-)matching benchmark IDs and funcs and re-run them
|
||||
# ./bench.PL id x y # invalidate cache for the (regex-)matching benchmark IDs, x and y and re-run them
|
||||
#
|
||||
# This script obviously has more dependencies than the FU distribution itself.
|
||||
# It's supposed to be used by maintainers, not users.
|
||||
|
|
@ -23,97 +23,109 @@ my %modules = map +($_, eval "require $_; \$${_}::VERSION"), qw/
|
|||
JSON::SIMD
|
||||
/;
|
||||
|
||||
my %data; # "id func modver" => { id func module modver rate exists }
|
||||
{
|
||||
my %data; # "id x y" => { id x y rate exists }
|
||||
my %oldmodules;
|
||||
{ if (open my $F, '<', 'FU/Benchmarks.pod') {
|
||||
my $indata;
|
||||
if (open my $F, '<', 'FU/Benchmarks.pod') {
|
||||
while (<$F>) {
|
||||
chomp;
|
||||
$indata = 1 if /^# Cached data used by bench\.PL/;
|
||||
next if !$indata || !$_ || /^#/;
|
||||
my %d;
|
||||
@d{qw/id func module modver rate/} = split /\t/;
|
||||
$data{"$d{id} $d{func} $d{modver}"} = \%d;
|
||||
}
|
||||
while (<$F>) {
|
||||
chomp;
|
||||
$oldmodules{$1} = $2 if /^=item L<([a-zA-Z0-9:]+)> ([0-9.]+)/;
|
||||
$indata = 1 if /^# Cached data used by bench\.PL/;
|
||||
next if !$indata || !$_ || /^#/;
|
||||
my %d;
|
||||
@d{qw/id x y rate/} = split /\t/;
|
||||
$data{"$d{id} $d{x} $d{y}"} = \%d;
|
||||
}
|
||||
}
|
||||
} }
|
||||
|
||||
if (@ARGV) {
|
||||
my $idre = qr/$ARGV[0]/i;
|
||||
my $funcre = $ARGV[1] ? qr/$ARGV[1]/i : qr/.*/;
|
||||
delete $_->{rate} for grep $_->{id} =~ /$idre/ && $_->{func} =~ /$funcre/, values %data;
|
||||
my $xre = $ARGV[1] ? qr/$ARGV[1]/i : qr/.*/;
|
||||
my $yre = $ARGV[2] ? qr/$ARGV[2]/i : qr/.*/;
|
||||
delete $_->{rate} for grep $_->{id} =~ /$idre/ && $_->{x} =~ /$xre/ && $_->{y} =~ /$yre/, values %data;
|
||||
}
|
||||
|
||||
|
||||
my @bench; # [ id, text, [ func_1, funcmodule_1, funcsub_n, .. ] ]
|
||||
sub def($id, $text, @f) {
|
||||
for my ($f, $m, $sub) (@f) {
|
||||
$m ||= $f;
|
||||
my $d = "$id $f $modules{$m}";
|
||||
$data{$d} ||= { id => $id, func => $f, module => $m, modver => $modules{$m} };
|
||||
$d = $data{$d};
|
||||
$d->{exists} = 1;
|
||||
if (!exists $d->{rate}) {
|
||||
my $o = timethis -1, $sub, 0, 'none';
|
||||
$d->{rate} = sprintf '%.0f', $o->iters/$o->real;
|
||||
printf "%-20s%-20s%10d/s\n", $d->{id}, $d->{func}, $d->{rate};
|
||||
my @bench; # [ id, text, [ x_1, .. ], [ [ y_1, mod_1, sub_1, .. ], .. ] ]
|
||||
sub def($id, $text, $xs, @ys) {
|
||||
for my ($ya) (@ys) {
|
||||
my($y, $m, @sub) = @$ya;
|
||||
$m ||= $y;
|
||||
for my($i, $x) (builtin::indexed @$xs) {
|
||||
my $d = "$id $x $y";
|
||||
$data{$d} ||= { id => $id, x => $x, y => $y };
|
||||
$d = $data{$d};
|
||||
$d->{exists} = 1;
|
||||
delete $d->{rate} if !$oldmodules{$m} || $modules{$m} ne $oldmodules{$m};
|
||||
if (!exists $d->{rate}) {
|
||||
my $o = timethis -1, $sub[$i], 0, 'none';
|
||||
$d->{rate} = sprintf '%.0f', $o->iters/$o->real;
|
||||
printf "%-20s%-12s%-20s%10d/s\n", $id, $x, $y, $d->{rate};
|
||||
}
|
||||
}
|
||||
}
|
||||
push @bench, [ $id, $text, \@f ];
|
||||
push @bench, [ $id, $text, $xs, \@ys ];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
use FU::Util 'json_format';
|
||||
use FU::Util 'json_format', 'json_parse';
|
||||
|
||||
sub jsonfmt($name, $text, $data) {
|
||||
sub defjson($name, $canon, $text, $val) {
|
||||
# Use similar options for fair comparisons.
|
||||
my $cp = Cpanel::JSON::XS->new->allow_nonref->unblessed_bool->convert_blessed;
|
||||
my $pp = JSON::PP->new->allow_nonref->core_bools->convert_blessed;
|
||||
my $xs = JSON::XS->new->allow_nonref->boolean_values([false,true])->convert_blessed;
|
||||
my $si = JSON::SIMD->new->allow_nonref->core_bools->convert_blessed;
|
||||
my @opt = ();
|
||||
if ($name =~ /^canon/) {
|
||||
$cp = $cp->canonical;
|
||||
$pp = $pp->canonical;
|
||||
$xs = $xs->canonical;
|
||||
$si = $si->canonical;
|
||||
@opt = (canonical => 1);
|
||||
}
|
||||
def "jsonfmt/$name", $text,
|
||||
'JSON::PP', undef, sub { $pp->encode($data) },
|
||||
'Cpanel::JSON::XS', undef, sub { $cp->encode($data) },
|
||||
'JSON::SIMD', undef, sub { $si->encode($data) },
|
||||
'JSON::XS', undef, sub { $xs->encode($data) },
|
||||
'FU::Util', 'FU', sub { json_format $data, @opt };
|
||||
my $c_cp = Cpanel::JSON::XS->new->allow_nonref->unblessed_bool->convert_blessed->canonical;
|
||||
my $c_pp = JSON::PP->new->allow_nonref->core_bools->convert_blessed->canonical;
|
||||
my $c_xs = JSON::XS->new->allow_nonref->boolean_values([false,true])->convert_blessed->canonical;
|
||||
my $c_si = JSON::SIMD->new->allow_nonref->core_bools->convert_blessed->canonical;
|
||||
my $enc = json_format $val;
|
||||
def "json/$name", $text, [ 'Encode', $canon ? 'Canonical' : (), 'Decode' ],
|
||||
[ 'JSON::PP', undef, sub { $pp->encode($val) }, $canon ? sub { $c_pp->encode($val) } : (), sub { $pp->decode($enc) } ],
|
||||
[ 'Cpanel::JSON::XS', undef, sub { $cp->encode($val) }, $canon ? sub { $c_cp->encode($val) } : (), sub { $cp->decode($enc) } ],
|
||||
[ 'JSON::SIMD', undef, sub { $si->encode($val) }, $canon ? sub { $c_si->encode($val) } : (), sub { $si->decode($enc) } ],
|
||||
[ 'JSON::XS', undef, sub { $xs->encode($val) }, $canon ? sub { $c_xs->encode($val) } : (), sub { $xs->decode($enc) } ],
|
||||
[ 'FU::Util', 'FU', sub { json_format $val }, $canon ? sub { json_format $val, canonical => 1 } : (), sub { json_parse $enc } ];
|
||||
}
|
||||
|
||||
# From JSON::XS POD.
|
||||
jsonfmt api => 'API object from L<JSON::XS> documentation.',
|
||||
defjson api => 1, 'API object from L<JSON::XS> documentation.',
|
||||
[ map +{method => 'handleMessage', params => ['user1','we were just talking'], 'id' => undef, 'array' => [1,11,234,-5,1e5,1e7,1,0]}, 1..10 ];
|
||||
|
||||
jsonfmt ints => 'Small integers', [ -5000..5000 ];
|
||||
jsonfmt intl => 'Large integers', [ map { my $n=$_; map +($n+1<<$_), 10..60 } 1..10 ];
|
||||
jsonfmt strs => 'ASCII strings', [ map +('hello, world', 'one more string', 'another string'), 1..100 ];
|
||||
jsonfmt stru => 'Unicode strings', do { use utf8;
|
||||
defjson objs => 1, 'Object (small)', [ map +{ map +("string$_", 1), 'a'..'f' }, 0..100 ];
|
||||
defjson objl => 1, 'Object (large)', { map +("string$_-something", 1), 'aa'..'zz' };
|
||||
defjson obju => 1, 'Object (large, mixed unicode)', { map +("str\x{1234}g$_-some\x{85232}hing", 1), 'aa'..'zz' };
|
||||
|
||||
defjson ints => 0, 'Small integers', [ -5000..5000 ];
|
||||
defjson intl => 0, 'Large integers', [ map { my $n=$_; map +($n+1<<$_), 10..60 } 1..10 ];
|
||||
defjson strs => 0, 'ASCII strings', [ map +('hello, world', 'one more string', 'another string'), 1..100 ];
|
||||
defjson stru => 0, 'Unicode strings', do { use utf8;
|
||||
[ map +('グリザイアの果実 -LE FRUIT DE LA GRISAIA-', '💩', 'Я люблю нічого не робити'), 1..50 ];
|
||||
};
|
||||
jsonfmt stres => 'String escaping (few)', [ map 'This string needs to "be escaped" a little bit', 1..100 ];
|
||||
jsonfmt strel => 'String escaping (many)', [ map "This \" \\ needs \b\x01\x02\x03\x04 more", 1..100 ];
|
||||
|
||||
jsonfmt canons => 'Canonical hash key ordering (small)', [ map +{ map +("string$_", 1), 'a'..'f' }, 0..100 ];
|
||||
jsonfmt canonl => 'Canonical hash key ordering (large)', { map +("string$_-something", 1), 'aa'..'zz' };
|
||||
defjson stres => 0, 'String escaping (few)', [ map 'This string needs to "be escaped" a little bit', 1..100 ];
|
||||
defjson strel => 0, 'String escaping (many)', [ map "This \" \\ needs \b\x01\x02\x03\x04 more", 1..100 ];
|
||||
|
||||
|
||||
|
||||
delete @data{ grep !$data{$_}{exists}, keys %data };
|
||||
|
||||
sub fmtbench($id, $text, $fs) {
|
||||
sub fmtbench($id, $text, $xs, $ys) {
|
||||
my $r = "$text\n\n";
|
||||
for my ($f, $m, $sub) (@$fs) {
|
||||
$m ||= $f;
|
||||
$r .= sprintf "%18s%10d/s\n", $f, $data{"$id $f $modules{$m}"}{rate};
|
||||
if (@$xs > 1) {
|
||||
$r .= sprintf '%18s', '';
|
||||
$r .= sprintf '%12s', $_ for @$xs;
|
||||
$r .= "\n";
|
||||
}
|
||||
for my ($n, $yr) (builtin::indexed @$ys) {
|
||||
my $x = $xs->[$n];
|
||||
my ($y, $m, @ys) = @$yr;
|
||||
$m ||= $y;
|
||||
$r .= sprintf '%18s', $y;
|
||||
$r .= sprintf '%10d/s', $data{"$id $xs->[$_] $y"}{rate} for (0..$#$xs);
|
||||
$r .= "\n";
|
||||
}
|
||||
"$r\n"
|
||||
}
|
||||
|
|
@ -128,7 +140,7 @@ sub fmtbench($id, $text, $fs) {
|
|||
}
|
||||
for (sort keys %data) {
|
||||
my $b = $data{$_};
|
||||
print join("\t", @{$b}{qw/ id func module modver rate /})."\n";
|
||||
print join("\t", @{$b}{qw/ id x y rate /})."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -173,8 +185,8 @@ smaller inputs, but I don't find that overhead very interesting.
|
|||
Also worth noting that JSON::SIMD formatting code is forked from JSON::XS, the
|
||||
SIMD parts are only used for parsing.
|
||||
|
||||
:benches ^jsonfmt
|
||||
:benches ^json
|
||||
|
||||
=cut
|
||||
|
||||
# Cached data used by bench.PL.
|
||||
# Cached data used by bench.PL. Same as the formatted tables above but easier to parse.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue