utf8: add an ASCII fast path to utf8_is_printable_newline()

This decoded every character twice: utf8_encoded_valid_unichar() decodes
internally to do its validation and then throws the codepoint away, and the
caller immediately called utf8_encoded_to_unichar() again to get the very same
codepoint back.

Fix that where it originates rather than at the call site: add
utf8_encoded_valid_unichar_full(), which optionally returns the character it had
to decode anyway, and make utf8_encoded_valid_unichar() a wrapper that passes
NULL. Callers that want both the validity and the value now decode once.

On top of that add a bulk fast path, since almost everything we are asked about
is plain printable ASCII: test eight bytes at a time for that, i.e. that no byte
is >= 0x80, none is < 0x20 and none is 0x7f. A remainder of less than eight
bytes is copied into a word padded with spaces so that it can be tested the same
way. A word that does not qualify falls through to the per character path, so
tabs, newlines and multibyte sequences are handled exactly as before.

The function sits on journalctl's per-field output path, so this is measurable:
on a 730M journal spread over 100 files, journalctl -o export goes from 4.02s to
about 2.9s.

Also extend the tests, which so far covered neither DEL nor the C1 range nor
allow_newline=false: place each of the interesting bytes at each offset within
the eight byte word, and check a multibyte character straddling the end of one.

The new code was verified to agree with the old one on 41945090 inputs, for both
values of allow_newline: every byte string of length zero to three over all 256
byte values, four million random strings over uniform bytes, over an ASCII-heavy
alphabet and over an alphabet of encoding boundary bytes, and a corpus placing
every byte value at every offset of buffers of every length from eight to
twenty-four bytes, with valid and truncated multibyte sequences straddling the
word boundary.
This commit is contained in:
Armaan Sandhu
2026-09-23 08:59:56 +09:00
committed by Yu Watanabe
parent 21561b5838
commit 7461360c15
3 changed files with 96 additions and 8 deletions
+57 -7
View File
@@ -11,6 +11,7 @@
#include "gunicode.h"
#include "hexdecoct.h"
#include "string-util.h"
#include "unaligned.h"
#include "utf8.h"
bool unichar_is_valid(char32_t ch) {
@@ -102,6 +103,20 @@ int utf8_encoded_to_unichar(const char *str, char32_t *ret_unichar) {
return len;
}
/* Tests whether all eight bytes of a word are plain printable ASCII, i.e. within 0x20…0x7e. The three
* terms below cover the three ways a byte can fail. A byte that fails is always flagged in its own
* lane, but the converse does not hold: the two subtractions are whole-word ones, so a borrow out of
* a failing lane also flags bytes above it. Only the aggregate test is meaningful, the mask is not a
* per-byte map of which bytes failed. */
static bool is_printable_fastpath_u64(uint64_t w) {
uint64_t x = w ^ UINT64_C(0x7f7f7f7f7f7f7f7f);
return (((w & UINT64_C(0x8080808080808080)) | /* >= 0x80 */
((w - UINT64_C(0x2020202020202020)) & ~w) | /* < 0x20 */
((x - UINT64_C(0x0101010101010101)) & ~x)) & /* == 0x7f */
UINT64_C(0x8080808080808080)) == 0;
}
bool utf8_is_printable_newline(const char* str, size_t length, bool allow_newline) {
assert(str);
@@ -109,14 +124,41 @@ bool utf8_is_printable_newline(const char* str, size_t length, bool allow_newlin
int encoded_len;
char32_t val;
encoded_len = utf8_encoded_valid_unichar(p, length);
/* Bulk fast path: almost everything we are asked about is plain printable ASCII, so
* test eight bytes of it at once. Anything else falls through to the per-character
* path below, which handles it as before. A remainder of less than eight bytes is
* copied into a word padded with spaces, so that it can be tested the same way. */
while (length > 0) {
uint64_t w;
size_t n;
if (length < sizeof(uint64_t)) {
n = length;
w = UINT64_C(0x2020202020202020);
memcpy(&w, p, n);
} else {
n = sizeof(uint64_t);
w = unaligned_read_ne64(p);
}
if (!is_printable_fastpath_u64(w))
break;
length -= n;
p += n;
}
/* Note that this is not redundant with the loop condition above: the fast path may
* have consumed the remainder of the string, and then there's nothing left to decode. */
if (length == 0)
break;
encoded_len = utf8_encoded_valid_unichar_full(p, length, &val);
if (encoded_len < 0)
return false;
assert(encoded_len > 0 && (size_t) encoded_len <= length);
if (utf8_encoded_to_unichar(p, &val) < 0 ||
unichar_is_control(val) ||
(!allow_newline && val == '\n'))
if (unichar_is_control(val) || (!allow_newline && val == '\n'))
return false;
length -= encoded_len;
@@ -541,7 +583,7 @@ static int utf8_unichar_to_encoded_len(char32_t unichar) {
}
/* validate one encoded unicode char and return its length */
int utf8_encoded_valid_unichar(const char *str, size_t length /* bytes */) {
int utf8_encoded_valid_unichar_full(const char *str, size_t length /* bytes */, char32_t *ret_unichar) {
char32_t unichar;
size_t len;
int r;
@@ -549,7 +591,9 @@ int utf8_encoded_valid_unichar(const char *str, size_t length /* bytes */) {
assert(str);
assert(length > 0);
/* We read until NUL, at most length bytes. SIZE_MAX may be used to disable the length check. */
/* We read until NUL, at most length bytes. SIZE_MAX may be used to disable the length check.
* If ret_unichar is non-NULL the decoded character is returned in it, so that callers
* interested in both validity and value needn't decode the character a second time. */
len = utf8_encoded_expected_len(str[0]);
if (len == 0)
@@ -560,8 +604,11 @@ int utf8_encoded_valid_unichar(const char *str, size_t length /* bytes */) {
return -EINVAL;
/* ascii is valid */
if (len == 1)
if (len == 1) {
if (ret_unichar)
*ret_unichar = (char32_t) (uint8_t) str[0];
return 1;
}
/* check if expected encoded chars are available */
for (size_t i = 0; i < len; i++)
@@ -580,6 +627,9 @@ int utf8_encoded_valid_unichar(const char *str, size_t length /* bytes */) {
if (!unichar_is_valid(unichar))
return -EINVAL;
if (ret_unichar)
*ret_unichar = unichar;
return (int) len;
}
+4 -1
View File
@@ -38,7 +38,10 @@ char16_t *utf8_to_utf16(const char *s, size_t length);
size_t char16_strlen(const char16_t *s) _pure_; /* returns the number of 16-bit words in the string (not bytes!) */
size_t char16_strsize(const char16_t *s) _pure_;
int utf8_encoded_valid_unichar(const char *str, size_t length) _pure_;
int utf8_encoded_valid_unichar_full(const char *str, size_t length, char32_t *ret_unichar);
static inline int _pure_ utf8_encoded_valid_unichar(const char *str, size_t length) {
return utf8_encoded_valid_unichar_full(str, length, NULL);
}
int utf8_encoded_to_unichar(const char *str, char32_t *ret_unichar);
static inline bool utf16_is_surrogate(char16_t c) {