Perl advanced sorting
last modified November 7, 2026
The sort function takes a block that compares two elements,
available as $a and $b, and returns a negative number,
zero, or a positive number. The basics, string and numeric order and sorting
records by one field, are covered in the Perl sort
article. This article goes further: sorting by several keys, writing reusable
comparison subroutines, natural order, and the classic idioms for sorting by
keys that are expensive to compute: the Schwartzian transform, the Orcish
maneuver, and the Guttman-Rosler transform. A benchmark at the end compares
them.
The examples were run with Perl 5.44 on Linux.
Sorting by several keys
To sort by more than one key, compare by the first key and fall back to the next
one when the first comparison returns zero. The low-precedence
or operator expresses this directly.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my @staff = (
{ name => 'Martin', dept => 'sales', salary => 2400 },
{ name => 'Eva', dept => 'dev', salary => 3100 },
{ name => 'Adam', dept => 'sales', salary => 2400 },
{ name => 'Zuzana', dept => 'dev', salary => 3800 },
{ name => 'Peter', dept => 'ops', salary => 2900 },
{ name => 'Lenka', dept => 'sales', salary => 2750 },
);
my @sorted = sort {
$a->{dept} cmp $b->{dept}
or $b->{salary} <=> $a->{salary}
or $a->{name} cmp $b->{name}
} @staff;
printf "%-6s %-7s %d\n", @$_{qw(dept name salary)} for @sorted;
The staff are sorted by department in ascending order, then by salary in
descending order, then by name. The comparison operators cmp and
<=> return 0 for equal values, so or moves on
to the next comparison only for ties. Swapping $a and
$b in one comparison reverses the direction of that key alone, as
for the salary. Adam and Martin earn the same, so their names decide.
$ perl multikey.pl dev Zuzana 3800 dev Eva 3100 ops Peter 2900 sales Lenka 2750 sales Adam 2400 sales Martin 2400
Named comparison subroutines
A comparison that is used more than once can be put in a named subroutine and
passed to sort by name.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my @words = qw(banana Apple cherry apple Banana date);
sub by_name {
fc($a) cmp fc($b) or $a cmp $b
}
sub by_length_desc {
length($b) <=> length($a) or by_name()
}
say join ' ', sort by_name @words;
say join ' ', sort by_length_desc @words;
say join ' ', reverse sort by_name @words;
my $order = \&by_name;
say join ' ', sort $order @words;
The by_name subroutine sorts case-insensitively with
fc, the case-folding function, and breaks ties with a
case-sensitive comparison, so Apple always comes before
apple. The sort subroutine reads the package variables
$a and $b rather than arguments, which is why
by_length_desc can call by_name() to break ties.
Putting reverse in front of sort reverses the whole
result; Perl recognises this combination and does not build an intermediate
list. A variable holding a code reference can also be used in place of the
name.
$ perl named.pl Apple apple Banana banana cherry date Banana banana cherry Apple apple date date cherry banana Banana apple Apple Apple apple Banana banana cherry date
Sort stability
A sort is stable when elements that compare equal keep their original order.
Perl's sort uses a merge sort, which is stable.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my @tasks = (
[ 'write report', 2 ],
[ 'call Peter', 1 ],
[ 'fix bug 17', 3 ],
[ 'order paper', 2 ],
[ 'backup', 1 ],
[ 'review PR', 2 ],
);
my @by_priority = sort { $a->[1] <=> $b->[1] } @tasks;
say "$_->[1] $_->[0]" for @by_priority;
The tasks are sorted by priority only. Tasks with the same priority stay in the
order in which they were listed: write report, order
paper, review PR. Stability allows sorting in several
passes, from the least important key to the most important, although a single
multi-key comparison is usually clearer.
$ perl stable.pl 1 call Peter 1 backup 2 write report 2 order paper 2 review PR 3 fix bug 17
Natural sort
String comparison puts file10 before file2, because
the character 1 is smaller than 2. A natural sort compares the embedded numbers
by value, as people expect.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my @files = qw(file10.txt file2.txt File1.txt file1.txt file20.txt file3.txt img12.png img9.png);
sub natural_key {
my $s = lc shift;
$s =~ s/(\d+)/sprintf '%09d', $1/ge;
return $s;
}
say 'plain: ', join ' ', sort @files;
say 'natural: ', join ' ', map { $_->[1] }
sort { $a->[0] cmp $b->[0] or $a->[1] cmp $b->[1] }
map { [natural_key($_), $_] } @files;
The natural_key function lowercases the string and pads every run
of digits with zeros to nine places, so that a plain string comparison orders
the numbers correctly: file000000002.txt sorts before
file000000010.txt. The keys are computed once per element and
sorted together with the original strings, a technique explained in the next
section. The tie-breaker keeps File1.txt and
file1.txt, which have the same key, in a defined order.
$ perl natural.pl plain: File1.txt file1.txt file10.txt file2.txt file20.txt file3.txt img12.png img9.png natural: File1.txt file1.txt file2.txt file3.txt file10.txt file20.txt img9.png img12.png
The Schwartzian transform
A sort block runs many times for each element, roughly n log n comparisons in total. If the sort key must be extracted from the element, for example parsed from a string, the extraction is repeated in every comparison. The Schwartzian transform, named after Randal Schwartz, computes each key only once.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my @events = (
'Concert;14.03.2027',
'Exhibition;02.11.2026',
'Marathon;28.09.2027',
'Workshop;02.11.2026',
'Festival;30.06.2027',
);
my @sorted =
map { $_->[0] }
sort { $a->[1] cmp $b->[1] or $a->[0] cmp $b->[0] }
map {
my ($d, $m, $y) = (split /;/)[1] =~ /(\d+)\.(\d+)\.(\d+)/;
[ $_, "$y$m$d" ];
} @events;
say for @sorted;
The pipeline reads from bottom to top. The first map turns each
event into a pair of the original string and a key, here the date rearranged
into the sortable form YYYYMMDD. The sort compares the
precomputed keys, falling back to the string for events on the same day. The
last map drops the keys and returns the original strings. The cost
is the creation of one small array per element, which pays off as soon as the
key is anything more than a hash lookup.
$ perl schwartzian.pl Exhibition;02.11.2026 Workshop;02.11.2026 Concert;14.03.2027 Festival;30.06.2027 Marathon;28.09.2027
The Orcish maneuver
The Orcish maneuver, a pun on or-cache
, keeps the ordinary sort block
but caches the keys in a hash. The //= operator computes a key the
first time it is needed and reuses it afterwards.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $calls = 0;
sub score {
my $word = shift;
$calls++;
return scalar(() = $word =~ /[aeiou]/gi);
}
my @words = qw(perl sort orcish maneuver schwartzian transform key cache);
$calls = 0;
my @plain = sort { score($b) <=> score($a) or $a cmp $b } @words;
say "plain: $calls calls";
$calls = 0;
my %cache;
my @orcish = sort {
($cache{$b} //= score($b)) <=> ($cache{$a} //= score($a))
or $a cmp $b
} @words;
say "orcish: $calls calls";
say "$_ ($cache{$_})" for @orcish;
The score function counts the vowels in a word and counts its own
calls. The plain sort calls it 30 times for 8 words; with the cache, it is
called exactly once per word. The cache is a hash keyed by the elements, so the
technique works only for strings or for elements whose string form is unique;
the Schwartzian transform has no such restriction. The original form of the
idiom used ||=, which recomputes keys that are 0 or empty; the
//= operator avoids that.
$ perl orcish.pl plain: 30 calls orcish: 8 calls maneuver (4) schwartzian (3) cache (2) orcish (2) transform (2) key (1) perl (1) sort (1)
The Guttman-Rosler transform
The Guttman-Rosler transform goes one step further: it packs the key and the element into a single string, so the sort needs no block at all. Perl's default string sort, without a comparison block, runs entirely in C and is the fastest sort available.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my @people = ('Kovac 1987', 'Novak 2001', 'Horvath 1987', 'Balaz 1995', 'Novak 1979');
my @sorted =
map { substr $_, 4 }
sort
map { my ($name, $year) = split; pack('N', $year) . $_ } @people;
say for @sorted;
The year is packed with pack('N', ...) into four bytes in
big-endian order, which sort correctly as a string, and is prepended to the
original element. After the default sort, substr
removes the first four bytes. People born in the same year are ordered by the
rest of the string, so Horvath comes before Kovac. The transform is fast but
demands care: every key must be encoded so that its byte order matches the
desired order, which is easy for non-negative integers and fixed-width strings
and harder for negative or floating-point numbers and descending order.
$ perl grt.pl Novak 1979 Horvath 1987 Kovac 1987 Balaz 1995 Novak 2001
sort, map, and grep pipelines
Since sort, map, and grep all take and
return lists, they combine into pipelines that filter, transform, and order
data in a single expression.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
my $text = <<'END';
the quick brown fox jumps over the lazy dog the fox
sleeps and the dog barks at the quick fox
END
my %freq;
$freq{$_}++ for split ' ', $text;
my @top = (sort { $freq{$b} <=> $freq{$a} or $a cmp $b } keys %freq)[0 .. 4];
say "$_: $freq{$_}" for @top;
my %seen;
say join ' ', sort grep { !$seen{$_}++ } map { length } split ' ', $text;
say join ' ', map { "$_->[0]=$_->[1]" }
sort { $b->[1] <=> $a->[1] or $a->[0] cmp $b->[0] }
grep { $_->[1] > 1 }
map { [$_, $freq{$_}] } keys %freq;
The first pipeline sorts the keys of a frequency hash by value and takes the
top five with a list slice. The second extracts the distinct word lengths:
map computes the lengths, grep with a
%seen hash removes duplicates, and sort orders them;
the default string sort is fine here because all lengths have one digit. The
third builds pairs, keeps the words that occur more than once, sorts them by
count and name, and formats the result. As in the Schwartzian transform, such
pipelines are read from right to left.
$ perl pipeline.pl the: 5 fox: 3 dog: 2 quick: 2 and: 1 2 3 4 5 6 the=5 fox=3 dog=2 quick=2
Benchmarking the techniques
The core Benchmark module measures how fast pieces of code run. We
sort 5,000 dates in the DD.MM.YYYY format with each of the four
approaches.
#!/usr/bin/perl
use strict;
use warnings;
use v5.34.0;
use Benchmark qw(cmpthese);
srand 42;
my @dates = map {
sprintf '%02d.%02d.%04d', 1 + int rand 28, 1 + int rand 12, 1990 + int rand 40
} 1 .. 5000;
sub key_of {
my ($d, $m, $y) = $_[0] =~ /(\d+)\.(\d+)\.(\d+)/;
return "$y$m$d";
}
my %subs = (
naive => sub {
my @s = sort { key_of($a) cmp key_of($b) } @dates;
return @s;
},
orcish => sub {
my %c;
my @s = sort { ($c{$a} //= key_of($a)) cmp ($c{$b} //= key_of($b)) } @dates;
return @s;
},
schwartzian => sub {
my @s = map { $_->[1] } sort { $a->[0] cmp $b->[0] } map { [key_of($_), $_] } @dates;
return @s;
},
grt => sub {
my @s = map { substr $_, 8 } sort map { key_of($_) . $_ } @dates;
return @s;
},
);
my @expected = sort { key_of($a) cmp key_of($b) } @dates;
for my $name (sort keys %subs) {
my @got = $subs{$name}->();
die "$name sorts differently\n" unless "@got" eq "@expected";
}
cmpthese(-2, \%subs);
Before measuring, the program checks that all four subroutines produce the same
order as the straightforward sort. The cmpthese function runs each
subroutine for at least two CPU seconds and prints a table of rates and
relative speeds. Each subroutine assigns the result to an array. This matters:
cmpthese does not call the code in list context, and the behaviour
of sort outside list context is undefined. Written as
return sort { ... } @dates, the naive and Orcish variants did no
sorting at all and reported impossibly high rates.
The naive sort extracts the key with a regular expression in every comparison and is about eight times slower than the Orcish maneuver. The Schwartzian transform is a bit faster than the cache, and the Guttman-Rosler transform, which sorts without a comparison block, is the fastest. The exact numbers depend on the machine and the data, but with an expensive key, any of the three techniques beats the naive sort by an order of magnitude.
$ perl bench.pl
Rate naive orcish schwartzian grt
naive 14.2/s -- -88% -89% -92%
orcish 116/s 714% -- -13% -39%
schwartzian 133/s 836% 15% -- -30%
grt 189/s 1227% 63% 42% --
Best practices
- Chain keys with or: write multi-key comparisons as a
sequence of
cmpand<=>joined byor, and reverse single keys by swapping$aand$b. - Use the right operator:
<=>for numbers,cmpfor strings, andfcfor case-insensitive order. - Precompute expensive keys: use the Schwartzian transform or the Orcish maneuver when the key requires parsing or computation.
- Use the GRT for hot paths: pack keys into strings only when profiling shows that sorting matters; it is the hardest variant to read.
- Add a final tie-breaker: a last comparison on a unique field makes the order fully deterministic.
- Benchmark in list context: assign the result of
sortto an array, otherwise the sort may not run at all.
In this article we have sorted by several keys, written named comparison
subroutines, relied on sort stability, implemented a natural sort, used the
Schwartzian transform, the Orcish maneuver, and the Guttman-Rosler transform,
combined sort with map and grep, and
compared the techniques with Benchmark.
Source
perlfunc: sort · perlfaq4: How do I sort an array by (anything)? · Benchmark
Author
List all Perl tutorials.