diff --git a/CHANGELOG.md b/CHANGELOG.md index 03b6157..a8b0b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. This projec ### Added +- `pony_bench`-based benchmark suite under `unicode_bench/` + `unicode_bench_main/` covering every 0.2.0 hot path: `Bytes` validation, `Text` construction (plain and indexed), `Codepoints` property lookups + counts, all four segmentation cursors (`Graphemes`/`Words`/`Sentences`/`Lines`), the four normal forms, case mappings, the five comparison flavours, `Search`/`Split`/`Trim`/`Replace`, and the `Scripts` ops. Seven canonical input corpora — ASCII, Latin precomposed, Latin decomposed, CJK, mixed, emoji, and a pathological combining-marks generator — let each topic exercise its representative shape. Per-size iteration caps keep the run tractable at 160M-byte inputs. New Makefile targets `bench-build` / `bench` (full release-mode run) / `bench-smoke` (single small bucket) / `bench-csv` (machine-readable for cross-run diffing). Not part of `ci`. ### Changed diff --git a/Makefile b/Makefile index 466c234..5f59885 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ $(BUILD_DIR): dependencies: corral.json $(GET_DEPENDENCIES_WITH) -.PHONY: all ci test unit-tests clean realclean dependencies docs ucd-build ucd-generate ucd-download conform conform-build conform-grapheme conform-grapheme-build conform-word conform-word-build conform-sentence conform-sentence-build conform-line conform-line-build +.PHONY: all ci test unit-tests clean realclean dependencies docs ucd-build ucd-generate ucd-download conform conform-build conform-grapheme conform-grapheme-build conform-word conform-word-build conform-sentence conform-sentence-build conform-line conform-line-build bench bench-build bench-smoke bench-csv .DEFAULT_GOAL := all @@ -187,3 +187,31 @@ $(conform_line_binary): $(SOURCE_FILES) $(shell find unicode_line_conform_main - conform-line: $(conform_line_binary) $(conform_line_binary) $(UCD_DIR)/auxiliary/LineBreakTest.txt + +# ---------- Benchmarks ---------- +# `make bench` runs the full pony_bench-based suite under release +# optimization; `bench-smoke` runs a single small bucket for quick +# verification; `bench-csv` writes machine-readable output for cross-run +# diffing. Benchmarks are NOT part of `ci` — runtimes are too long for +# every PR. + +BENCH_BUILD_DIR := build/release +bench_binary := $(BENCH_BUILD_DIR)/unicode_bench_main + +bench-build: $(bench_binary) + +$(bench_binary): $(SOURCE_FILES) $(shell find unicode_bench unicode_bench_main -name '*.pony' 2>/dev/null) | $(BENCH_BUILD_DIR) dependencies + $(COMPILE_WITH) -o $(BENCH_BUILD_DIR) unicode_bench_main -b unicode_bench_main + +$(BENCH_BUILD_DIR): + mkdir -p $(BENCH_BUILD_DIR) + +bench: $(bench_binary) + $(bench_binary) --ponynoyield + +bench-smoke: $(bench_binary) + $(bench_binary) --ponynoyield --smoke + +bench-csv: $(bench_binary) | $(BUILD_DIR) + $(bench_binary) --ponynoyield -csv | tee $(BUILD_DIR)/bench-$(shell git rev-parse --short HEAD).csv + @echo "wrote $(BUILD_DIR)/bench-$(shell git rev-parse --short HEAD).csv" diff --git a/unicode_bench/_bench.pony b/unicode_bench/_bench.pony new file mode 100644 index 0000000..87cb81b --- /dev/null +++ b/unicode_bench/_bench.pony @@ -0,0 +1,214 @@ +use "pony_bench" + +primitive BenchSizes + """ + Size buckets every size-sensitive benchmark is run across, plus helpers + for labelling and tuning the per-bucket iteration budget. Same layout + as the sibling `string_benchmark` project so cross-comparisons line up. + """ + fun apply(): Array[USize] val => + [as USize: 16; 160; 1_600; 16_000; 160_000; 1_600_000; 16_000_000; 160_000_000] + + fun smoke(): Array[USize] val => + """ + Single small bucket — used by the smoke build to confirm the harness + boots and a benchmark completes before exercising the full size range. + """ + [as USize: 1_600] + + fun label(size: USize): String => + """ + Exact byte count with a `B` suffix. We don't round to K/M + abbreviations because that loses precision — the 1_600-byte + bucket isn't 1 KiB, and `Text.from_string/emoji/152M` reads as + "152 × 1 MiB" when the actual size is 160_000_000. + """ + size.string() + "B" + + fun samples(): USize => 20 + fun sample_ns(): U64 => 30_000_000 + + fun heavy_op_cap(): USize => + """ + Maximum input size for "heavy" operations — ones whose per-byte cost + is high enough that a single iteration at 160M takes minutes + (`Text.from_string[indexed]` builds an O(n) grapheme bitmap; + `Normalize.nfc` over `combining_marks` runs the canonical reorder + over a 6-cp combining run per base letter, then composes). Gate + such registrations with `if size <= BenchSizes.heavy_op_cap()`. + Tighten this if even 16M takes too long; loosen once the + implementations are fast enough that the top bucket is tractable. + """ + 16_000_000 + + fun default_cfg(): BenchConfig => + BenchConfig(where samples' = samples(), max_sample_time' = sample_ns()) + + fun cfg(size: USize): BenchConfig => + """ + Allocating benchmarks accumulate per-iteration garbage inside a single + sample. Bound iterations so large sizes don't generate gigabytes of + garbage before GC runs. Mirrors the string_benchmark calibration. + """ + let max_it: U64 = + if size >= 1_048_576 then 1_024 + elseif size >= 65_536 then 4_096 + elseif size >= 4_096 then 100_000 + else 1_000_000 + end + let s: USize = + if size >= 1_048_576 then samples().min(5) + elseif size >= 65_536 then samples().min(10) + else samples() + end + BenchConfig(where + samples' = s, + max_iterations' = max_it, + max_sample_time' = sample_ns()) + +class iso _Overhead is MicroBenchmark + """ + Overhead benchmark sized to the actual benchmark's config so the + subtraction uses the same sample budget. pony_bench's built-in + `OverheadBenchmark` always uses the default 20-sample/100ms config, + which is far larger than the smallest buckets need. + """ + let _config: BenchConfig + new iso create(config': BenchConfig) => _config = config' + fun name(): String => "Benchmark Overhead" + fun config(): BenchConfig => _config + fun overhead(): MicroBenchmark^ => _Overhead(_config) + fun ref apply() => + DoNotOptimise[None](None) + DoNotOptimise.observe() + +class iso _Bench is MicroBenchmark + """ + Read-only benchmark (box-receiver). The subject is built once and never + mutated, so no per-iteration reset is needed. + """ + let _name: String + let _s: String val + let _op: {(String box) ?} val + let _config: BenchConfig + + new iso create( + name': String, + s: String val, + op: {(String box) ?} val, + config': BenchConfig = BenchSizes.default_cfg()) + => + _name = name' + _s = s + _op = op + _config = config' + + fun name(): String => _name + fun config(): BenchConfig => _config + fun overhead(): MicroBenchmark^ => _Overhead(_config) + fun ref apply() ? => _op(_s)? + +class iso _ValBench is MicroBenchmark + """ + Val-receiver benchmark — for ops that take `String val` (e.g. + `Text.from_iso_string` returns iso but most ops want box; this is for + the cases where the iso input is consumed). + """ + let _name: String + let _s: String val + let _op: {(String val) ?} val + let _config: BenchConfig + + new iso create( + name': String, + s: String val, + op: {(String val) ?} val, + config': BenchConfig = BenchSizes.default_cfg()) + => + _name = name' + _s = s + _op = op + _config = config' + + fun name(): String => _name + fun config(): BenchConfig => _config + fun overhead(): MicroBenchmark^ => _Overhead(_config) + fun ref apply() ? => _op(_s)? + +class iso _CtorBench is MicroBenchmark + """ + Constructor benchmark. The op captures whatever (val) inputs it needs + and builds + observes a fresh result each iteration. + """ + let _name: String + let _op: {() ?} val + let _config: BenchConfig + + new iso create( + name': String, + op: {() ?} val, + config': BenchConfig = BenchSizes.default_cfg()) + => + _name = name' + _op = op + _config = config' + + fun name(): String => _name + fun config(): BenchConfig => _config + fun overhead(): MicroBenchmark^ => _Overhead(_config) + fun ref apply() ? => _op()? + +class iso _BenchPair is MicroBenchmark + """ + Pair-input benchmark for binary ops like `Compares.equal_*(a, b)` or + `Search.contains(haystack, needle)`. Both strings are built once. + """ + let _name: String + let _a: String val + let _b: String val + let _op: {(String box, String box) ?} val + let _config: BenchConfig + + new iso create( + name': String, + a: String val, + b: String val, + op: {(String box, String box) ?} val, + config': BenchConfig = BenchSizes.default_cfg()) + => + _name = name' + _a = a + _b = b + _op = op + _config = config' + + fun name(): String => _name + fun config(): BenchConfig => _config + fun overhead(): MicroBenchmark^ => _Overhead(_config) + fun ref apply() ? => _op(_a, _b)? + +class iso _BenchU32 is MicroBenchmark + """ + Single-codepoint benchmark for property lookups (`category`, `script`, + etc.). Carries a U32 directly so the op doesn't pay String boxing. + """ + let _name: String + let _cp: U32 + let _op: {(U32)} val + let _config: BenchConfig + + new iso create( + name': String, + cp: U32, + op: {(U32)} val, + config': BenchConfig = BenchSizes.default_cfg()) + => + _name = name' + _cp = cp + _op = op + _config = config' + + fun name(): String => _name + fun config(): BenchConfig => _config + fun overhead(): MicroBenchmark^ => _Overhead(_config) + fun ref apply() => _op(_cp) diff --git a/unicode_bench/_corpora.pony b/unicode_bench/_corpora.pony new file mode 100644 index 0000000..7bc6d5c --- /dev/null +++ b/unicode_bench/_corpora.pony @@ -0,0 +1,115 @@ +primitive _Corpora + """ + Sample text generators for the benchmarks. Each generator returns a + `String val` whose byte length is at least the requested `size` (and + at most `size + pattern.size()` — never trimmed mid-codepoint). + + All patterns are valid UTF-8. The patterns are picked to exercise + different bytes-per-cp distributions and (where relevant) different + normalization / segmentation states: + + * `ascii` — 1-byte cps, ASCII only + * `latin_precomposed` — Latin-1 / 2-byte cps in NFC + * `latin_decomposed` — same text in NFD; stresses NFC composition + * `cjk` — 3-byte cps, Han ideographs + * `mixed` — ASCII + Latin + CJK in alternation + * `emoji` — 4-byte cps + ZWJ sequences (multi-cp graphemes) + * `combining_marks` — pathological: base + 5 combining marks per cluster + """ + + fun ascii(size: USize): String val => + _repeat(_ascii_pattern(), size) + + fun latin_precomposed(size: USize): String val => + _repeat(_latin_precomposed_pattern(), size) + + fun latin_decomposed(size: USize): String val => + _repeat(_latin_decomposed_pattern(), size) + + fun cjk(size: USize): String val => + _repeat(_cjk_pattern(), size) + + fun mixed(size: USize): String val => + _repeat(_mixed_pattern(), size) + + fun emoji(size: USize): String val => + _repeat(_emoji_pattern(), size) + + fun combining_marks(size: USize): String val => + _repeat(_combining_pattern(), size) + + fun _ascii_pattern(): String val => + "The quick brown fox jumps over the lazy dog. " + + fun _latin_precomposed_pattern(): String val => + "café déjà naïve résumé schöne Straße über Hände " + + fun _latin_decomposed_pattern(): String val => + recover val + let s = String + s.append("cafe"); s.push_utf32(0x0301); s.push(' ') // café + s.append("deja"); s.push_utf32(0x0300); s.push(' ') // déjà (à) + s.append("naive"); s.push_utf32(0x0308); s.push(' ') // naïve (ï) + s.append("resume"); s.push_utf32(0x0301); s.push(' ') // résumé + s.append("schone"); s.push_utf32(0x0308); s.push(' ') // schöne + s.append("Strasse"); s.push_utf32(0x0301); s.push(' ') // (placeholder) + s.append("uber"); s.push_utf32(0x0308); s.push(' ') // über + s.append("Hande"); s.push_utf32(0x0308); s.push(' ') // Hände + s + end + + fun _cjk_pattern(): String val => + "中国汉字测试样本日本語サンプル " + + fun _mixed_pattern(): String val => + "Hello 世界 café 中国 résumé 日本 World " + + fun _emoji_pattern(): String val => + recover val + let s = String + s.push_utf32(0x1F600); s.push(' ') // 😀 (single cp grapheme) + s.push_utf32(0x1F389); s.push(' ') // 🎉 + // Flag of France — Regional_Indicator pair = 1 grapheme. + s.push_utf32(0x1F1EB); s.push_utf32(0x1F1F7); s.push(' ') + // Family ZWJ sequence (man + ZWJ + woman + ZWJ + girl) — 1 grapheme. + s.push_utf32(0x1F468); s.push_utf32(0x200D) + s.push_utf32(0x1F469); s.push_utf32(0x200D) + s.push_utf32(0x1F467); s.push(' ') + s.push_utf32(0x1F680); s.push(' ') // 🚀 + s + end + + fun _combining_pattern(): String val => + """ + Pathological case for NFC composition / canonical-ordering: each + base letter is followed by FIVE combining marks. NFC must read + them all, canonical-order them, then re-compose where possible. + """ + recover val + let s = String + var i: USize = 0 + while i < 8 do + // Latin letter 'a' + 5 combining marks (each above/below). + s.push('a') + s.push_utf32(0x0301) // combining acute (Above) + s.push_utf32(0x0308) // combining diaeresis (Above) + s.push_utf32(0x0323) // combining dot below (Below) + s.push_utf32(0x0327) // combining cedilla (Below_Attached) + s.push_utf32(0x0316) // combining grave below (Below) + i = i + 1 + end + s.push(' ') + s + end + + fun _repeat(pattern: String val, target: USize): String val => + """ + Append `pattern` repeatedly until the buffer reaches at least + `target` bytes. Boundaries are always at codepoint boundaries + because we only append whole patterns. + """ + recover val + let out = String(target + pattern.size()) + while out.size() < target do out.append(pattern) end + out + end diff --git a/unicode_bench/bench_bytes.pony b/unicode_bench/bench_bytes.pony new file mode 100644 index 0000000..50b2c7b --- /dev/null +++ b/unicode_bench/bench_bytes.pony @@ -0,0 +1,50 @@ +use "pony_bench" +use "../unicode" + +primitive BenchBytes + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + + _is_valid(bench, "Bytes.is_valid_utf8/ascii/" + lbl, + _Corpora.ascii(size), cfg) + _is_valid(bench, "Bytes.is_valid_utf8/cjk/" + lbl, + _Corpora.cjk(size), cfg) + _is_valid(bench, "Bytes.is_valid_utf8/emoji/" + lbl, + _Corpora.emoji(size), cfg) + _is_valid(bench, "Bytes.is_valid_utf8/mixed/" + lbl, + _Corpora.mixed(size), cfg) + + _first_bad(bench, "Bytes.first_bad_utf8_offset/ascii/" + lbl, + _Corpora.ascii(size), cfg) + _first_bad(bench, "Bytes.first_bad_utf8_offset/cjk/" + lbl, + _Corpora.cjk(size), cfg) + + fun _is_valid( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) => + DoNotOptimise[Bool](Bytes.is_valid_utf8(s)) + DoNotOptimise.observe() + }, cfg)) + + fun _first_bad( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) => + DoNotOptimise[(AllValid | USize)](Bytes.first_bad_utf8_offset(s)) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_case.pony b/unicode_bench/bench_case.pony new file mode 100644 index 0000000..d667625 --- /dev/null +++ b/unicode_bench/bench_case.pony @@ -0,0 +1,55 @@ +use "pony_bench" +use "../unicode" + +primitive BenchCase + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let latin = _Corpora.latin_precomposed(size) + let mixed = _Corpora.mixed(size) + + _op(bench, "Case.upper/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Case.upper(s)) + DoNotOptimise.observe() + }, cfg) + _op(bench, "Case.upper/latin/" + lbl, latin, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Case.upper(s)) + DoNotOptimise.observe() + }, cfg) + _op(bench, "Case.lower/latin/" + lbl, latin, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Case.lower(s)) + DoNotOptimise.observe() + }, cfg) + _op(bench, "Case.title/latin/" + lbl, latin, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Case.title(s)) + DoNotOptimise.observe() + }, cfg) + _op(bench, "Case.fold/latin/" + lbl, latin, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Case.fold(s)) + DoNotOptimise.observe() + }, cfg) + _op(bench, "Case.fold/mixed/" + lbl, mixed, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Case.fold(s)) + DoNotOptimise.observe() + }, cfg) + + fun _op( + bench: PonyBench, + name: String, + s: String val, + op: {(String box) ?} val, + cfg: BenchConfig) + => + bench(_Bench(name, s, op, cfg)) diff --git a/unicode_bench/bench_codepoints.pony b/unicode_bench/bench_codepoints.pony new file mode 100644 index 0000000..a88cc2d --- /dev/null +++ b/unicode_bench/bench_codepoints.pony @@ -0,0 +1,93 @@ +use "pony_bench" +use "../unicode" + +primitive BenchCodepoints + fun register(bench: PonyBench, sizes: Array[USize] val) => + // Single-cp property lookups: size-independent — register once. + _single_cp(bench) + + // Whole-string count: size-sensitive. + for size in sizes.values() do + _count_size(bench, size) + end + + fun _single_cp(bench: PonyBench) => + let cfg = BenchSizes.default_cfg() + let cps: Array[(U32, String)] val = + [as (U32, String): + (U32('A'), "ascii") + (U32(0xE9), "latin1") + (U32(0x4E2D), "cjk") + (U32(0x1F600), "emoji") + ] + for c in cps.values() do + (let cp, let lbl) = c + bench(_BenchU32("Codepoints.category/" + lbl, cp, + {(cp: U32) => + DoNotOptimise[Category](Codepoints.category(cp)) + DoNotOptimise.observe() + }, cfg)) + bench(_BenchU32("Codepoints.script/" + lbl, cp, + {(cp: U32) => + DoNotOptimise[Script](Codepoints.script(cp)) + DoNotOptimise.observe() + }, cfg)) + bench(_BenchU32("Codepoints.script_extensions/" + lbl, cp, + {(cp: U32) => + DoNotOptimise[Array[Script] val]( + Codepoints.script_extensions(cp)) + DoNotOptimise.observe() + }, cfg)) + bench(_BenchU32("Codepoints.east_asian_width/" + lbl, cp, + {(cp: U32) => + DoNotOptimise[EastAsianWidth](Codepoints.east_asian_width(cp)) + DoNotOptimise.observe() + }, cfg)) + bench(_BenchU32("Codepoints.has_binary_property/" + lbl, cp, + {(cp: U32) => + DoNotOptimise[Bool]( + Codepoints.has_binary_property(cp, PropAlphabetic)) + DoNotOptimise.observe() + }, cfg)) + bench(_BenchU32("Codepoints.name/" + lbl, cp, + {(cp: U32) => + DoNotOptimise[(String val | None)](Codepoints.name(cp)) + DoNotOptimise.observe() + }, cfg)) + bench(_BenchU32("Codepoints.combining_class/" + lbl, cp, + {(cp: U32) => + DoNotOptimise[U8](Codepoints.combining_class(cp)) + DoNotOptimise.observe() + }, cfg)) + end + + // Reverse name lookup — linear scan; the slow path. + bench(_CtorBench("Codepoints.from_name", + {() => + DoNotOptimise[(U32 | None)]( + Codepoints.from_name("LATIN CAPITAL LETTER A")) + DoNotOptimise.observe() + }, cfg)) + + fun _count_size(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let cjk = _Corpora.cjk(size) + let mixed = _Corpora.mixed(size) + + bench(_Bench("Codepoints.count/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Codepoints.count(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Codepoints.count/cjk/" + lbl, cjk, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Codepoints.count(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Codepoints.count/mixed/" + lbl, mixed, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Codepoints.count(s)) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_compares.pony b/unicode_bench/bench_compares.pony new file mode 100644 index 0000000..184c0e9 --- /dev/null +++ b/unicode_bench/bench_compares.pony @@ -0,0 +1,65 @@ +use "pony_bench" +use "../unicode" + +primitive BenchCompares + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + + // Worst case for each predicate: both strings are equal under the + // operation, so the comparison has to walk the entire length. + let ascii_a = _Corpora.ascii(size) + let ascii_b = _Corpora.ascii(size) + let latin_a = _Corpora.latin_precomposed(size) + let latin_b = _Corpora.latin_precomposed(size) + + bench(_BenchPair("Compares.bytes/ascii/" + lbl, + ascii_a, ascii_b, + {(a: String box, b: String box) => + DoNotOptimise[Compare](Compares.bytes(a, b)) + DoNotOptimise.observe() + }, cfg)) + + bench(_BenchPair("Compares.equal_bytes/ascii/" + lbl, + ascii_a, ascii_b, + {(a: String box, b: String box) => + DoNotOptimise[Bool](Compares.equal_bytes(a, b)) + DoNotOptimise.observe() + }, cfg)) + + bench(_BenchPair("Compares.equal_canonical/latin/" + lbl, + latin_a, latin_b, + {(a: String box, b: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Compares.equal_canonical(a, b)) + DoNotOptimise.observe() + }, cfg)) + + bench(_BenchPair("Compares.equal_compat/latin/" + lbl, + latin_a, latin_b, + {(a: String box, b: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Compares.equal_compat(a, b)) + DoNotOptimise.observe() + }, cfg)) + + bench(_BenchPair("Compares.equal_caseless/latin/" + lbl, + latin_a, latin_b, + {(a: String box, b: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Compares.equal_caseless(a, b)) + DoNotOptimise.observe() + }, cfg)) + + bench(_BenchPair("Compares.equal_caseless_canonical/latin/" + lbl, + latin_a, latin_b, + {(a: String box, b: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Compares.equal_caseless_canonical(a, b)) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_graphemes.pony b/unicode_bench/bench_graphemes.pony new file mode 100644 index 0000000..c276cd7 --- /dev/null +++ b/unicode_bench/bench_graphemes.pony @@ -0,0 +1,58 @@ +use "pony_bench" +use "../unicode" + +primitive BenchGraphemes + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let latin = _Corpora.latin_precomposed(size) + let cjk = _Corpora.cjk(size) + let emoji = _Corpora.emoji(size) + + _count(bench, "Graphemes.count/ascii/" + lbl, ascii, cfg) + _count(bench, "Graphemes.count/latin/" + lbl, latin, cfg) + _count(bench, "Graphemes.count/cjk/" + lbl, cjk, cfg) + _count(bench, "Graphemes.count/emoji/" + lbl, emoji, cfg) + + _ranges(bench, "Graphemes.ranges-walk/ascii/" + lbl, ascii, cfg) + _ranges(bench, "Graphemes.ranges-walk/cjk/" + lbl, cjk, cfg) + _ranges(bench, "Graphemes.ranges-walk/emoji/" + lbl, emoji, cfg) + + fun _count( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Graphemes.count(s)) + DoNotOptimise.observe() + }, cfg)) + + fun _ranges( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) => + match Graphemes.ranges(s) + | let it: Iterator[(USize, USize)] => + var n: USize = 0 + while it.has_next() do + try (let lo, let hi) = it.next()?; DoNotOptimise[USize](lo + hi) end + n = n + 1 + end + DoNotOptimise[USize](n) + | let _: InvalidUtf8 => None + end + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_lines.pony b/unicode_bench/bench_lines.pony new file mode 100644 index 0000000..1908503 --- /dev/null +++ b/unicode_bench/bench_lines.pony @@ -0,0 +1,53 @@ +use "pony_bench" +use "../unicode" + +primitive BenchLines + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let cjk = _Corpora.cjk(size) + let mixed = _Corpora.mixed(size) + + bench(_Bench("Lines.count/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Lines.count(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Lines.count/cjk/" + lbl, cjk, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Lines.count(s)) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Lines.ranges-walk/ascii/" + lbl, ascii, + {(s: String box) => + match Lines.ranges(s) + | let it: Iterator[(USize, USize)] => + var n: USize = 0 + while it.has_next() do + try (let lo, let hi) = it.next()?; DoNotOptimise[USize](lo + hi) end + n = n + 1 + end + DoNotOptimise[USize](n) + end + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Lines.ranges-walk/mixed/" + lbl, mixed, + {(s: String box) => + match Lines.ranges(s) + | let it: Iterator[(USize, USize)] => + var n: USize = 0 + while it.has_next() do + try (let lo, let hi) = it.next()?; DoNotOptimise[USize](lo + hi) end + n = n + 1 + end + DoNotOptimise[USize](n) + end + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_normalize.pony b/unicode_bench/bench_normalize.pony new file mode 100644 index 0000000..0f9a30c --- /dev/null +++ b/unicode_bench/bench_normalize.pony @@ -0,0 +1,82 @@ +use "pony_bench" +use "../unicode" + +primitive BenchNormalize + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let latin_pre = _Corpora.latin_precomposed(size) + let latin_dec = _Corpora.latin_decomposed(size) + let combining = _Corpora.combining_marks(size) + + _nfd(bench, "Normalize.nfd/ascii/" + lbl, ascii, cfg) + _nfd(bench, "Normalize.nfd/latin-pre/" + lbl, latin_pre, cfg) + + // NFC composition is the headline workload — decomposed and + // pathological combining-marks corpora are the worst cases. + _nfc(bench, "Normalize.nfc/ascii/" + lbl, ascii, cfg) + _nfc(bench, "Normalize.nfc/latin-pre/" + lbl, latin_pre, cfg) + _nfc(bench, "Normalize.nfc/latin-dec/" + lbl, latin_dec, cfg) + // `combining` is the worst-case input — every base letter carries + // 5 marks that must be canonically reordered, then composed if + // possible. At 160M a single call takes minutes; cap at heavy_op_cap. + if size <= BenchSizes.heavy_op_cap() then + _nfc(bench, "Normalize.nfc/combining/" + lbl, combining, cfg) + end + + _nfkd(bench, "Normalize.nfkd/latin-pre/" + lbl, latin_pre, cfg) + _nfkc(bench, "Normalize.nfkc/latin-pre/" + lbl, latin_pre, cfg) + + fun _nfd( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Normalize.nfd(s)) + DoNotOptimise.observe() + }, cfg)) + + fun _nfc( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Normalize.nfc(s)) + DoNotOptimise.observe() + }, cfg)) + + fun _nfkd( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Normalize.nfkd(s)) + DoNotOptimise.observe() + }, cfg)) + + fun _nfkc( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Normalize.nfkc(s)) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_replace.pony b/unicode_bench/bench_replace.pony new file mode 100644 index 0000000..cf24bb4 --- /dev/null +++ b/unicode_bench/bench_replace.pony @@ -0,0 +1,45 @@ +use "pony_bench" +use "../unicode" + +primitive BenchReplace + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + + // Dense matches: "the" appears many times in the ascii corpus. + bench(_Bench("Replace.all[dense]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)]( + Replace.all(s, "the", "THE")) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Replace.first[dense]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)]( + Replace.first(s, "the", "THE")) + DoNotOptimise.observe() + }, cfg)) + + // No matches: must walk the whole string for a needle that isn't + // there. + bench(_Bench("Replace.all[absent]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)]( + Replace.all(s, "NOMATCH_XYZ", "anything")) + DoNotOptimise.observe() + }, cfg)) + + // Same-size replacement vs growing replacement (different copying + // strategies in Replace.all). + bench(_Bench("Replace.all[grow]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)]( + Replace.all(s, "the", "th_e_")) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_scripts.pony b/unicode_bench/bench_scripts.pony new file mode 100644 index 0000000..6eca92b --- /dev/null +++ b/unicode_bench/bench_scripts.pony @@ -0,0 +1,66 @@ +use "pony_bench" +use "../unicode" + +primitive BenchScripts + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let cjk = _Corpora.cjk(size) + let mixed = _Corpora.mixed(size) + + bench(_Bench("Scripts.of/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[ScriptSet val](Scripts.of(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Scripts.of/cjk/" + lbl, cjk, + {(s: String box) => + DoNotOptimise[ScriptSet val](Scripts.of(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Scripts.of/mixed/" + lbl, mixed, + {(s: String box) => + DoNotOptimise[ScriptSet val](Scripts.of(s)) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Scripts.dominant/mixed/" + lbl, mixed, + {(s: String box) => + DoNotOptimise[Script](Scripts.dominant(s)) + DoNotOptimise.observe() + }, cfg)) + + let allowed = ScriptSet.create([as Script: ScriptLatin]) + bench(_Bench("Scripts.restrict_to[Latin-only]/ascii/" + lbl, ascii, + {(s: String box)(allowed) => + DoNotOptimise[Bool](Scripts.restrict_to(s, allowed)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Scripts.restrict_to[Latin-only]/mixed/" + lbl, mixed, + {(s: String box)(allowed) => + DoNotOptimise[Bool](Scripts.restrict_to(s, allowed)) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Scripts.is_single_script/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[Bool](Scripts.is_single_script(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Scripts.is_single_script/mixed/" + lbl, mixed, + {(s: String box) => + DoNotOptimise[Bool](Scripts.is_single_script(s)) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Scripts.resolved_script_set/mixed/" + lbl, mixed, + {(s: String box) => + DoNotOptimise[ScriptSet val](Scripts.resolved_script_set(s)) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_search.pony b/unicode_bench/bench_search.pony new file mode 100644 index 0000000..1938de2 --- /dev/null +++ b/unicode_bench/bench_search.pony @@ -0,0 +1,69 @@ +use "pony_bench" +use "../unicode" + +primitive BenchSearch + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let cjk = _Corpora.cjk(size) + + // Three cases per op: needle absent (worst-case for `contains`/ + // `index_of`), present early, present at the very end. + bench(_Bench("Search.contains[absent]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Search.contains(s, "NOMATCH_XYZ_123")) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Search.contains[present]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Search.contains(s, "quick")) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Search.index_of[absent]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(USize | None | InvalidUtf8)]( + Search.index_of(s, "NOMATCH")) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Search.last_index_of[absent]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(USize | None | InvalidUtf8)]( + Search.last_index_of(s, "NOMATCH")) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Search.count[dense]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Search.count(s, "the")) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Search.starts_with/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Search.starts_with(s, "The")) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Search.ends_with/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Search.ends_with(s, " ")) + DoNotOptimise.observe() + }, cfg)) + + // Multi-byte needle search. + bench(_Bench("Search.contains[present]/cjk/" + lbl, cjk, + {(s: String box) => + DoNotOptimise[(Bool | InvalidUtf8)]( + Search.contains(s, "中国")) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_sentences.pony b/unicode_bench/bench_sentences.pony new file mode 100644 index 0000000..300aa32 --- /dev/null +++ b/unicode_bench/bench_sentences.pony @@ -0,0 +1,39 @@ +use "pony_bench" +use "../unicode" + +primitive BenchSentences + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let mixed = _Corpora.mixed(size) + + bench(_Bench("Sentences.count/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Sentences.count(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Sentences.count/mixed/" + lbl, mixed, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Sentences.count(s)) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Sentences.ranges-walk/ascii/" + lbl, ascii, + {(s: String box) => + match Sentences.ranges(s) + | let it: Iterator[(USize, USize)] => + var n: USize = 0 + while it.has_next() do + try (let lo, let hi) = it.next()?; DoNotOptimise[USize](lo + hi) end + n = n + 1 + end + DoNotOptimise[USize](n) + end + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_split.pony b/unicode_bench/bench_split.pony new file mode 100644 index 0000000..9c672fc --- /dev/null +++ b/unicode_bench/bench_split.pony @@ -0,0 +1,34 @@ +use "pony_bench" +use "../unicode" + +primitive BenchSplit + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + + bench(_Bench("Split.on[space]/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(Array[String val] val | InvalidUtf8)]( + Split.on(s, " ")) + DoNotOptimise.observe() + }, cfg)) + + // Lines: build a corpus with explicit \n separators. + let with_nl = recover val + let out = String(size + 64) + out.append(ascii) + out.append("\nsecond line\nthird line\r\nfourth\r\n") + out + end + bench(_Bench("Split.lines/" + lbl, with_nl, + {(s: String box) => + DoNotOptimise[(Array[String val] val | InvalidUtf8)]( + Split.lines(s)) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_text.pony b/unicode_bench/bench_text.pony new file mode 100644 index 0000000..b2598e1 --- /dev/null +++ b/unicode_bench/bench_text.pony @@ -0,0 +1,58 @@ +use "pony_bench" +use "../unicode" + +primitive BenchText + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let cjk = _Corpora.cjk(size) + let mixed = _Corpora.mixed(size) + let emoji = _Corpora.emoji(size) + + _from_string(bench, "Text.from_string/ascii/" + lbl, ascii, cfg) + _from_string(bench, "Text.from_string/cjk/" + lbl, cjk, cfg) + _from_string(bench, "Text.from_string/mixed/" + lbl, mixed, cfg) + _from_string(bench, "Text.from_string/emoji/" + lbl, emoji, cfg) + + // Indexed Text construction walks the grapheme cursor over the + // entire string to build the bitmap. At the 160M bucket a single + // iteration takes minutes — skip it to keep the suite tractable. + if size <= BenchSizes.heavy_op_cap() then + _from_string_indexed(bench, + "Text.from_string[indexed]/ascii/" + lbl, ascii, cfg) + _from_string_indexed(bench, + "Text.from_string[indexed]/cjk/" + lbl, cjk, cfg) + _from_string_indexed(bench, + "Text.from_string[indexed]/emoji/" + lbl, emoji, cfg) + end + + fun _from_string( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) ? => + DoNotOptimise[Text val](Text.from_string(s.clone())?) + DoNotOptimise.observe() + }, cfg)) + + fun _from_string_indexed( + bench: PonyBench, + name: String, + s: String val, + cfg: BenchConfig) + => + bench(_Bench(name, s, + {(s: String box) ? => + DoNotOptimise[Text val]( + Text.from_string(s.clone() where indexed = true)?) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_trim.pony b/unicode_bench/bench_trim.pony new file mode 100644 index 0000000..8359099 --- /dev/null +++ b/unicode_bench/bench_trim.pony @@ -0,0 +1,37 @@ +use "pony_bench" +use "../unicode" + +primitive BenchTrim + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + + // Construct a padded input: whitespace at both ends, content in middle. + let padded = recover val + let out = String(size + 64) + out.append("\t\n ") + out.append(_Corpora.ascii(size)) + out.append(" \n\t") + out + end + + bench(_Bench("Trim.trim/" + lbl, padded, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Trim.trim(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Trim.trim_start/" + lbl, padded, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Trim.trim_start(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Trim.trim_end/" + lbl, padded, + {(s: String box) => + DoNotOptimise[(String iso | InvalidUtf8)](Trim.trim_end(s)) + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench/bench_words.pony b/unicode_bench/bench_words.pony new file mode 100644 index 0000000..6af60a2 --- /dev/null +++ b/unicode_bench/bench_words.pony @@ -0,0 +1,45 @@ +use "pony_bench" +use "../unicode" + +primitive BenchWords + fun register(bench: PonyBench, sizes: Array[USize] val) => + for size in sizes.values() do + _one(bench, size) + end + + fun _one(bench: PonyBench, size: USize) => + let cfg = BenchSizes.cfg(size) + let lbl = BenchSizes.label(size) + let ascii = _Corpora.ascii(size) + let mixed = _Corpora.mixed(size) + let cjk = _Corpora.cjk(size) + + bench(_Bench("Words.count/ascii/" + lbl, ascii, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Words.count(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Words.count/mixed/" + lbl, mixed, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Words.count(s)) + DoNotOptimise.observe() + }, cfg)) + bench(_Bench("Words.count/cjk/" + lbl, cjk, + {(s: String box) => + DoNotOptimise[(USize | InvalidUtf8)](Words.count(s)) + DoNotOptimise.observe() + }, cfg)) + + bench(_Bench("Words.ranges-walk/ascii/" + lbl, ascii, + {(s: String box) => + match Words.ranges(s) + | let it: Iterator[(USize, USize)] => + var n: USize = 0 + while it.has_next() do + try (let lo, let hi) = it.next()?; DoNotOptimise[USize](lo + hi) end + n = n + 1 + end + DoNotOptimise[USize](n) + end + DoNotOptimise.observe() + }, cfg)) diff --git a/unicode_bench_main/main.pony b/unicode_bench_main/main.pony new file mode 100644 index 0000000..6d3ab14 --- /dev/null +++ b/unicode_bench_main/main.pony @@ -0,0 +1,47 @@ +use "pony_bench" +use "../unicode" +use "../unicode_bench" + +actor Main is BenchmarkList + """ + Entry point for the unicode benchmark suite. Each topic primitive + (BenchBytes, BenchText, …) registers its benchmarks against the + `PonyBench` actor in `create`. `benchmarks(bench)` is a no-op — all + registration happens here in `create` because we want corpora prepared + once and then handed to each topic. + + Run modes: + `./unicode_bench_main` — full suite (every size bucket) + `./unicode_bench_main --smoke` — single small bucket only + `./unicode_bench_main -csv` — machine-readable CSV (pony_bench flag) + + Recommended ponyc flag: `--ponynoyield` (see pony_bench docs). Release + builds matter — the debug build is 10× slower and only useful for + shaking out compile errors. + """ + new create(env: Env) => + var smoke: Bool = false + for a in env.args.values() do + if a == "--smoke" then smoke = true end + end + let pb = PonyBench(env, this) + let sizes = if smoke then BenchSizes.smoke() else BenchSizes() end + + BenchBytes.register(pb, sizes) + BenchText.register(pb, sizes) + BenchCodepoints.register(pb, sizes) + BenchGraphemes.register(pb, sizes) + BenchWords.register(pb, sizes) + BenchSentences.register(pb, sizes) + BenchLines.register(pb, sizes) + BenchNormalize.register(pb, sizes) + BenchCase.register(pb, sizes) + BenchCompares.register(pb, sizes) + BenchSearch.register(pb, sizes) + BenchSplit.register(pb, sizes) + BenchTrim.register(pb, sizes) + BenchReplace.register(pb, sizes) + BenchScripts.register(pb, sizes) + + fun tag benchmarks(bench: PonyBench) => + None