Skip to content

Commit 29245b7

Browse files
CopilotBrooooooklyn
andcommitted
Implement AVX512-accelerated JSON string escaping with runtime feature detection
Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com>
1 parent 2fe2bec commit 29245b7

4 files changed

Lines changed: 174 additions & 4 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ license = "MIT"
1010
publish = true
1111
readme = "README.md"
1212
repository = "https://github.com/oxc-project/oxc-sourcemap"
13-
rust-version = "1.85.0"
13+
rust-version = "1.89.0"
1414
description = "Basic sourcemap handling for Rust"
1515

1616
# <https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html>

benches/simple.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
use criterion::{Criterion, criterion_group, criterion_main};
2-
use oxc_sourcemap::{SourceMap, SourceMapBuilder};
2+
use oxc_sourcemap::{SourceMap, SourceMapBuilder, escape_json_string, escape_json_string_fallback};
3+
4+
pub fn bench_json_escaping(c: &mut Criterion) {
5+
let long_clean = "abcdefghijklmnopqrstuvwxyz".repeat(50);
6+
let long_quotes = "\"test\"".repeat(100);
7+
8+
let test_strings = vec![
9+
("short_clean", "simple string without escapes"),
10+
("short_quotes", "string with \"quotes\" and \\backslashes"),
11+
("short_control", "string with\ncontrol\tchars\r"),
12+
("long_clean", long_clean.as_str()), // Long string without escapes
13+
("long_quotes", long_quotes.as_str()), // Many escapes
14+
("mixed", "mixed: \"quotes\", \\backslashes, \ncontrol\tchars, and regular text"),
15+
];
16+
17+
for (name, test_str) in test_strings {
18+
c.bench_function(&format!("escape_fallback_{}", name), |b| {
19+
b.iter(|| escape_json_string_fallback(test_str));
20+
});
21+
22+
c.bench_function(&format!("escape_avx512_{}", name), |b| {
23+
b.iter(|| escape_json_string(test_str));
24+
});
25+
}
26+
}
327

428
pub fn bench(c: &mut Criterion) {
529
let input = r#"{
@@ -35,5 +59,5 @@ pub fn bench(c: &mut Criterion) {
3559
});
3660
}
3761

38-
criterion_group!(sourcemap, bench);
62+
criterion_group!(sourcemap, bench, bench_json_escaping);
3963
criterion_main!(sourcemap);

src/encode.rs

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,22 @@ impl<'a> PreAllocatedString<'a> {
330330
}
331331
}
332332

