Choosing Between Perl's Built-in Sort and the Schwartzian Transform
A decision guide for Perl sorting: when the plain built-in sort is enough and when the Schwartzian transform pays off, with code, benchmarks, and memory trade-offs.
21 May 2026, 02:33 UTC

The Decision
You need to sort a list where the sort key is not the element itself but the result of a function that may be expensive to compute — a regex extraction, a database lookup, or a non-trivial calculation. The question is whether to let Perl's built-in sort recompute that key on every comparison, or to pre-compute each key once using the Schwartzian transform. The short answer: if the key function is cheap, keep the plain sort; if it is costly and the list is large, the transform usually wins by a wide margin.
Constraints to weigh
- Readability: the plain sort is immediately obvious; the transform is a well-known idiom but still denser.
- Performance: a comparison-based sort performs roughly
O(n log n)comparisons, and the naive form calls the key function twice per comparison. - Memory: the transform allocates one temporary two-element array per item.
- Data size: for small lists the difference rarely matters.
Option Comparison
| Option | When to use | Pros | Cons |
|---|---|---|---|
Built-in sort |
Key extraction is cheap (field access, simple substring) and the dataset is small to medium. | Minimal code, no extra data structures, fast when keys are inexpensive. | Recomputes the key on every comparison; becomes a bottleneck when the key function is costly. |
| Schwartzian transform | Key extraction is expensive (regex, I/O, computation) and the dataset is large enough that repeated cost matters. | Computes the key exactly once per element; often 2–3× faster for costly keys. | Extra memory for temporary arrays; slightly more syntactic complexity. |
Why the Trade-off Works
The naive form, sort { expensive_key($a) cmp expensive_key($b) } @data, invokes expensive_key twice per comparison. For 100,000 elements that can mean well over a million key computations. The Schwartzian transform decorates each element with its precomputed key, sorts on the key, then strips the decoration — so the key function runs exactly n times. You pay one extra array reference per element in exchange for eliminating all redundant key work.
Concrete Implementation
Run this in any Perl script with a standard Perl 5 installation (5.10+ is fine; no special permissions needed). The example sorts strings by a key derived from a regex match, standing in for a genuinely expensive operation.
use strict;
use warnings;
my @data = map { "record_$_" . ('x' x (50 + $_ % 20)) } 1..10_000;
sub expensive_key {
my $s = shift;
# Simulate a costly extraction
$s =~ /^(\w+?)(x+)$/ or return $s;
return length($2); # sort by trailing-x count
}
# Naive: key recomputed on every comparison
my @naive = sort { expensive_key($a) <=> expensive_key($b) } @data;
# Schwartzian transform: key computed once per element
my @schwartz = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [$_, expensive_key($_)] } @data;
# Correctness check: both orderings must match
use List::Util qw(all);
my $same = all { $naive[$_] eq $schwartz[$_] } 0..$#naive;
print "orderings match: ", ($same ? "yes" : "NO"), "\n";
Read the transform bottom-up: the inner map builds [original, key] pairs, sort compares the second slot, and the outer map discards the keys. Note the use of <=> for numeric keys; use cmp for string keys.
Validating the Performance Claim
Do not take the speedup on faith — measure it on your data with the core Benchmark module. The -2 argument runs each candidate for at least two CPU seconds.
use Benchmark qw(timethese);
timethese(-2, {
naive => sub { my @x = sort { expensive_key($a) <=> expensive_key($b) } @data; },
schwartz => sub { my @x = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [$_, expensive_key($_)] } @data; },
});
Compare the reported wall-clock rates. With a genuinely expensive key you should see the transform complete noticeably more iterations per second; with a trivial key the two will be close, and the naive form may even edge ahead. If you want to confirm the memory cost, install Devel::Size from CPAN and call size(\@tmp) on the intermediate decorated array — expect roughly one extra array reference per element.
Limitations and Edge Cases
- Very large datasets: if the temporary arrays push you into swap, consider reducing key complexity or sorting externally (e.g., the Unix
sortutility with a precomputed key column). - Side effects: the transform calls the key function exactly once per element. If your key function mutates state or returns different values on repeated calls (like the
rand-based example sometimes used in tutorials), the naive sort and the transform can produce different orderings — make the key function pure and idempotent. - Small lists: below roughly a thousand elements, the readability of the plain sort usually outweighs the modest gain.
- Stability: Perl's
sorthas been stable by default since 5.8, and this applies equally to both forms, so equal keys retain input order in either approach.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.