Skip to content

Optimization Progress

mkuch edited this page Jul 13, 2026 · 2 revisions

PDF Conversion Optimization Progress

Objective: process all todo*.md optimization notes, implement viable speedups, benchmark after each batch, and keep only changes that improve or preserve conversion speed.

Files Reviewed

  • todo.md
  • todo_part2.md
  • todo_v3.md

Benchmark Evidence

WSL Release benchmarks using build/linux-verify/rayomd --bench ... elegant normal.

Input Baseline avg Final avg Notes
tester.md 0.20 ms 0.12 ms 40% faster on final post-clean 1000-iteration run.
verifier ASCII 0.017 ms 0.015 ms About 12% faster; displayed as 0.02 ms due report formatting.
todo.md 1.93 ms 0.91 ms 53% faster on final post-clean 1000-iteration run.
todo_part2.md 2.64 ms 1.28 ms 52% faster on final post-clean 1000-iteration run.
todo_v3.md 2.29 ms 0.79 ms 66% faster on final 1000-iteration run.
emoji-only status-symbol doc not measured standard-font-ascii path Verifies supported emoji normalization can avoid Unicode path when no other Unicode remains.

Rejected experiment:

  • Direct-mapped glyph lookup cache: removed after tester.md worsened from 0.14 ms to 0.17 ms and todo_part2.md worsened from 1.31 ms to 1.35 ms.

Verification notes:

  • WSL Release build and tests/verify_cli.py completed.
  • Windows Release build completed from fresh build/windows-fix tree.
  • Windows benchmark mode initially failed for nested output paths because Win32 used one-level CreateDirectoryW; fixed with recursive directory creation.
  • Windows GUI-subsystem executable benchmarks were run hidden with Start-Process -Wait.

Windows Release benchmarks using build/windows-fix/rayomd.exe --bench ... elegant normal.

Input Windows final avg Notes
tester.md 0.35 ms 1000 iterations.
Windows ASCII 0.06 ms 5000 iterations.
todo.md 2.11 ms 1000 iterations.
todo_part2.md 3.94 ms 1000 iterations.
todo_v3.md 2.39 ms 1000 iterations.

Current Step

  1. Complete. All todo files were reviewed; implemented speedups are benchmarked and deferred items are documented.

Implemented In This Run

  • Reused HexText for faux-bold text instead of encoding/inserting CIDs twice.
  • Replaced std::set<uint16_t> CID collection with a bit-backed collector and sorted vector.
  • Computed the CID cache key once per Unicode PDF build and reused it across font caches.
  • Replaced std::to_string/ostringstream PDF hot-path assembly with reserved strings and to_chars append helpers.
  • Hoisted TextWidth scale calculation and removed WrapCodeLine string copies.
  • Converted UTF-8 conversion to std::string_view inputs to avoid substring allocations.
  • Merged styled spaces into adjacent spans where visually equivalent, reducing PaintText/HexText calls and PDF content size.
  • Added fast paths for plain StripInlineMarkdown, rule-line checks, and non-table line rejection.
  • Added Linux CLI mmap reads with ifstream fallback.
  • Added GUI/native PDF output-buffer reuse.
  • Treated supported status-symbol emoji as ASCII-path-safe after normalization.
  • Changed CID width caching from unordered_map to a direct lazy array for faster large Unicode documents.
  • Added a legacy stress benchmark helper to generate large mixed-style Markdown corpora and run single-file plus 10/100/1000-file batch benchmarks with max-RSS reporting. This helper was later removed after consolidation into tools/benchmark.py.

Deferred Or Skipped

  • Full TTF glyph-ID remapping for cmap/hmtx/name subsetting: not implemented yet; needs correctness work because current subset preserves original glyph IDs.
  • Parser arena/streaming refactor: not implemented yet; requires changing Block ownership/lifetime or rendering directly from a scanner.
  • simdutf: vendored as an optional CMake experiment, but defaults OFF because stress benchmarks were slower.
  • StringZilla dependency: not added; external dependency and licensing/build integration need separate decision.
  • Knuth-Plass line breaking: skipped for this performance goal because it improves layout quality rather than conversion speed.

Research Follow-Up Benchmark Round

Triggered by research notes in research_notes.md.

Accepted/practical result:

  • Added a legacy WSL ext4 wrapper to copy the project into native WSL storage, build there, and run the same generated stress corpus. This helper was later removed after consolidation into tools/benchmark.py.
  • Native WSL ext4 is the biggest measured win for Linux batch conversion. On the same stress harness, batch1000 improved from 10.15 s on /mnt/e to 2.20 s on WSL ext4 in the first controlled comparison.
  • Final kept default run on WSL ext4 measured:
Case Time RSS
style_mix --bench 2.85 ms avg n/a
large_mix --bench 17.39 ms avg n/a
batch10 0.039 s wall 8200 KB
random100 0.371 s wall 14240 KB
batch1000 1.844 s wall 47024 KB

