|
| 1 | +//! Optimized SIMD routines for escaping JSON strings. |
| 2 | +//! |
| 3 | +//! ## <div class="warning">Important</div> |
| 4 | +//! |
| 5 | +//! On aarch64 NEON hosts the available register width is **128** bits, which is narrower than the lookup table this implementation prefers. As a result the SIMD path may not outperform the generic fallback, which is reflected in the benchmark numbers below. |
| 6 | +//! |
| 7 | +//! On some modern macOS devices with larger register numbers, the SIMD path may outperform the generic fallback, see the [M3 max benchmark](#apple-m3-max) below. |
| 8 | +//! |
| 9 | +//! ### Note |
| 10 | +//! |
| 11 | +//! The `force_aarch64_neon` feature flag can be used to force use of the neon implementation on aarch64. This is useful for the benchmark. |
| 12 | +//! |
| 13 | +//! ## Benchmarks |
| 14 | +//! |
| 15 | +//! Numbers below come from `cargo bench` runs on GitHub Actions hardware. Criterion reports are summarized to make it easier to spot relative performance. "vs fastest" shows how much slower each implementation is compared to the fastest entry in the table (1.00× means fastest). |
| 16 | +//! |
| 17 | +//! ### GitHub Actions x86_64 (`ubuntu-latest`) |
| 18 | +//! |
| 19 | +//! `AVX2` enabled. |
| 20 | +//! |
| 21 | +//! **RxJS payload (~10k iterations)** |
| 22 | +//! |
| 23 | +//! | Implementation | Median time | vs fastest | |
| 24 | +//! | --------------------- | ------------- | ---------- | |
| 25 | +//! | **`escape simd`** | **345.06 µs** | **1.00×** | |
| 26 | +//! | `escape v_jsonescape` | 576.25 µs | 1.67× | |
| 27 | +//! | `escape generic` | 657.94 µs | 1.91× | |
| 28 | +//! | `serde_json` | 766.72 µs | 2.22× | |
| 29 | +//! | `json-escape` | 782.65 µs | 2.27× | |
| 30 | +//! |
| 31 | +//! **Fixtures payload (~300 iterations)** |
| 32 | +//! |
| 33 | +//! | Implementation | Median time | vs fastest | |
| 34 | +//! | --------------------- | ------------ | ---------- | |
| 35 | +//! | **`escape simd`** | **12.84 ms** | **1.00×** | |
| 36 | +//! | `escape v_jsonescape` | 19.66 ms | 1.53× | |
| 37 | +//! | `escape generic` | 22.53 ms | 1.75× | |
| 38 | +//! | `serde_json` | 24.65 ms | 1.92× | |
| 39 | +//! | `json-escape` | 26.64 ms | 2.07× | |
| 40 | +//! |
| 41 | +//! ### GitHub Actions aarch64 (`ubuntu-24.04-arm`) |
| 42 | +//! |
| 43 | +//! Neon enabled. |
| 44 | +//! |
| 45 | +//! **RxJS payload (~10k iterations)** |
| 46 | +//! |
| 47 | +//! | Implementation | Median time | vs fastest | |
| 48 | +//! | --------------------- | ------------- | ---------- | |
| 49 | +//! | **`escape generic`** | **546.89 µs** | **1.00×** | |
| 50 | +//! | `escape simd` | 589.29 µs | 1.08× | |
| 51 | +//! | `serde_json` | 612.33 µs | 1.12× | |
| 52 | +//! | `json-escape` | 624.66 µs | 1.14× | |
| 53 | +//! | `escape v_jsonescape` | 789.14 µs | 1.44× | |
| 54 | +//! |
| 55 | +//! **Fixtures payload (~300 iterations)** |
| 56 | +//! |
| 57 | +//! | Implementation | Median time | vs fastest | |
| 58 | +//! | --------------------- | ------------ | ---------- | |
| 59 | +//! | **`escape generic`** | **17.81 ms** | **1.00×** | |
| 60 | +//! | `serde_json` | 19.77 ms | 1.11× | |
| 61 | +//! | `json-escape` | 20.84 ms | 1.17× | |
| 62 | +//! | `escape simd` | 21.04 ms | 1.18× | |
| 63 | +//! | `escape v_jsonescape` | 25.57 ms | 1.44× | |
| 64 | +//! |
| 65 | +//! ### GitHub Actions macOS (`macos-latest`) |
| 66 | +//! |
| 67 | +//! Apple M1 chip |
| 68 | +//! |
| 69 | +//! **RxJS payload (~10k iterations)** |
| 70 | +//! |
| 71 | +//! | Implementation | Median time | vs fastest | |
| 72 | +//! | --------------------- | ------------- | ---------- | |
| 73 | +//! | **`escape generic`** | **759.07 µs** | **1.00×** | |
| 74 | +//! | `escape simd` | 764.98 µs | 1.01× | |
| 75 | +//! | `serde_json` | 793.91 µs | 1.05× | |
| 76 | +//! | `json-escape` | 868.21 µs | 1.14× | |
| 77 | +//! | `escape v_jsonescape` | 926.00 µs | 1.22× | |
| 78 | +//! |
| 79 | +//! **Fixtures payload (~300 iterations)** |
| 80 | +//! |
| 81 | +//! | Implementation | Median time | vs fastest | |
| 82 | +//! | --------------------- | ------------ | ---------- | |
| 83 | +//! | **`serde_json`** | **26.41 ms** | **1.00×** | |
| 84 | +//! | `escape generic` | 26.43 ms | 1.00× | |
| 85 | +//! | `escape simd` | 26.42 ms | 1.00× | |
| 86 | +//! | `json-escape` | 28.94 ms | 1.10× | |
| 87 | +//! | `escape v_jsonescape` | 29.22 ms | 1.11× | |
| 88 | +//! |
| 89 | +//! ### Apple M3 Max |
| 90 | +//! |
| 91 | +//! **RxJS payload (~10k iterations)** |
| 92 | +//! |
| 93 | +//! | Implementation | Median time | vs fastest | |
| 94 | +//! | --------------------- | ------------- | ---------- | |
| 95 | +//! | **`escape simd`** | **307.20 µs** | **1.00×** | |
| 96 | +//! | `escape generic` | 490.00 µs | 1.60× | |
| 97 | +//! | `serde_json` | 570.35 µs | 1.86× | |
| 98 | +//! | `escape v_jsonescape` | 599.72 µs | 1.95× | |
| 99 | +//! | `json-escape` | 644.73 µs | 2.10× | |
| 100 | +//! |
| 101 | +//! **Fixtures payload (~300 iterations)** |
| 102 | +//! |
| 103 | +//! | Implementation | Median time | vs fastest | |
| 104 | +//! | --------------------- | ------------ | ---------- | |
| 105 | +//! | **`escape generic`** | **17.89 ms** | **1.00×** | |
| 106 | +//! | **`escape simd`** | **17.92 ms** | **1.00×** | |
| 107 | +//! | `serde_json` | 19.78 ms | 1.11× | |
| 108 | +//! | `escape v_jsonescape` | 21.09 ms | 1.18× | |
| 109 | +//! | `json-escape` | 22.43 ms | 1.25× | |
| 110 | +
|
1 | 111 | #[cfg(target_arch = "x86_64")]
|
2 | 112 | mod x86;
|
3 | 113 |
|
@@ -60,19 +170,8 @@ pub(crate) const HEX_BYTES: [(u8, u8); 256] = {
|
60 | 170 | bytes
|
61 | 171 | };
|
62 | 172 |
|
63 |
| -#[macro_export] |
64 |
| -// We only use our own error type; no need for From conversions provided by the |
65 |
| -// standard library's try! macro. This reduces lines of LLVM IR by 4%. |
66 |
| -macro_rules! tri { |
67 |
| - ($e:expr $(,)?) => { |
68 |
| - match $e { |
69 |
| - core::result::Result::Ok(val) => val, |
70 |
| - core::result::Result::Err(err) => return core::result::Result::Err(err), |
71 |
| - } |
72 |
| - }; |
73 |
| -} |
74 |
| - |
75 | 173 | #[inline]
|
| 174 | +/// Cross platform generic implementation without any platform specific instructions |
76 | 175 | pub fn escape_generic<S: AsRef<str>>(input: S) -> String {
|
77 | 176 | let s = input.as_ref();
|
78 | 177 | let bytes = s.as_bytes();
|
@@ -133,6 +232,7 @@ pub fn escape_generic<S: AsRef<str>>(input: S) -> String {
|
133 | 232 | }
|
134 | 233 |
|
135 | 234 | /// Main entry point for JSON string escaping with SIMD acceleration
|
| 235 | +/// If the platform is supported, the SIMD path will be used. Otherwise, the generic fallback will be used. |
136 | 236 | pub fn escape<S: AsRef<str>>(input: S) -> String {
|
137 | 237 | #[cfg(target_arch = "x86_64")]
|
138 | 238 | {
|
|
0 commit comments