Skip to content

Commit dbef33f

Browse files
committed
Use UTF-8 error length enum to reduce register spill
When using `error_len: Option<u8>`, `Result<(), Utf8Error>` will be returned on stack and produces suboptimal stack suffling operations. It causes 50%-200% latency increase on the error path.
1 parent 1efec71 commit dbef33f

File tree

3 files changed

+51
-22
lines changed

3 files changed

+51
-22
lines changed

library/core/src/str/error.rs

+29-6
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,24 @@ use crate::fmt;
4242
/// }
4343
/// }
4444
/// ```
45-
#[derive(Copy, Eq, PartialEq, Clone, Debug)]
45+
#[derive(Copy, Eq, PartialEq, Clone)]
4646
#[stable(feature = "rust1", since = "1.0.0")]
4747
pub struct Utf8Error {
4848
pub(super) valid_up_to: usize,
49-
pub(super) error_len: Option<u8>,
49+
// Use a single value instead of tagged enum `Option<u8>` to make `Result<(), Utf8Error>` fits
50+
// in two machine words, so `run_utf8_validation` does not need to returns values on stack on
51+
// x86(_64). Register spill is very expensive on `run_utf8_validation` and can give up to 200%
52+
// latency penalty on the error path.
53+
pub(super) error_len: Utf8ErrorLen,
54+
}
55+
56+
#[derive(Copy, Eq, PartialEq, Clone)]
57+
#[repr(u8)]
58+
pub(super) enum Utf8ErrorLen {
59+
Eof = 0,
60+
One,
61+
Two,
62+
Three,
5063
}
5164