Rejected experiments from this round:

  • simdutf UTF conversion, UTF-16 widening path: slower on WSL ext4 (style_mix 2.95 ms, large_mix 17.66 ms, batch1000 2.300 s). Kept optional but defaulted OFF.
  • simdutf Linux UTF-32 path: still slower on single-doc benches (style_mix 3.13 ms, large_mix 19.58 ms). Reverted to the narrower optional UTF-16 path and kept default OFF.
  • PDF object move/copy reduction: regressed the full stress suite (style_mix 3.74 ms, large_mix 20.75 ms, batch1000 2.540 s). Reverted.

Focused large-Unicode simdutf follow-up:

  • Used a legacy large-Unicode WSL ext4 helper to compare RAYOMD_USE_SIMDUTF=OFF vs ON on native WSL ext4. This helper was later removed after consolidation into tools/benchmark.py.
  • Raw report: benchmark-output/simdutf-large-unicode/simdutf_20260606/simdutf_large_unicode.md.
  • Result remained mixed, so default stays OFF:
Doc OFF avg ms ON avg ms ON delta
unicode_1mb 68.19 66.20 -2.92%
unicode_2_5mb 178.60 217.98 +22.05%
unicode_5mb 425.31 387.09 -8.99%
unicode_10mb 800.01 848.21 +6.02%

Windows verification:

  • Fresh Windows Release build succeeded in build/windows-final2.
  • Hidden Windows --bench tester.md smoke test: 100 iterations, 0.34 ms average, 100395 average PDF bytes.
  • RAYOMD_USE_SIMDUTF=ON Windows build was not verified because the escalation request hit the session usage limit; the option is OFF by default and Linux ON build was verified.

Commercial Benchmark Campaign

Run date: 2026-06-06.

  • Added a legacy commercial benchmark helper for deterministic synthetic corpora, 50+ test campaigns, JSON output, Markdown reports, and Linux/Windows memory reporting. This helper was later removed after consolidation into tools/benchmark.py.
  • Added a legacy commercial WSL ext4 wrapper to run the Linux campaign on native WSL ext4 and copy metrics back without generated PDFs. This helper was later removed after consolidation into tools/benchmark.py.
  • Raised Markdown input file cap from 64 MiB to 128 MiB in Linux CLI and Windows CLI/GUI paths so 100 MiB inputs can be benchmarked.
  • Ran 60 total commercial benchmark cases: 48 Linux WSL ext4, 12 Windows, all passed.
  • Covered --bench, --export, --batch, --stdin-batch, --serve, all three styles, standard margins, and generated files from about 100 KB to 100 MB.
  • Headline local results: Linux 100 MB export 7.03 s, Windows 100 MB export 13.48 s, Linux 100 MB warm conversion 6.48 s avg, Linux peak warm throughput 21.91 MiB/s, Linux 100-file stdin batch 0.144 s.
  • Extra Windows Pandoc compatibility smoke test passed separately: 100 KB ASCII export in 8.11 s, output 151,524 bytes.
  • Full tracked summary: ../benchmarks/commercial_benchmark_summary.md.
  • Raw ignored reports: benchmark-output/commercial/reports/commercial_benchmark_combined_combined_commercial_20260606.md and .json.

Stress Benchmark Round

Generated by the legacy stress helper, later removed after consolidation into tools/benchmark.py, with:

  • single/style_mix.md (36 KB, heavy inline styles)
  • single/large_mix.md (213 KB, headings, paragraphs, tables, lists, quotes, code, math, Unicode)
  • batch10/
  • random100/
  • batch1000/

Baseline stress run on accepted optimized code:

Case Time RSS
style_mix --bench 2.78 ms avg n/a
large_mix --bench 16.62 ms avg n/a
batch10 0.116 s wall 8412 KB
random100 1.034 s wall 14516 KB
batch1000 8.216 s wall 47316 KB

Rejected larger optimization experiments:

  • Precomputed width+hex paragraph spans: improved batch1000 to 7.947 s once, but regressed style_mix to 2.97 ms and large_mix to 16.73 ms.
  • Width-only paragraph spans: kept style_mix flat at 2.78 ms, but regressed large_mix to 17.03 ms and batch1000 to 8.934 s.
  • Direct BMP glyph array: regressed style_mix to 2.96 ms, large_mix to 17.31 ms, and batch1000 to 9.244 s.
  • POSIX output writes for Linux CLI: regressed style_mix to 3.39 ms, large_mix to 18.54 ms, and batch1000 to 8.426 s.
  • Manual non-Windows UTF-8 decoder: mixed results; one run improved large_mix to 15.74 ms and batch1000 to 7.979 s, but rerun regressed style_mix to 2.86 ms and batch1000 to 8.548 s. Reverted as not stable enough.

Final post-revert stress sanity run:

Case Time RSS
style_mix --bench 2.88 ms avg n/a
large_mix --bench 16.78 ms avg n/a
batch10 0.124 s wall 8220 KB
random100 1.067 s wall 14348 KB
batch1000 8.248 s wall 46936 KB

Todo Item Audit

