Skip to content

Commit 7d3d826

Browse files
authored
Merge pull request #11 from lpgauth/perf/simd-fused-utf8-escape
SIMD-fused UTF-8 validation and JSON escape in encoder
2 parents 80bae0e + 9e9f3c9 commit 7d3d826

4 files changed

Lines changed: 552 additions & 90 deletions

File tree

README.md

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -147,64 +147,64 @@ Apple M2 Pro, OTP 28, Elixir 1.19:
147147

148148
| Library | ips | mean | median | p99 | memory |
149149
|---|---|---|---|---|---|
150-
| torque | **258.5K** | **3.87 μs** | **3.54 μs** | **8.29 μs** | **1.56 KB** |
151-
| simdjsone | 185.0K | 5.41 μs | 4.96 μs | 11.50 μs | 1.59 KB |
152-
| jiffy | 152.0K | 6.58 μs | 5.83 μs | 17.83 μs | **1.56 KB** |
153-
| otp json | 130.9K | 7.64 μs | 7.17 μs | 15.54 μs | 7.73 KB |
154-
| jason | 108.0K | 9.26 μs | 8.50 μs | 20.21 μs | 9.54 KB |
150+
| torque | **262.0K** | **3.82 μs** | **3.54 μs** | **8.08 μs** | **1.56 KB** |
151+
| simdjsone | 187.7K | 5.33 μs | 4.96 μs | 12.46 μs | 1.59 KB |
152+
| jiffy | 156.1K | 6.41 μs | 5.79 μs | 16.17 μs | **1.56 KB** |
153+
| otp json | 130.9K | 7.64 μs | 7.21 μs | 15.17 μs | 7.73 KB |
154+
| jason | 107.1K | 9.34 μs | 8.54 μs | 20.92 μs | 9.54 KB |
155155

156156
### Parse + Get
157157

158158
| Library | ips | mean | median | p99 | memory |
159159
|---|---|---|---|---|---|
160-
| torque parse+get_many_nil | **193.4K** | **5.17 μs** | **4.50 μs** | 10.92 μs | 1.59 KB |
161-
| torque parse+get_many | 187.4K | 5.34 μs | 4.54 μs | **10.63 μs** | **1.58 KB** |
162-
| torque parse+get | 152.6K | 6.55 μs | 5.83 μs | 15.63 μs | 2.80 KB |
163-
| simdjsone parse+get | 119.6K | 8.36 μs | 6.13 μs | 34.88 μs | 2.28 KB |
160+
| torque parse+get_many_nil | **196.5K** | **5.09 μs** | **4.46 μs** | **10.79 μs** | 1.59 KB |
161+
| torque parse+get_many | 190.3K | 5.26 μs | 4.54 μs | 10.88 μs | **1.58 KB** |
162+
| torque parse+get | 155.6K | 6.43 μs | 5.75 μs | 14.29 μs | 2.80 KB |
163+
| simdjsone parse+get | 125.2K | 7.99 μs | 5.83 μs | 30.83 μs | 2.28 KB |
164164

165165
### Encode (1.2 KB)
166166

167167
| Library | ips | mean | median | p99 | memory |
168168
|---|---|---|---|---|---|
169-
| otp json: map => iodata | **1055.0K** | **0.95 μs** | **0.88 μs** | 1.63 μs | 3928 B |
170-
| torque: proplist => iodata | 843.0K | 1.19 μs | 1.08 μs | **1.54 μs** | **64 B** |
171-
| torque: proplist => binary | 834.0K | 1.20 μs | 1.08 μs | **1.54 μs** | 88 B |
172-
| torque: map => iodata | 756.5K | 1.32 μs | 1.21 μs | 1.75 μs | **64 B** |
173-
| torque: map => binary | 744.7K | 1.34 μs | 1.21 μs | 1.75 μs | 88 B |
174-
| jason: map => iodata | 606.2K | 1.65 μs | 1.50 μs | 3.08 μs | 3488 B |
175-
| jiffy: proplist => iodata | 562.0K | 1.78 μs | 1.54 μs | 3.33 μs | 120 B |
176-
| otp json: map => binary | 548.8K | 1.82 μs | 1.54 μs | 5.25 μs | 3992 B |
177-
| jiffy: map => iodata | 462.5K | 2.16 μs | 1.87 μs | 6.00 μs | 824 B |
178-
| simdjsone: proplist => iodata | 445.6K | 2.24 μs | 2.04 μs | 3.50 μs | 184 B |
179-
| jason: map => binary | 381.0K | 2.62 μs | 2.33 μs | 7.17 μs | 3552 B |
180-
| simdjsone: map => iodata | 370.9K | 2.70 μs | 2.42 μs | 6.04 μs | 888 B |
169+
| torque: proplist => iodata | **1287.4K** | **0.78 μs** | **0.71 μs** | **0.96 μs** | **64 B** |
170+
| torque: proplist => binary | 1259.3K | 0.79 μs | **0.71 μs** | **0.96 μs** | 88 B |
171+
| torque: map => iodata | 1065.7K | 0.94 μs | 0.88 μs | 1.17 μs | **64 B** |
172+
| otp json: map => iodata | 1056.7K | 0.95 μs | 0.88 μs | 1.54 μs | 3928 B |
173+
| torque: map => binary | 1047.9K | 0.95 μs | 0.88 μs | 1.17 μs | 88 B |
174+
| jason: map => iodata | 611.8K | 1.63 μs | 1.50 μs | 3.08 μs | 3848 B |
175+
| otp json: map => binary | 580.6K | 1.72 μs | 1.54 μs | 3.42 μs | 3992 B |
176+
| jiffy: proplist => iodata | 571.2K | 1.75 μs | 1.50 μs | 3.33 μs | 120 B |
177+
| jiffy: map => iodata | 487.5K | 2.05 μs | 1.79 μs | 4.42 μs | 824 B |
178+
| simdjsone: proplist => iodata | 452.6K | 2.21 μs | 2.00 μs | 3.38 μs | 184 B |
179+
| jason: map => binary | 390.7K | 2.56 μs | 2.33 μs | 6.13 μs | 3912 B |
180+
| simdjsone: map => iodata | 384.0K | 2.60 μs | 2.38 μs | 5.50 μs | 888 B |
181181

