, 8 min read
Performance Comparison: gzip vs Brotli vs Zstd
This is a continuation of the post Performance Comparison: gzip vs Brotli three years ago. Here we will present measurements for three compression algorithms (gzip, brotli, and zstd) employed on two different hardware architectures (AMD and ARM). The measurements cover all possible compression levels. Input data are all HTML files used on this blog. These were 793 HTML files in total.
1. NGINX and compression
The NGINX web-server offers HTTP compression. The compression algorithms in question are:
My current nginx.conf file uses
gzip on;
brotli on;
gzip_comp_level 9;
brotli_comp_level 10;
gzip_min_length 512;
brotli_min_length 512;
2. AMD Ryzen 7 5700G
For the measurements I used below hardware and software:
| Type | Value |
|---|---|
| CPU | AMD Ryzen 7 5700G |
| Clock | 4673.8232 MHz |
| RAM | 64GB DDR4-3600 |
| OS | Arch Linux kernel 7.1.8-arch1-3 |
| gzip | 1.14-modified |
| brotli | 1.2.0 |
| zstd | (64-bit) v1.5.7, by Yann Collet |
Running the compression programs for all permissable compression levels on this desktop machine.
All files were stored in /tmp, i.e., they were all in a RAM disk.
All compressed files were also written to /tmp.
So processing time is not distorted by file I/O.
In total there were 793 HTML files, with roughly 47 MB of data.
The individual HTML files, of course, were smaller.
All runtime is given is seconds.
| compr. level |
brotli size |
real | user | gzip size |
real | user | zstd size |
real | user |
|---|---|---|---|---|---|---|---|---|---|
| 47,800,644 | 47,800,644 | 47,800,644 | |||||||
| 0 | 18,637,258 | 0.14 | 0.10 | n/a | n/a | n/a | n/a | n/a | n/a |
| 1 | 18,042,822 | 0.18 | 0.14 | 17,730,989 | 0.40 | 0.36 | 17,479,236 | 0.15 | 0.09 |
| 2 | 16,396,160 | 0.33 | 0.29 | 17,314,433 | 0.43 | 0.40 | 16,798,801 | 0.17 | 0.12 |
| 3 | 16,252,214 | 0.39 | 0.35 | 17,117,991 | 0.50 | 0.46 | 16,653,562 | 0.21 | 0.16 |
| 4 | 15,397,998 | 0.66 | 0.61 | 16,328,922 | 0.58 | 0.55 | 16,289,749 | 0.26 | 0.20 |
| 5 | 14,729,521 | 0.93 | 0.87 | 15,965,453 | 0.81 | 0.77 | 15,900,141 | 0.40 | 0.34 |
| 6 | 14,708,705 | 0.98 | 0.89 | 15,920,931 | 0.97 | 0.93 | 15,597,981 | 0.50 | 0.45 |
| 7 | 14,695,823 | 1.04 | 1.00 | 15,906,130 | 1.03 | 1.00 | 15,532,238 | 0.60 | 0.54 |
| 8 | 14,690,162 | 1.08 | 1.03 | 15,891,227 | 1.24 | 1.21 | 15,486,025 | 0.64 | 0.59 |
| 9 | 14,680,471 | 1.63 | 1.21 | 15,889,892 | 1.32 | 1.29 | 15,475,953 | 0.69 | 0.65 |
| 10 | 13,106,789 | 16.31 | 16.20 | n/a | n/a | n/a | 15,469,574 | 0.79 | 0.73 |
| 11 | 12,938,604 | 44.50 | 44.25 | n/a | n/a | n/a | 15,448,926 | 1.13 | 1.06 |
| 12 | n/a | n/a | n/a | n/a | n/a | n/a | 15,445,040 | 1.16 | 1.10 |
| 13 | n/a | n/a | n/a | n/a | n/a | n/a | 15,176,152 | 2.08 | 2.01 |
| 14 | n/a | n/a | n/a | n/a | n/a | n/a | 14,990,965 | 2.74 | 2.67 |
| 15 | n/a | n/a | n/a | n/a | n/a | n/a | 14,947,573 | 3.13 | 3.05 |
| 16 | n/a | n/a | n/a | n/a | n/a | n/a | 14,724,958 | 4.86 | 4.78 |
| 17 | n/a | n/a | n/a | n/a | n/a | n/a | 14,723,797 | 5.23 | 5.15 |
| 18 | n/a | n/a | n/a | n/a | n/a | n/a | 14,727,022 | 6.05 | 5.96 |
| 19 | n/a | n/a | n/a | n/a | n/a | n/a | 14,688,864 | 10.23 | 10.15 |
The above table is depicted graphically below. Size is scaled by 1,000,000 in the graph below so that runtimes and size can be shown in the same graph.
One can clearly see the steep slope for brotli at level 10. At level 15 zstd starts to become noticeably slower.
3. ARM Cortex A720
Next, I used below hardware and software:
| Type | Value |
|---|---|
| CPU | ARM Dimensity 9300+ Cortex A720 |
| Clock | 2000 MHz |
| RAM | 12GB |
| OS | 6.1.138-android14-11-g6ab8c9a86a33-ab14396278 |
| gzip | 1.14 |
| brotli | 1.2.0 |
| zstd | (64-bit) v1.5.7, by Yann Collet |
Running the compression programs for all permissable compression levels on this smartphone.
| compr. level |
brotli size |
real | user | gzip size |
real | user | zstd size |
real | user |
|---|---|---|---|---|---|---|---|---|---|
| 47,800,644 | 47,800,644 | 47,800,644 | |||||||
| 0 | 18,637,258 | 0.47 | 0.19 | n/a | n/a | n/a | n/a | n/a | n/a |
| 1 | 18,042,822 | 0.33 | 0.27 | 17,730,989 | 1.10 | 0.74 | 17,479,236 | 0.44 | 0.17 |
| 2 | 16,396,160 | 0.50 | 0.43 | 17,314,433 | 0.79 | 0.72 | 16,798,801 | 0.29 | 0.17 |
| 3 | 16,252,214 | 0.55 | 0.44 | 17,117,991 | 0.92 | 0.82 | 16,653,562 | 0.33 | 0.22 |
| 4 | 15,397,998 | 0.84 | 0.72 | 16,328,922 | 1.04 | 0.93 | 16,289,749 | 0.38 | 0.27 |
| 5 | 14,729,521 | 1.28 | 1.16 | 15,965,453 | 1.49 | 1.25 | 15,900,141 | 0.60 | 0.47 |
| 6 | 14,708,705 | 1.44 | 1.24 | 15,920,931 | 1.75 | 1.46 | 15,597,981 | 0.75 | 0.60 |
| 7 | 14,695,823 | 1.76 | 1.29 | 15,906,130 | 1.90 | 1.58 | 15,532,238 | 0.90 | 0.77 |
| 8 | 14,690,162 | 2.38 | 1.46 | 15,891,227 | 2.30 | 2.03 | 15,486,025 | 0.98 | 0.81 |
| 9 | 14,680,471 | 3.47 | 1.76 | 15,889,892 | 2.43 | 2.15 | 15,475,953 | 1.09 | 0.93 |
| 10 | 13,106,789 | 26.50 | 25.45 | n/a | n/a | n/a | 15,469,574 | 1.22 | 1.09 |
| 11 | 12,938,604 | 70.72 | 68.20 | n/a | n/a | n/a | 15,448,926 | 1.80 | 1.63 |
| 12 | n/a | n/a | n/a | n/a | n/a | n/a | 15,445,040 | 1.87 | 1.69 |
| 13 | n/a | n/a | n/a | n/a | n/a | n/a | 15,176,152 | 3.03 | 2.81 |
| 14 | n/a | n/a | n/a | n/a | n/a | n/a | 14,990,965 | 3.88 | 3.65 |
| 15 | n/a | n/a | n/a | n/a | n/a | n/a | 14,947,573 | 4.29 | 4.09 |
| 16 | n/a | n/a | n/a | n/a | n/a | n/a | 14,724,958 | 5.89 | 5.68 |
| 17 | n/a | n/a | n/a | n/a | n/a | n/a | 14,723,797 | 6.28 | 6.02 |
| 18 | n/a | n/a | n/a | n/a | n/a | n/a | 14,727,022 | 7.16 | 6.88 |
| 19 | n/a | n/a | n/a | n/a | n/a | n/a | 14,688,864 | 11.71 | 11.37 |
Depicted graphically.
Qualitatively the picture is pretty similar to the AMD chart.
4. Automating the measurements
Three years ago I made the measurements in a manual way. This time it was clear I needed a script to run the measurements.
The method is the same as three years ago:
- Use
findto list all HTML files - Use
wc -cto count characters - Run each compression algorithm and iterate over the compression levels
- Overwrite previous compressed files
Below is the Perl script.
#!/bin/perl -W
# Performance comparison gzip vs. brotli vs zstd
use strict;
use Getopt::Std;
my %opts = ();
getopts('dk:',\%opts);
my $debug = (defined($opts{'d'}) ? 1 : 0);
my $scale = (defined($opts{'k'}) ? $opts{'k'} : 1);
my %C = (
gzip => { flag => '-', min => 1, max => 9, suffix => '.gz' },
brotli => { flag => '-q ', min => 0, max => 11, suffix => '.br' },
zstd => { flag => '-q -', min => 1, max => 19, suffix => '.zst' },
);
my @R;
push(@R,{}) for(0..19);
die("One directory as argument expected") if ($#ARGV < 0);
die("First argument is not a directory") if (! -d $ARGV[0]);
my $html = $debug ? $ARGV[0] . "/blog/index.html" : `find $ARGV[0] -name \*.html`;
$html =~ s/\n/ /g;
$C{'gzip'}{'wc'} = ($html =~ s/\.html/\.html$C{'gzip'}{'suffix'}/gr);
$C{'brotli'}{'wc'} = ($html =~ s/\.html/\.html$C{'brotli'}{'suffix'}/gr);
$C{'zstd'}{'wc'} = ($html =~ s/\.html/\.html$C{'zstd'}{'suffix'}/gr);
sub fmt1000(@) { # thousand's formating
# Perl printf to use commas as thousands-separator:
# stackoverflow.com/questions/33442240 + perlmonks.org?node_id=1035411
return scalar reverse(join(',', (unpack('(A3)*',reverse $_[0]))));
}
# Measure execution time and size
for my $c (sort keys %C) {
my ($f,$m,$M) = @{$C{$c}}{'flag','min','max'};
#printf("%s: f=%s, m=%d, M=%d\n", $c, $f, $m, $M);
for (my $l=$m; $l<=$M; ++$l) {
$R[$l]->{$c} = {};
my $r = \$R[$l]{$c};
my $cmd = "$c -kf $f$l";
#printf("\tc=%s, l=%s, cmd=%s\n",$c,$l,$cmd);
open(RES,"/bin/time -p $cmd $html 2>&1 |") || die("Cannot run $c");
while (<RES>) {
my ($type,$time) = ($_ =~ /(\w+) (\d+\.\d+)/);
$$r->{$type} = $time;
#print "\t\t(type,time)=($type,$time): $_";
}
close(RES) || die("Cannot close $c");
my ($wc) = (split(' ', `wc -c $C{$c}{'wc'} | tail -1`,2))[0];
$$r->{'size'} = $wc;
}
}
# Print Markdown table
my ($wcHtml) = (split(' ', `wc -c $html | tail -1`,2))[0];
$wcHtml = fmt1000($wcHtml);
printf("compr.<br>level | %s <br>size | real | user\n", join('<br>size | real | user |', sort keys %C));
printf("-|%s\n", "-:|-:|-:|" x keys %C);
printf(" |%s| | |%s| | |%s\n",$wcHtml,$wcHtml,$wcHtml);
for (my $l=0; $l<=19; ++$l) {
printf("%d",$l);
for my $c (sort keys %C) {
my ($real,$user,$wc) = @{$R[$l]->{$c}}{'real','user','size'};
printf(" | %s | %s | %s",
defined($wc) ? sprintf("%s",fmt1000($wc)) : "n/a",
defined($real) ? sprintf("%.2f",$real) : "n/a",
defined($user) ? sprintf("%.2f",$user) : "n/a"
);
}
printf("\n");
}
# Print Apache ECharts JSON
print "series: [\n";
for my $c (sort keys %C) {
for my $name ( ('size','real') ) {
printf("\t{\n\t\tname: '%s %s',\n\t\ttype: 'line',\n\t\tsmooth: true,\n\t\tdata: [", $c,$name);
for (my $l=0; $l<=19; ++$l) {
my $t = $R[$l]->{$c}->{$name};
if (defined($t) && $name eq 'size') { $t /= $scale; }
printf(" %s,", defined($t) ? sprintf("%s",$t) : "'n/a'");
}
printf(" ]\n\t},\n");
}
}
print "]\n";
5. Summary
As in the previous post Performance Comparison: gzip vs Brotli three years ago:
- brotli compresses best. No other method reaches this level.
- brotli is always faster than gzip when comparing similar compression levels
- zstd is more than two times faster than gzip
- zstd has similar speed to brotli for some range of compression level but then falls off
- Except for compatibility, there is nothing to recommend for gzip
One takeaway is that for my HTML files zstd does offer no real advantage over brotli, which I already support on my web-server.
Further readings.
- Paul Calvano: Choosing Between gzip, Brotli and zStandard Compression
- Danila Vershinin: NGINX Zstd Compression: Complete Configuration Guide