todo.md

  • 1.1 Parser string_view: already mostly present; added plain inline/table/rule fast paths. Block::text ownership refactor deferred.
  • 1.2 IsBlockStart redundant parsing: already solved by ClassifyLine.
  • 1.3 SIMD line/ascii scanning: already present.
  • 1.4 Arena allocator: deferred; current parser returns owning Block data.
  • 2.1 Font object/map caches: already present; improved by computing CID key once.
  • 2.2 Font subsetting: existing glyf/loca subset retained; full glyph-ID remap for cmap/hmtx deferred.
  • 2.3 Width lookup: changed from hash-map lazy cache to direct lazy CID cache for large-document speed.
  • 2.4 BMP glyph map: already sparse unordered_map.
  • 3.1 ostringstream: removed from tiny_pdf.cpp PDF generation.
  • 3.2 F() allocations: hot PDF paths use AppendF; F() remains public/reporting helper.
  • 3.3 Single small-file write: already present in Win32 writer.
  • 3.4 No zlib: left unchanged intentionally.
  • 3.5 PdfObjects::Build reserve/to_string: reserve already present; integer appends now use to_chars.
  • 4.1 WrapText: partially improved with view/code-line changes; full output-buffer redesign deferred.
  • 4.2 UTF-8 conversion: improved to string_view inputs and fewer substring allocations; full per-block lazy UTF-8 rendering deferred.
  • 4.3 Hex table: already present.
  • 4.4 Page reserve: already present.
  • 5.1 Font cache in serve: already present; CID key reuse improved.
  • 5.2 File reads: Win32 mmap already present; Linux CLI mmap added.
  • 5.3 Output buffer reuse: CLI already reused buffers; GUI/native export now reuses a thread-local buffer.
  • 6.1 GUI stats: already updated only on text changes.
  • 6.2 WaitMessage/vsync: WaitMessage already present; vsync left unchanged.

todo_part2.md

  • A.1 Duplicate HexText on faux-bold: implemented.
  • A.2 Oversized word fallback: improved by avoiding one-character TextWidth calls; binary-search splitter not needed for measured win.
  • A.3 MakeWidths integer allocations: implemented with AppendInt.
  • B.1 usedCids std::set: replaced with bit-backed collector and sorted vector.
  • B.2 Repeated MakeCidKey: computed once per Unicode PDF build.
  • B.3 Full cmap/hmtx/name pruning: deferred pending safe glyph-ID remap.
  • B.4 Lazy cmap parsing: deferred; startup-only in current benchmark because font is cached.
  • C.1 UTF-8 fragment conversion: partially improved with view conversion and no substring allocation.
  • C.2 Styled spaces: implemented by merging spaces into adjacent spans when visually equivalent.
  • C.3 TextWidth division: implemented with precomputed scale.
  • C.4 cp-to-cid cache: tested direct glyph cache and rejected because it was slower.
  • C.5 WrapCodeLine(wstring_view): implemented.
  • D.1 SplitStyledWords reserve: implemented.
  • D.2 SplitTableRow trim allocation: reduced via TrimView.
  • D.3 PdfObjects::Build to_string: implemented with AppendSize/AppendInt.
  • D.4 Numbered list marker to_string: implemented with reserved strings and AppendInt.
  • D.5 NormalizeSymbols copy/replace work: added no-symbol fast exits and plain inline fast path.
  • E.1 SIMD mixed-mode renderer: deferred as future architecture work.
  • E.3 Faster integer formatting: implemented via C++17 to_chars.
  • E.4 Parser arena: deferred with 1.4.

todo_v3.md

  • Streaming parser / incremental read+parse: deferred as larger architecture work; current gains came from low-risk parser/rendering changes.
  • simdutf: vendored as optional RAYOMD_USE_SIMDUTF; measured slower on stress benchmarks and left OFF by default.
  • Arena allocator: deferred with todo.md 1.4.
  • SIMD ascii/line scan: already present.
  • Knuth-Plass: skipped for speed goal because it is a quality/layout feature.
  • StringZilla: not added; external dependency integration deferred.
  • Emoji fallback before path decision: implemented for supported status symbols.
  • usedCids set: implemented with bit-backed collector.
  • TextWidth Codepoints() allocation: TextWidth uses ForEachCodepoint; unused allocating helper removed.
  • WrapText normalization copy: no NormalizeSpaces hot-path copy exists in current code; wrapping already scans views.
  • Precomputed width scale: implemented.

Windows -O2 Release Experiment (2026-07-10)

  • Release compiler tuning changed MinGW Windows builds from -Os to -O2 for RayoMD 1.1.7.
  • Five alternating warm tester.md runs (1,000 iterations, modern normal) had medians of 1.52 ms for -Os and 1.41 ms for -O2 (about 7% faster).
  • Earlier three-run checks also favored -O2: README.md ASCII median 2.74 -> 2.63 ms; Unicode smoke median 1.83 -> 1.71 ms; initial tester.md median 1.73 -> 1.58 ms.
  • Tradeoff: Windows executable size increased from 2,374,656 to 2,706,432 bytes (about 14%). The faster setting was retained deliberately and should be rechecked when toolchain or packaging changes.
  • A renderer experiment passing table cells through string_view was rejected: direct alternating table benchmarks regressed from 2.30 ms to 2.61 ms median.

Clone this wiki locally