|
1 | | -# Performance: As Fast as `htmlspecialchars()`, Often 2-4x Faster |
| 1 | +# Performance: 4x Faster Than Calling `htmlspecialchars()` Yourself |
2 | 2 |
|
3 | | -SmartString's auto-encoding produces byte-identical output to `htmlspecialchars()` |
4 | | -and is usually faster: on a real-world mix of database fields, 2-4x faster. Most |
| 3 | +Our automatic encoding produces byte-identical output to `htmlspecialchars()` |
| 4 | +and is faster: about 4x on a real-world page. Most |
5 | 5 | values don't need encoding, and proving that with a scan costs less than encoding |
6 | 6 | them anyway. This page shows how that works, the |
7 | 7 | measurements, and the tests that keep the shortcut honest. |
8 | 8 |
|
| 9 | +The exact multiplier depends on the platform: our scans cost about the same |
| 10 | +everywhere, so the win tracks how slowly each platform's `htmlspecialchars()` |
| 11 | +runs. On a realistic page mix: |
| 12 | + |
| 13 | +- **Linux x64** - the most common web platform: **4.1x** |
| 14 | +- **Linux ARM** - Graviton-class hosts: **2.9x** |
| 15 | +- **Windows** - its PHP builds encode slowest: **11x**, and long clean fields |
| 16 | + reach 43x |
| 17 | + |
9 | 18 | ## How It Works |
10 | 19 |
|
11 | 20 | Every `<?= $row->title ?>` runs the cheapest check that can settle the value. |
@@ -47,39 +56,51 @@ echo $title; // SmartString: scans first, encodes only if nee |
47 | 56 |
|
48 | 57 | Measured on Linux x64, PHP 8.5 with opcache, on GitHub Actions' standard cloud |
49 | 58 | servers; output was verified byte-identical before every timing run. Timings are identical |
50 | | -whether you write `<?= $title ?>` or `echo $title;`. |
51 | | - |
52 | | -| Content | Size | Example | SmartString speed | |
53 | | -|--------------------------------|-------|------------------------------------------------------------------------------------|-------------------| |
54 | | -| Clean text - no `& < > " '` | 16 B | `Annual Report 2026` | 1.0x | |
55 | | -| Clean text - no `& < > " '` | 1 KB | a plain-text paragraph | 9.4x | |
56 | | -| Clean text - no `& < > " '` | 10 KB | a long field, nothing to encode | 13x | |
57 | | -| Has `& < > " '` | 16 B | `O'Brien & Co Ltd` | 0.5x | |
58 | | -| Has `& < > " '` | 1 KB | a paragraph with quotes | 3.6x | |
59 | | -| Has `& < > " '` | 10 KB | a 1,500-word article | 4.4x | |
60 | | -| Accented text - no `& < > " '` | 16 B | `Café Montréal QC` | 0.4x | |
61 | | -| Accented text - no `& < > " '` | 1 KB | a French paragraph | 3.8x | |
62 | | -| Accented text - no `& < > " '` | 10 KB | a French article | 4.8x | |
63 | | -| Realistic page mix | mixed | 70% 16B (two of 45 are names with specials), 15% 200B, 10% 1KB, 5% 1KB with quotes | 4.1x | |
64 | | -| Page mix + one 10 KB article | mixed | the mix above plus an article with quotes | 4.3x | |
65 | | - |
66 | | -2x = SmartString outputs the value in half the time `htmlspecialchars()` takes; |
67 | | -1.0x = same speed; below 1x = slower. |
68 | | - |
69 | | -**The bottom line: a realistic page encodes 2-4x faster on Linux (2.9x on ARM, |
70 | | -4.1x on x64) and 11x on Windows, whose PHP builds encode slowly (long clean |
71 | | -fields reach 43x there).** The content mix matches measured corpus rates: the |
72 | | -apostrophe is the only special that commonly appears in real text, `& < >` |
73 | | -live in names rather than prose, and the French rows carry accents at true |
74 | | -French density (~3% of characters). SmartString is slower in one case: short |
75 | | -fields with quotes or accents, where the quick scan finds something and full |
76 | | -encoding has to run anyway. That wastes about 0.2 microseconds per field - |
77 | | -you'd need 5,000 of them on one page to lose a millisecond - and the wins |
78 | | -everywhere else pay it back many times over; even a long article in the mix |
79 | | -barely moves the page total. This table comes from the repo's Speed Page Table |
80 | | -workflow |
| 59 | +whether you write `<?= $title ?>` or `echo $title;`. The test content matches |
| 60 | +the character mix of real English and French writing, measured on both classic |
| 61 | +and current text. |
| 62 | + |
| 63 | +| Content | Size | Example | Speed vs `htmlspecialchars()` | |
| 64 | +|--------------------------------|-------|---------------------------------|-------------------------------| |
| 65 | +| Clean text - no `& < > " '` | 16 B | `Annual Report 2026` | 1.0x | |
| 66 | +| Clean text - no `& < > " '` | 1 KB | a plain-text paragraph | 9.4x | |
| 67 | +| Clean text - no `& < > " '` | 10 KB | a long field, nothing to encode | 13x | |
| 68 | +| Has `& < > " '` | 16 B | `O'Brien & Co Ltd` | 0.5x | |
| 69 | +| Has `& < > " '` | 1 KB | a paragraph with quotes | 3.6x | |
| 70 | +| Has `& < > " '` | 10 KB | a 1,500-word article | 4.4x | |
| 71 | +| Accented text - no `& < > " '` | 16 B | `Café Montréal QC` | 0.4x | |
| 72 | +| Accented text - no `& < > " '` | 1 KB | a French paragraph | 3.8x | |
| 73 | +| Accented text - no `& < > " '` | 10 KB | a French article | 4.8x | |
| 74 | + |
| 75 | +Real pages combine those rows. Here is a news-article page priced field by |
| 76 | +field (the ~200 B caption sits between the measured sizes, so its numbers are |
| 77 | +interpolated): |
| 78 | + |
| 79 | +| Field | Table row | `htmlspecialchars()` | SmartString | Speed vs `htmlspecialchars()` | |
| 80 | +|------------------------------|------------------------|----------------------|--------------|-------------------------------| |
| 81 | +| Headline - `Mayor Says 'No'` | Has `& < > " '`, 16 B | 0.12 µs | 0.26 µs | 0.5x | |
| 82 | +| Author | Clean text, 16 B | 0.11 µs | 0.12 µs | 1.0x | |
| 83 | +| Category | Clean text, 16 B | 0.11 µs | 0.12 µs | 1.0x | |
| 84 | +| Date | Clean text, 16 B | 0.11 µs | 0.12 µs | 1.0x | |
| 85 | +| Photo caption | Clean text, ~200 B | ~1 µs | ~0.2 µs | ~5x | |
| 86 | +| Article body with quotes | Has `& < > " '`, 10 KB | 52.1 µs | 11.9 µs | 4.4x | |
| 87 | +| **Whole page** | | **~53.6 µs** | **~12.7 µs** | **4.2x** | |
| 88 | + |
| 89 | +The body settles the outcome: the five short fields together cost under two |
| 90 | +microseconds on either side, the quoted headline is the one field SmartString |
| 91 | +loses (by 0.14 µs), and the body alone saves 40 µs. A body pasted with smart |
| 92 | +quotes would contain none of `& < > " '`, land on the clean 10 KB row, and |
| 93 | +save even more. The measured page mix behind the bullets at the top prices |
| 94 | +out the same way. |
| 95 | + |
| 96 | +The one case where SmartString is slower: short fields with quotes or accents, |
| 97 | +where the scan finds something and full encoding has to run anyway. That costs |
| 98 | +about 0.2 microseconds per field - you'd need 5,000 of them on one page to |
| 99 | +lose a millisecond - and the wins everywhere else repay it many times over. |
| 100 | + |
| 101 | +These numbers come from the repo's Speed Page Table workflow |
81 | 102 | ([this run](https://github.com/interactivetools-com/SmartString/actions/runs/29868668475)); |
82 | | -run it yourself any time for fresh numbers. |
| 103 | +you can run it yourself any time to confirm the numbers. |
83 | 104 |
|
84 | 105 | ## How We Know It's Safe |
85 | 106 |
|
@@ -114,6 +135,15 @@ Reading is cheaper than transforming, and most values only need to be read. Ever |
114 | 135 | scan threshold comes from A/B benchmarks across PHP 8.1-8.5 on five OS and CPU |
115 | 136 | combinations, recorded in the repo at `.github/scripts/speed-results.md`. |
116 | 137 |
|
| 138 | +Three notes for careful readers. The baseline helper runs the common faster |
| 139 | +flags while SmartString always produces the stronger full-flag output; full |
| 140 | +flags would slow the baseline about 50% on long strings, so the multipliers |
| 141 | +above understate the win. Timings cover output, not construction - values |
| 142 | +from SmartArray or ZenDB arrive already wrapped, and constructing one |
| 143 | +yourself adds about 60ns per field. And a full pass of the benchmark matrix |
| 144 | +with opcache's tracing JIT enabled produced the same verdicts as the JIT-off |
| 145 | +runs shown here. |
| 146 | + |
117 | 147 | Most libraries and frameworks run the full encoder on every value, every time. |
118 | 148 | Checking first and skipping the work when there is nothing to do is, as far as |
119 | 149 | we know, unique to SmartString - the kind of care we try to put into everything |
|
0 commit comments