182182
### Decode (750 KB)
183183

184184
| Library | ips | mean | median | p99 | memory |
185185
|---|---|---|---|---|---|
186-
| torque | **486.5** | **2.06 ms** | **1.58 ms** | **3.23 ms** | **1.56 KB** |
187-
| simdjsone | 436.3 | 2.29 ms | 1.82 ms | 3.51 ms | **1.56 KB** |
188-
| otp json | 193.0 | 5.18 ms | 5.20 ms | 6.17 ms | 2.49 MB |
189-
| jason | 137.8 | 7.26 ms | 7.09 ms | 8.66 ms | 3.55 MB |
190-
| jiffy | 107.9 | 9.27 ms | 9.47 ms | 10.62 ms | 5.53 MB |
186+
| torque | **525.2** | **1.90 ms** | **1.58 ms** | **2.85 ms** | **1.56 KB** |
187+
| simdjsone | 462.7 | 2.16 ms | 1.82 ms | 3.18 ms | **1.56 KB** |
188+
| otp json | 198.6 | 5.04 ms | 5.08 ms | 5.87 ms | 2.49 MB |
189+
| jason | 145.3 | 6.88 ms | 6.88 ms | 7.28 ms | 3.55 MB |
190+
| jiffy | 109.0 | 9.17 ms | 9.35 ms | 10.05 ms | 5.53 MB |
191191

192192
### Encode (750 KB)
193193

194194
| Library | ips | mean | median | p99 | memory |
195195
|---|---|---|---|---|---|
196-
| torque: proplist => iodata | **613.7** | **1.63 ms** | **1.62 ms** | 1.79 ms | **64 B** |
197-
| torque: proplist => binary | 613.1 | **1.63 ms** | **1.62 ms** | **1.78 ms** | 88 B |
198-
| torque: map => iodata | 572.4 | 1.75 ms | 1.73 ms | 2.03 ms | **64 B** |
199-
| torque: map => binary | 570.8 | 1.75 ms | 1.74 ms | 1.93 ms | 88 B |
200-
| otp json: map => iodata | 532.6 | 1.88 ms | 1.83 ms | 2.24 ms | 5.40 MB |
201-
| jiffy: proplist => iodata | 314.3 | 3.18 ms | 2.87 ms | 4.21 ms | 37.7 KB |
202-
| jiffy: map => iodata | 282.8 | 3.54 ms | 3.27 ms | 4.61 ms | 1.06 MB |
203-
| jason: map => iodata | 281.0 | 3.56 ms | 3.55 ms | 4.57 ms | 4.96 MB |
204-
| otp json: map => binary | 259.0 | 3.86 ms | 3.71 ms | 5.62 ms | 5.40 MB |
205-
| simdjsone: proplist => iodata | 239.5 | 4.18 ms | 3.88 ms | 5.18 ms | 37.7 KB |
206-
| simdjsone: map => iodata | 214.9 | 4.65 ms | 4.41 ms | 5.78 ms | 1.06 MB |
207-
| jason: map => binary | 190.5 | 5.25 ms | 5.28 ms | 6.59 ms | 4.96 MB |
196+
| torque: proplist => iodata | **1274.1** | **0.78 ms** | **0.78 ms** | **0.90 ms** | **64 B** |
197+
| torque: proplist => binary | 1273.9 | 0.79 ms | **0.78 ms** | **0.90 ms** | 88 B |
198+
| torque: map => iodata | 1103.3 | 0.91 ms | 0.90 ms | 1.03 ms | **64 B** |
199+
| torque: map => binary | 1103.2 | 0.91 ms | 0.90 ms | 1.02 ms | 88 B |
200+
| otp json: map => iodata | 522.8 | 1.91 ms | 1.88 ms | 2.20 ms | 5.40 MB |
201+
| jiffy: proplist => iodata | 328.9 | 3.04 ms | 2.79 ms | 4.11 ms | 37.7 KB |
202+
| jiffy: map => iodata | 296.1 | 3.38 ms | 3.28 ms | 3.94 ms | 1.06 MB |
203+
| otp json: map => binary | 262.0 | 3.82 ms | 3.74 ms | 5.36 ms | 5.40 MB |
204+
| simdjsone: proplist => iodata | 238.2 | 4.20 ms | 3.89 ms | 5.19 ms | 37.7 KB |
205+
| simdjsone: map => iodata | 219.5 | 4.55 ms | 4.31 ms | 5.55 ms | 1.06 MB |
206+
| jason: map => iodata | 209.3 | 4.78 ms | 4.77 ms | 5.60 ms | 4.96 MB |
207+
| jason: map => binary | 151.7 | 6.59 ms | 6.57 ms | 7.73 ms | 4.96 MB |
208208