333-
fn escape_json_string<S: AsRef<str>>(s: S) -> String {
333+
pub fn escape_json_string<S: AsRef<str>>(s: S) -> String {
334+
let s = s.as_ref();
335+
336+
// Use AVX512 acceleration if available on x86_64
337+
#[cfg(target_arch = "x86_64")]
338+
{
339+
if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("avx512bw") {
340+
return unsafe { escape_json_string_avx512(s) };
341+
}
342+
}
343+
344+
// Fallback to serde_json implementation
345+
escape_json_string_fallback(s)
346+
}
347+
348+
pub fn escape_json_string_fallback<S: AsRef<str>>(s: S) -> String {
334349
let s = s.as_ref();
335350
let mut escaped_buf = Vec::with_capacity(s.len() * 2 + 2);
336351
// This call is infallible as only error it can return is if the writer errors.
@@ -340,6 +355,81 @@ fn escape_json_string<S: AsRef<str>>(s: S) -> String {
340355
unsafe { String::from_utf8_unchecked(escaped_buf) }
341356
}
342357

358+
#[cfg(target_arch = "x86_64")]
359+
#[target_feature(enable = "avx512f,avx512bw")]
360+
unsafe fn escape_json_string_avx512(s: &str) -> String {
361+
use std::arch::x86_64::*;
362+
363+
let bytes = s.as_bytes();
364+
let mut result = Vec::with_capacity(s.len() * 2 + 2);
365+
366+
// Add opening quote
367+
result.push(b'"');
368+
369+
let mut i = 0;
370+
371+
// Process 64-byte chunks with AVX512
372+
while i + 64 <= bytes.len() {
373+
unsafe {
374+
let chunk = _mm512_loadu_si512(bytes.as_ptr().add(i) as *const __m512i);
375+
376+
// Check for characters that need escaping
377+
// ASCII control characters (0x00-0x1F), quote (0x22), backslash (0x5C)
378+
let control_mask = _mm512_cmplt_epu8_mask(chunk, _mm512_set1_epi8(0x20));
379+
let quote_mask = _mm512_cmpeq_epu8_mask(chunk, _mm512_set1_epi8(b'"' as i8));
380+
let backslash_mask = _mm512_cmpeq_epu8_mask(chunk, _mm512_set1_epi8(b'\\' as i8));
381+
382+
let escape_mask = control_mask | quote_mask | backslash_mask;
383+
384+
if escape_mask == 0 {
385+
// No characters need escaping, copy directly
386+
result.extend_from_slice(&bytes[i..i + 64]);
387+
i += 64;
388+
} else {
389+
// Process byte by byte for this chunk due to escaping needed
390+
for j in 0..64 {
391+
let byte = bytes[i + j];
392+
append_escaped_byte(&mut result, byte);
393+
}
394+
i += 64;
395+
}
396+
}
397+
}
398+
399+
// Process remaining bytes
400+
while i < bytes.len() {
401+
append_escaped_byte(&mut result, bytes[i]);
402+
i += 1;
403+
}
404+
405+
// Add closing quote
406+
result.push(b'"');
407+
408+
// Safety: We only write valid UTF-8 sequences
409+
unsafe { String::from_utf8_unchecked(result) }
410+
}
411+
412+
#[inline]
413+
fn append_escaped_byte(result: &mut Vec<u8>, byte: u8) {
414+
match byte {
415+
b'"' => result.extend_from_slice(b"\\\""),
416+
b'\\' => result.extend_from_slice(b"\\\\"),
417+
b'\x08' => result.extend_from_slice(b"\\b"),
418+
b'\x0C' => result.extend_from_slice(b"\\f"),
419+
b'\n' => result.extend_from_slice(b"\\n"),
420+
b'\r' => result.extend_from_slice(b"\\r"),
421+
b'\t' => result.extend_from_slice(b"\\t"),
422+
0x00..=0x1F => {
423+
// Other control characters as unicode escapes
424+
result.extend_from_slice(b"\\u00");
425+
let hex_chars = b"0123456789abcdef";
426+
result.push(hex_chars[(byte >> 4) as usize]);
427+
result.push(hex_chars[(byte & 0x0F) as usize]);
428+
}
429+
_ => result.push(byte),
430+
}
431+
}
432+
343433
#[test]
344434
fn test_escape_json_string() {
345435
const FIXTURES: &[(char, &str)] = &[
@@ -364,6 +454,61 @@ fn test_escape_json_string() {
364454
}
365455
}
366456

457+
#[test]
458+
fn test_avx512_vs_fallback_consistency() {
459+
let long_x = "x".repeat(100);
460+
let long_quotes = "\"".repeat(100);
461+
let long_control = "\n".repeat(100);
462+
463+
let test_strings = vec![
464+
"",
465+
"simple",
466+
"with\"quotes",
467+
"with\\backslashes",
468+
"with\ncontrol\tchars\r",
469+
"🚀 unicode 虎",
470+
&long_x, // Test longer strings to trigger AVX512 path
471+
&long_quotes, // Many quotes to test escaping
472+
&long_control, // Many control chars
473+
"mixed content with \"quotes\", \\backslashes, and \ncontrol\tchars",
474+
];
475+
476+
for test_str in test_strings {
477+
let fallback_result = escape_json_string_fallback(test_str);
478+
let main_result = escape_json_string(test_str);
479+
assert_eq!(
480+
fallback_result, main_result,
481+
"Results differ for input: {:?}",
482+
test_str
483+
);
484+
}
485+
}
486+
487+
#[test]
488+
#[cfg(target_arch = "x86_64")]
489+
fn test_avx512_direct() {
490+
// Test the AVX512 function directly if AVX512 is available
491+
if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("avx512bw") {
492+
let long_a = "a".repeat(128);
493+
let test_strings = vec![
494+
"simple test",
495+
"with \"quotes\"",
496+
"with\ncontrol\rchars\t",
497+
long_a.as_str(), // Longer than one AVX512 chunk
498+
];
499+
500+
for test_str in test_strings {
501+
let avx512_result = unsafe { escape_json_string_avx512(test_str) };
502+
let fallback_result = escape_json_string_fallback(test_str);
503+
assert_eq!(
504+
avx512_result, fallback_result,
505+
"AVX512 and fallback results differ for: {:?}",
506+
test_str
507+
);
508+
}
509+
}
510+
}
511+
367512
#[test]
368513
fn test_encode() {
369514
let input = r#"{

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod napi;
1212

1313
pub use concat_sourcemap_builder::ConcatSourceMapBuilder;
1414
pub use decode::JSONSourceMap;
15+
pub use encode::{escape_json_string, escape_json_string_fallback};
1516
pub use error::Error;
1617
pub use sourcemap::SourceMap;
1718
pub use sourcemap_builder::SourceMapBuilder;

0 commit comments

Comments
 (0)