5265
impl Utf8Error {
@@ -100,18 +113,28 @@ impl Utf8Error {
100113
#[must_use]
101114
#[inline]
102115
pub const fn error_len(&self) -> Option<usize> {
103-
// FIXME(const-hack): This should become `map` again, once it's `const`
104116
match self.error_len {
105-
Some(len) => Some(len as usize),
106-
None => None,
117+
Utf8ErrorLen::Eof => None,
118+
// FIXME(136972): Direct `match` gives suboptimal codegen involving two table lookups.
119+
len => Some(len as usize),
107120
}
108121
}
109122
}
110123

124+
#[stable(feature = "rust1", since = "1.0.0")]
125+
impl fmt::Debug for Utf8Error {
126+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127+
f.debug_struct("Utf8Error")
128+
.field("valid_up_to", &self.valid_up_to)
129+
.field("error_len", &self.error_len())
130+
.finish()
131+
}
132+
}
133+
111134
#[stable(feature = "rust1", since = "1.0.0")]
112135
impl fmt::Display for Utf8Error {
113136
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114-
if let Some(error_len) = self.error_len {
137+
if let Some(error_len) = self.error_len() {
115138
write!(
116139
f,
117140
"invalid utf-8 sequence of {} bytes from index {}",

library/core/src/str/lossy.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::from_utf8_unchecked;
2+
use super::validations::run_utf8_validation;
23
use crate::fmt;
34
use crate::fmt::{Formatter, Write};
45
use crate::iter::FusedIterator;
@@ -196,8 +197,10 @@ impl<'a> Iterator for Utf8Chunks<'a> {
196197
return None;
197198
}
198199

199-
match super::from_utf8(self.source) {
200-
Ok(valid) => {
200+
match run_utf8_validation(self.source) {
201+
Ok(()) => {
202+
// SAFETY: The whole `source` is valid in UTF-8.
203+
let valid = unsafe { from_utf8_unchecked(&self.source) };
201204
// Truncate the slice, no need to touch the pointer.
202205
self.source = &self.source[..0];
203206
Some(Utf8Chunk { valid, invalid: &[] })

library/core/src/str/validations.rs

+17-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Operations related to UTF-8 validation.
22
33
use super::Utf8Error;
4+
use super::error::Utf8ErrorLen;
45
use crate::intrinsics::const_eval_select;
56

67
/// Returns the initial codepoint accumulator for the first byte.
@@ -210,25 +211,26 @@ const fn is_utf8_first_byte(byte: u8) -> bool {
210211
/// The caller must ensure `bytes[..i]` is a valid UTF-8 prefix and `st` is the DFA state after
211212
/// executing on `bytes[..i]`.
212213
#[inline]
213-
const unsafe fn resolve_error_location(st: u32, bytes: &[u8], i: usize) -> (usize, u8) {
214+
const unsafe fn resolve_error_location(st: u32, bytes: &[u8], i: usize) -> Utf8Error {
214215
// There are two cases:
215216
// 1. [valid UTF-8..] | *here
216217
// The previous state must be ACCEPT for the case 1, and `valid_up_to = i`.
217218
// 2. [valid UTF-8..] | valid first byte, [valid continuation byte...], *here
218219
// `valid_up_to` is at the latest non-continuation byte, which must exist and
219220
// be in range `(i-3)..i`.
220-
if st & STATE_MASK == ST_ACCEPT {
221-
(i, 1)
221+
let (valid_up_to, error_len) = if st & STATE_MASK == ST_ACCEPT {
222+
(i, Utf8ErrorLen::One)
222223
// SAFETY: UTF-8 first byte must exist if we are in an intermediate state.
223224
// We use pointer here because `get_unchecked` is not const fn.
224225
} else if is_utf8_first_byte(unsafe { bytes.as_ptr().add(i - 1).read() }) {
225-
(i - 1, 1)
226+
(i - 1, Utf8ErrorLen::One)
226227
// SAFETY: Same as above.
227228
} else if is_utf8_first_byte(unsafe { bytes.as_ptr().add(i - 2).read() }) {
228-
(i - 2, 2)
229+
(i - 2, Utf8ErrorLen::Two)
229230
} else {
230-
(i - 3, 3)
231-
}
231+
(i - 3, Utf8ErrorLen::Three)
232+
};
233+
Utf8Error { valid_up_to, error_len }
232234
}
233235

234236
// The simpler but slower algorithm to run DFA with error handling.
@@ -245,8 +247,7 @@ const unsafe fn run_with_error_handling(
245247
let new_st = next_state(*st, bytes[i]);
246248
if new_st & STATE_MASK == ST_ERROR {
247249
// SAFETY: Guaranteed by the caller.
248-
let (valid_up_to, error_len) = unsafe { resolve_error_location(*st, bytes, i) };
249-
return Err(Utf8Error { valid_up_to, error_len: Some(error_len) });
250+
return Err(unsafe { resolve_error_location(*st, bytes, i) });
250251
}
251252
*st = new_st;
252253
i += 1;
@@ -256,7 +257,7 @@ const unsafe fn run_with_error_handling(
256257

257258
/// Walks through `v` checking that it's a valid UTF-8 sequence,
258259
/// returning `Ok(())` in that case, or, if it is invalid, `Err(err)`.
259-
#[inline(always)]
260+
#[inline]
260261
#[rustc_allow_const_fn_unstable(const_eval_select)] // fallback impl has same behavior
261262
pub(super) const fn run_utf8_validation(bytes: &[u8]) -> Result<(), Utf8Error> {
262263
const_eval_select((bytes,), run_utf8_validation_const, run_utf8_validation_rt)
@@ -273,8 +274,9 @@ const fn run_utf8_validation_const(bytes: &[u8]) -> Result<(), Utf8Error> {
273274
Ok(())
274275
} else {
275276
// SAFETY: `st` is the last state after execution without encountering any error.
276-
let (valid_up_to, _) = unsafe { resolve_error_location(st, bytes, bytes.len()) };
277-
Err(Utf8Error { valid_up_to, error_len: None })
277+
let mut err = unsafe { resolve_error_location(st, bytes, bytes.len()) };
278+
err.error_len = Utf8ErrorLen::Eof;
279+
Err(err)
278280
}
279281
}
280282
}
@@ -333,8 +335,9 @@ fn run_utf8_validation_rt(bytes: &[u8]) -> Result<(), Utf8Error> {
333335

334336
if st & STATE_MASK != ST_ACCEPT {
335337
// SAFETY: Same as above.
336-
let (valid_up_to, _) = unsafe { resolve_error_location(st, bytes, bytes.len()) };
337-
return Err(Utf8Error { valid_up_to, error_len: None });
338+
let mut err = unsafe { resolve_error_location(st, bytes, bytes.len()) };
339+
err.error_len = Utf8ErrorLen::Eof;
340+
return Err(err);
338341
}
339342

340343
Ok(())

0 commit comments

Comments
 (0)