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:
committed by
Yu Watanabe
parent
21561b5838
commit
7461360c15
+57
-7
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user