From 01a380592a38acde1903156f00c1080c6ef950d0 Mon Sep 17 00:00:00 2001 From: SayanthRock <202829406+SayanthRock@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:09:46 +0000 Subject: [PATCH] perf: replace char_indices with byte iteration in normalize_expression Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/bolt.md | 3 +++ compiler/rockql-sql/src/lib.rs | 36 ++++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index aca28d8..2ff9897 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -9,3 +9,6 @@ ## 2024-08-14 - [Rust String Parsing - Whitespace Semantics Regression] **Learning:** When optimizing whitespace scanning in Rust parsing loops by replacing `.char_indices()` with byte-level ASCII checks (e.g., `as_bytes().iter().position(|b| b.is_ascii_whitespace())`), it can introduce a subtle functional regression. Rust's `char::is_whitespace()` matches all Unicode whitespace characters (like non-breaking spaces), whereas `is_ascii_whitespace()` only matches standard ASCII whitespace. **Action:** When exact Unicode semantics must be preserved while optimizing, use `.find()` (e.g., `text.find(char::is_whitespace)`) instead of dropping down to byte-level operations. This leverages internal optimizations while preserving the exact semantic meaning of the original code. +## 2024-08-15 - [Rust String Parsing - Hot Path Byte Iteration] +**Learning:** In very hot parsing loops where the majority of characters are ASCII (like SQL expressions), `.char_indices()` introduces noticeable overhead because it decodes UTF-8 for every character. +**Action:** Iterate over `.as_bytes()` using a manual index. If `byte.is_ascii()` is true, cast it to `char` and increment the index by 1. Only decode UTF-8 using `expression[i..].chars().next().unwrap()` when a non-ASCII byte is encountered, advancing the index by the resulting character's `len_utf8()`. diff --git a/compiler/rockql-sql/src/lib.rs b/compiler/rockql-sql/src/lib.rs index 2baee83..98d1716 100644 --- a/compiler/rockql-sql/src/lib.rs +++ b/compiler/rockql-sql/src/lib.rs @@ -182,9 +182,20 @@ fn normalize_expression(expression: &str, _dialect: Dialect) -> String { }; let bytes = expression.as_bytes(); - let mut chars = expression.char_indices().peekable(); + let mut i = 0; + + while i < bytes.len() { + let b = bytes[i]; + + let character = if b.is_ascii() { + i += 1; + b as char + } else { + let ch = expression[i..].chars().next().unwrap(); + i += ch.len_utf8(); + ch + }; - while let Some((i, character)) = chars.next() { if let Some(active_quote) = quote { output.push(character); if character == active_quote { @@ -202,11 +213,16 @@ fn normalize_expression(expression: &str, _dialect: Dialect) -> String { quote = Some(character); } else if character.is_ascii_alphanumeric() || character == '_' { // Handle numeric separators + let current_i = if character.is_ascii() { + i - 1 + } else { + i - character.len_utf8() + }; if character == '_' - && i > 0 - && i + 1 < bytes.len() - && bytes[i - 1].is_ascii_digit() - && bytes[i + 1].is_ascii_digit() + && current_i > 0 + && current_i + 1 < bytes.len() + && bytes[current_i - 1].is_ascii_digit() + && bytes[current_i + 1].is_ascii_digit() { continue; // Skip the underscore } @@ -223,16 +239,16 @@ fn normalize_expression(expression: &str, _dialect: Dialect) -> String { // Handle operators != and == if character == '!' { - if let Some(&(_, '=')) = chars.peek() { + if i < bytes.len() && bytes[i] == b'=' { output.push_str("<>"); - chars.next(); // Consume '=' + i += 1; // Consume '=' } else { output.push(character); } } else if character == '=' { - if let Some(&(_, '=')) = chars.peek() { + if i < bytes.len() && bytes[i] == b'=' { output.push('='); - chars.next(); // Consume second '=' + i += 1; // Consume second '=' } else { output.push(character); }