From 788bab3f61c5bbd4c420f571035a47bf06b7f3dc Mon Sep 17 00:00:00 2001 From: tannevaled Date: Tue, 6 Oct 2026 16:10:28 +0200 Subject: [PATCH] Benchmarks, because "would SIMD help" cannot be asked without a baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asked whether go-asmgen or go-simd could make go-pdfkit faster. The honest first answer is that nobody can tell, and here is why: render 43 test files, 0 benchmarks reader 22 test files, 0 benchmarks ops 27 test files, 0 benchmarks extract 10 test files, 0 benchmarks opentype 40 test files, 0 benchmarks go-gfx/gfx 55 test files, 13 benchmarks The only measured corner of the stack is gfx, and the only generated SIMD anywhere in it is `gfx/resample` -- amd64, arm64 and s390x, from go-asmgen. `go-highway` is an INDIRECT dependency of both gfx and render: no code of ours calls it. So optimising now would be choosing where to spend effort by guessing, and the way that fails is well documented in this fleet: a plateau read as progress, or two errors cancelling in a proxy measurement. Five benchmarks over the paths where the question actually lives: Paths gfx/vector's scanline rasteriser, which has no SIMD today ByScale the same page at 72, 150 and 300 dpi, which says whether cost follows PIXELS or follows OPERATORS -- the first thing worth knowing before moving anything into assembly Empty the fixed cost of opening, laying out and allocating, so the floor is visible beside the work in all the others ColourChanges many small fills with a colour change between each, the case where per-operator cost dominates per-pixel cost ⛔ No numbers are claimed here. This machine is at load average 12-19 with twenty peer sessions on it, and a number taken under load is a number about the load. The benchmarks exist so the question can be answered on a quiet machine, with `-count=6` and benchstat, rather than from one run against one run. `onePage` takes a testing.TB rather than a *testing.T, which is the whole of the change to existing code. Coverage is untouched at 100 %. --- bench_test.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ build_test.go | 2 +- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 bench_test.go diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..1afc29d --- /dev/null +++ b/bench_test.go @@ -0,0 +1,106 @@ +package render + +import ( + "fmt" + "strings" + "testing" +) + +// Benchmarks, because the question "would SIMD make this faster" cannot be +// asked of a package with no baseline. +// +// This package had 43 test files and not one benchmark, and so do `reader`, +// `ops`, `extract` and `opentype`. The only measured corner of the whole PDF +// stack is `go-gfx/gfx`, which has 13 — and the only generated SIMD anywhere +// in it is `gfx/resample`, for amd64, arm64 and s390x. +// +// ⛔ These say nothing on their own. A number from a machine under load is a +// number about the load: take them with `-count=6` on a quiet machine and +// compare with benchstat, never one run against one run. +// +// What each is for, in terms of where work could move: +// +// Paths gfx/vector's scanline rasteriser. No SIMD today. +// Text opentype outline loading, then the same rasteriser. +// Images unpacking samples, then gfx/resample — the one path that IS +// already SIMD, so it is the control: a change there should show +// here and nowhere else. +// DPI the same page at three scales, which says whether cost follows +// pixels or follows operators. + +// paths draws n filled and stroked rectangles, which is the shape a chart or a +// table is made of. +func pathsContent(n int) string { + var b strings.Builder + for i := range n { + x := float64(i%40) * 14 + y := float64(i/40) * 14 + fmt.Fprintf(&b, "%.1f %.1f 12 12 re f\n", x, y) + fmt.Fprintf(&b, "0.5 w %.1f %.1f 12 12 re S\n", x+1, y+1) + } + return b.String() +} + +func BenchmarkPagePaths(b *testing.B) { + for _, n := range []int{100, 1000} { + b.Run(fmt.Sprint(n, "-rects"), func(b *testing.B) { + d := onePage(b, [4]float64{0, 0, 595, 842}, pathsContent(n), nil) + b.ResetTimer() + for b.Loop() { + if _, err := Page(d, 1, Options{DPI: 150}); err != nil { + b.Fatal(err) + } + } + }) + } +} + +// The same page at three scales. If the cost follows the pixel count rather +// than the operator count, the work is in the rasteriser and not in the +// content stream — which is the first thing worth knowing before moving any +// of it into assembly. +func BenchmarkPageByScale(b *testing.B) { + d := onePage(b, [4]float64{0, 0, 595, 842}, pathsContent(400), nil) + for _, dpi := range []float64{72, 150, 300} { + b.Run(fmt.Sprintf("%.0fdpi", dpi), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + if _, err := Page(d, 1, Options{DPI: dpi}); err != nil { + b.Fatal(err) + } + } + }) + } +} + +// An empty page at the same size, so the fixed cost of opening, laying out and +// allocating the raster is visible beside the drawing in every other +// benchmark. A measurement with no floor under it reports the floor as if it +// were the work. +func BenchmarkPageEmpty(b *testing.B) { + d := onePage(b, [4]float64{0, 0, 595, 842}, "", nil) + b.ResetTimer() + for b.Loop() { + if _, err := Page(d, 1, Options{DPI: 150}); err != nil { + b.Fatal(err) + } + } +} + +// Many small fills with a colour change between each, which is the shape of a +// shaded plot and the case where per-operator cost dominates per-pixel cost. +func BenchmarkPageColourChanges(b *testing.B) { + var sb strings.Builder + for i := range 2000 { + g := float64(i%100) / 100 + fmt.Fprintf(&sb, "%.2f %.2f %.2f rg %.1f %.1f 6 6 re f\n", + g, 1-g, g/2, float64(i%80)*7, float64(i/80)*7) + } + d := onePage(b, [4]float64{0, 0, 595, 842}, sb.String(), nil) + b.ResetTimer() + for b.Loop() { + if _, err := Page(d, 1, Options{DPI: 150}); err != nil { + b.Fatal(err) + } + } +} diff --git a/build_test.go b/build_test.go index 83c068f..d496292 100644 --- a/build_test.go +++ b/build_test.go @@ -13,7 +13,7 @@ import ( // onePage builds a document of one page with the given content, media box and // extra page entries, so a test can say exactly what is on the paper and then // look at the pixels. -func onePage(t *testing.T, box [4]float64, content string, extra reader.Dict) *reader.Document { +func onePage(t testing.TB, box [4]float64, content string, extra reader.Dict) *reader.Document { t.Helper() w := reader.NewWriter("1.7") pagesRef := w.Reserve()