209209
Run benchmarks locally:
210210

native/torque_nif/src/encoder.rs

Lines changed: 6 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ fn encode_map_key(
158158
TermType::Atom => {
159159
let mut atom_buf = [0u8; 256];
160160
let name = unsafe { atom_to_stack_buf(env_raw, key.as_c_arg(), &mut atom_buf)? };
161-
escape_bytes(name, buf);
161+
crate::escape::escape_to_vec(name, buf);
162162
}
163163
TermType::Binary => {
164164
let mut bin = MaybeUninit::<ErlNifBinary>::uninit();
@@ -168,10 +168,8 @@ fn encode_map_key(
168168
}
169169
let bin = bin.assume_init();
170170
let slice = std::slice::from_raw_parts(bin.data, bin.size);
171-
if std::str::from_utf8(slice).is_err() {
172-
return Err(EncodeError::InvalidUtf8);
173-
}
174-
escape_bytes(slice, buf);
171+
crate::escape::validate_and_escape_to_vec(slice, buf)
172+
.map_err(|_| EncodeError::InvalidUtf8)?;
175173
}
176174
}
177175
_ => return Err(EncodeError::InvalidKey),
@@ -221,11 +219,9 @@ fn encode_binary(
221219
}
222220
let bin = bin.assume_init();
223221
let slice = std::slice::from_raw_parts(bin.data, bin.size);
224-
if std::str::from_utf8(slice).is_err() {
225-
return Err(EncodeError::InvalidUtf8);
226-
}
227222
buf.push(b'"');
228-
escape_bytes(slice, buf);
223+
crate::escape::validate_and_escape_to_vec(slice, buf)
224+
.map_err(|_| EncodeError::InvalidUtf8)?;
229225
buf.push(b'"');
230226
}
231227
Ok(())
@@ -281,7 +277,7 @@ fn encode_atom(env_raw: *mut ErlNifEnv, term: Term, buf: &mut Vec<u8>) -> Result
281277
let mut atom_buf = [0u8; 256];
282278
let name = unsafe { atom_to_stack_buf(env_raw, raw, &mut atom_buf)? };
283279
buf.push(b'"');
284-
escape_bytes(name, buf);
280+
crate::escape::escape_to_vec(name, buf);
285281
buf.push(b'"');
286282
}
287283
Ok(())
@@ -358,45 +354,3 @@ fn encode_proplist(
358354
buf.push(b'}');
359355
Ok(())
360356
}
361-
362-
#[inline]
363-
fn escape_bytes(bytes: &[u8], buf: &mut Vec<u8>) {
364-
let len = bytes.len();
365-
let mut start = 0;
366-
367-
for i in 0..len {
368-
let b = bytes[i];
369-
let escape = match b {
370-
b'"' => b"\\\"" as &[u8],
371-
b'\\' => b"\\\\",
372-
b'\n' => b"\\n",
373-
b'\r' => b"\\r",
374-
b'\t' => b"\\t",
375-
0x08 => b"\\b",
376-
0x0C => b"\\f",
377-
b if b < 0x20 => {
378-
if start < i {
379-
buf.extend_from_slice(&bytes[start..i]);
380-
}
381-
buf.extend_from_slice(b"\\u00");
382-
buf.push(HEX_DIGITS[(b >> 4) as usize]);
383-
buf.push(HEX_DIGITS[(b & 0x0F) as usize]);
384-
start = i + 1;
385-
continue;
386-
}
387-
_ => {
388-
continue;
389-
}
390-
};
391-
if start < i {
392-
buf.extend_from_slice(&bytes[start..i]);
393-
}
394-
buf.extend_from_slice(escape);
395-
start = i + 1;
396-
}
397-
if start < len {
398-
buf.extend_from_slice(&bytes[start..len]);
399-
}
400-
}
401-
402-
const HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";

0 commit comments

Comments
 (0)