libutils: Improve performance of utf8_to_utf16/utf16_to_utf8

This CL improves the performance of below functions in helping with conversion
between utf8/utf16 with libutils:

  - utf8_to_utf16_length
  - utf8_to_utf16
  - utf16_to_utf8_length
  - utf16_to_utf

The basic idea is to keep the loop as tight as possible for the most
common cases, e.g. in UTF16-->UTF8 case, the most common case is
when the character is < 0x80 (ASCII), next is when it's < 0x0800 (
most Latin), and so on.

This version of implementation reduces the number of instructions
needed for every incoming utf-8 bytes in the original implementation
where:

  1) calculating how many bytes needed given a leading UTF-8 byte
     in utf8_codepoint_len(), it's a very clever way but involves
     multiple instructions to calculate regardless

  2) and an intermediate conversion to utf32, and then to utf16
     utf8_to_utf32_codepoint()

The end result is about ~1.5x throughput improvement.

Benchmark results on redfin (64bit) before the change:

utf8_to_utf16_length: bytes_per_second=307.556M/s
utf8_to_utf16:        bytes_per_second=246.664M/s
utf16_to_utf8_length: bytes_per_second=482.241M/s
utf16_to_utf8:        bytes_per_second=351.376M/s

After the change:

utf8_to_utf16_length: bytes_per_second=544.022M/s
utf8_to_utf16:        bytes_per_second=471.135M/s
utf16_to_utf8_length: bytes_per_second=685.381M/s
utf16_to_utf8:        bytes_per_second=580.004M/s

Ideas for future improvement could include alignment handling and loop
unrolling to increase throughput more.

This CL also fixes issues below:

  1. utf16_to_utf8_length() should return 0 when the source string has
     length of 0, the original code returns -1 as below:

    ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len)
    {
        if (src == nullptr || src_len == 0) {
            return -1;
        }
	...

  2. utf8_to_utf16() should check whether input string is valid.

Change-Id: I546138a7a8050681a524eabce9864219fc44f48e
This commit is contained in:
Eric Miao 2022-11-30 16:05:49 -08:00
parent dc8ae8c55a
commit cb199b4795

View file

@ -280,158 +280,181 @@ int strzcmp16(const char16_t *s1, size_t n1, const char16_t *s2, size_t n2)
: 0);
}
// is_any_surrogate() returns true if w is either a high or low surrogate
static constexpr bool is_any_surrogate(char16_t w) {
return (w & 0xf800) == 0xd800;
}
// is_surrogate_pair() returns true if w1 and w2 form a valid surrogate pair
static constexpr bool is_surrogate_pair(char16_t w1, char16_t w2) {
return ((w1 & 0xfc00) == 0xd800) && ((w2 & 0xfc00) == 0xdc00);
}
// TODO: currently utf16_to_utf8_length() returns -1 if src_len == 0,
// which is inconsistent with utf8_to_utf16_length(), here we keep the
// current behavior as intended not to break compatibility
ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len)
{
if (src == nullptr || src_len == 0)
return -1;
const char16_t* const end = src + src_len;
const char16_t* in = src;
size_t utf8_len = 0;
while (in < end) {
char16_t w = *in++;
if (LIKELY(w < 0x0080)) {
utf8_len += 1;
continue;
}
if (LIKELY(w < 0x0800)) {
utf8_len += 2;
continue;
}
if (LIKELY(!is_any_surrogate(w))) {
utf8_len += 3;
continue;
}
if (in < end && is_surrogate_pair(w, *in)) {
utf8_len += 4;
in++;
continue;
}
/* skip if at the end of the string or invalid surrogate pair */
}
return (in == end && utf8_len < SSIZE_MAX) ? utf8_len : -1;
}
void utf16_to_utf8(const char16_t* src, size_t src_len, char* dst, size_t dst_len)
{
if (src == nullptr || src_len == 0 || dst == nullptr) {
return;
}
const char16_t* cur_utf16 = src;
const char16_t* const end_utf16 = src + src_len;
char *cur = dst;
while (cur_utf16 < end_utf16) {
char32_t utf32;
// surrogate pairs
if((*cur_utf16 & 0xFC00) == 0xD800 && (cur_utf16 + 1) < end_utf16
&& (*(cur_utf16 + 1) & 0xFC00) == 0xDC00) {
utf32 = (*cur_utf16++ - 0xD800) << 10;
utf32 |= *cur_utf16++ - 0xDC00;
utf32 += 0x10000;
} else {
utf32 = (char32_t) *cur_utf16++;
const char16_t* in = src;
const char16_t* const in_end = src + src_len;
char* out = dst;
const char* const out_end = dst + dst_len;
char16_t w2;
auto err_out = [&out, &out_end, &dst_len]() {
LOG_ALWAYS_FATAL_IF(out >= out_end,
"target utf8 string size %zu too short", dst_len);
};
while (in < in_end) {
char16_t w = *in++;
if (LIKELY(w < 0x0080)) {
if (out + 1 > out_end)
return err_out();
*out++ = (char)(w & 0xff);
continue;
}
const size_t len = utf32_codepoint_utf8_length(utf32);
LOG_ALWAYS_FATAL_IF(dst_len < len, "%zu < %zu", dst_len, len);
utf32_codepoint_to_utf8((uint8_t*)cur, utf32, len);
cur += len;
dst_len -= len;
if (LIKELY(w < 0x0800)) {
if (out + 2 > out_end)
return err_out();
*out++ = (char)(0xc0 | ((w >> 6) & 0x1f));
*out++ = (char)(0x80 | ((w >> 0) & 0x3f));
continue;
}
if (LIKELY(!is_any_surrogate(w))) {
if (out + 3 > out_end)
return err_out();
*out++ = (char)(0xe0 | ((w >> 12) & 0xf));
*out++ = (char)(0x80 | ((w >> 6) & 0x3f));
*out++ = (char)(0x80 | ((w >> 0) & 0x3f));
continue;
}
/* surrogate pair */
if (in < in_end && (w2 = *in, is_surrogate_pair(w, w2))) {
if (out + 4 > out_end)
return err_out();
char32_t dw = (char32_t)(0x10000 + ((w - 0xd800) << 10) + (w2 - 0xdc00));
*out++ = (char)(0xf0 | ((dw >> 18) & 0x07));
*out++ = (char)(0x80 | ((dw >> 12) & 0x3f));
*out++ = (char)(0x80 | ((dw >> 6) & 0x3f));
*out++ = (char)(0x80 | ((dw >> 0) & 0x3f));
in++;
}
/* We reach here in two cases:
* 1) (in == in_end), which means end of the input string
* 2) (w2 & 0xfc00) != 0xdc00, which means invalid surrogate pair
* In either case, we intentionally do nothing and skip
*/
}
LOG_ALWAYS_FATAL_IF(dst_len < 1, "%zu < 1", dst_len);
*cur = '\0';
*out = '\0';
return;
}
// --------------------------------------------------------------------------
// UTF-8
// --------------------------------------------------------------------------
ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len)
{
if (src == nullptr || src_len == 0) {
return -1;
}
size_t ret = 0;
const char16_t* const end = src + src_len;
while (src < end) {
size_t char_len;
if ((*src & 0xFC00) == 0xD800 && (src + 1) < end
&& (*(src + 1) & 0xFC00) == 0xDC00) {
// surrogate pairs are always 4 bytes.
char_len = 4;
src += 2;
} else {
char_len = utf32_codepoint_utf8_length((char32_t)*src++);
}
if (SSIZE_MAX - char_len < ret) {
// If this happens, we would overflow the ssize_t type when
// returning from this function, so we cannot express how
// long this string is in an ssize_t.
android_errorWriteLog(0x534e4554, "37723026");
return -1;
}
ret += char_len;
}
return ret;
static char32_t utf8_4b_to_utf32(uint8_t c1, uint8_t c2, uint8_t c3, uint8_t c4) {
return ((c1 & 0x07) << 18) | ((c2 & 0x3f) << 12) | ((c3 & 0x3f) << 6) | (c4 & 0x3f);
}
/**
* Returns 1-4 based on the number of leading bits.
*
* 1111 -> 4
* 1110 -> 3
* 110x -> 2
* 10xx -> 1
* 0xxx -> 1
*/
static inline size_t utf8_codepoint_len(uint8_t ch)
{
return ((0xe5000000 >> ((ch >> 3) & 0x1e)) & 3) + 1;
}
static inline void utf8_shift_and_mask(uint32_t* codePoint, const uint8_t byte)
{
*codePoint <<= 6;
*codePoint |= 0x3F & byte;
}
static inline uint32_t utf8_to_utf32_codepoint(const uint8_t *src, size_t length)
{
uint32_t unicode;
switch (length)
{
case 1:
return src[0];
case 2:
unicode = src[0] & 0x1f;
utf8_shift_and_mask(&unicode, src[1]);
return unicode;
case 3:
unicode = src[0] & 0x0f;
utf8_shift_and_mask(&unicode, src[1]);
utf8_shift_and_mask(&unicode, src[2]);
return unicode;
case 4:
unicode = src[0] & 0x07;
utf8_shift_and_mask(&unicode, src[1]);
utf8_shift_and_mask(&unicode, src[2]);
utf8_shift_and_mask(&unicode, src[3]);
return unicode;
default:
return 0xffff;
}
//printf("Char at %p: len=%d, utf-16=%p\n", src, length, (void*)result);
}
// TODO: current behavior of converting UTF8 to UTF-16 has a few issues below
//
// 1. invalid trailing bytes (i.e. not b'10xxxxxx) are treated as valid trailing
// bytes and follows normal conversion rules
// 2. invalid leading byte (b'10xxxxxx) is treated as a valid single UTF-8 byte
// 3. invalid leading byte (b'11111xxx) is treated as a valid leading byte
// (same as b'11110xxx) for a 4-byte UTF-8 sequence
// 4. an invalid 4-byte UTF-8 sequence that translates to a codepoint < U+10000
// will be converted as a valid UTF-16 character
//
// We keep the current behavior as is but with warnings logged, so as not to
// break compatibility. However, this needs to be addressed later.
ssize_t utf8_to_utf16_length(const uint8_t* u8str, size_t u8len, bool overreadIsFatal)
{
const uint8_t* const u8end = u8str + u8len;
const uint8_t* u8cur = u8str;
/* Validate that the UTF-8 is the correct len */
size_t u16measuredLen = 0;
while (u8cur < u8end) {
u16measuredLen++;
int u8charLen = utf8_codepoint_len(*u8cur);
// Malformed utf8, some characters are beyond the end.
// Cases:
// If u8charLen == 1, this becomes u8cur >= u8end, which cannot happen as u8cur < u8end,
// then this condition fail and we continue, as expected.
// If u8charLen == 2, this becomes u8cur + 1 >= u8end, which fails only if
// u8cur == u8end - 1, that is, there was only one remaining character to read but we need
// 2 of them. This condition holds and we return -1, as expected.
if (u8cur + u8charLen - 1 >= u8end) {
if (overreadIsFatal) {
LOG_ALWAYS_FATAL("Attempt to overread computing length of utf8 string");
} else {
return -1;
}
}
uint32_t codepoint = utf8_to_utf32_codepoint(u8cur, u8charLen);
if (codepoint > 0xFFFF) u16measuredLen++; // this will be a surrogate pair in utf16
u8cur += u8charLen;
}
/**
* Make sure that we ended where we thought we would and the output UTF-16
* will be exactly how long we were told it would be.
*/
if (u8cur != u8end) {
if (u8str == nullptr)
return -1;
}
return u16measuredLen;
const uint8_t* const in_end = u8str + u8len;
const uint8_t* in = u8str;
size_t utf16_len = 0;
while (in < in_end) {
uint8_t c = *in;
utf16_len++;
if (LIKELY((c & 0x80) == 0)) {
in++;
continue;
}
if (UNLIKELY(c < 0xc0)) {
ALOGW("Invalid UTF-8 leading byte: 0x%02x", c);
in++;
continue;
}
if (LIKELY(c < 0xe0)) {
in += 2;
continue;
}
if (LIKELY(c < 0xf0)) {
in += 3;
continue;
} else {
uint8_t c2, c3, c4;
if (UNLIKELY(c >= 0xf8)) {
ALOGW("Invalid UTF-8 leading byte: 0x%02x", c);
}
c2 = in[1]; c3 = in[2]; c4 = in[3];
if (utf8_4b_to_utf32(c, c2, c3, c4) >= 0x10000) {
utf16_len++;
}
in += 4;
continue;
}
}
if (in == in_end) {
return utf16_len < SSIZE_MAX ? utf16_len : -1;
}
if (overreadIsFatal)
LOG_ALWAYS_FATAL("Attempt to overread computing length of utf8 string");
return -1;
}
char16_t* utf8_to_utf16(const uint8_t* u8str, size_t u8len, char16_t* u16str, size_t u16len) {
@ -444,38 +467,75 @@ char16_t* utf8_to_utf16(const uint8_t* u8str, size_t u8len, char16_t* u16str, si
char16_t* utf8_to_utf16_no_null_terminator(
const uint8_t* src, size_t srcLen, char16_t* dst, size_t dstLen) {
if (dstLen == 0) {
if (src == nullptr || srcLen == 0 || dstLen == 0) {
return dst;
}
// A value > SSIZE_MAX is probably a negative value returned as an error and casted.
LOG_ALWAYS_FATAL_IF(dstLen > SSIZE_MAX, "dstLen is %zu", dstLen);
const uint8_t* const u8end = src + srcLen;
const uint8_t* u8cur = src;
const char16_t* const u16end = dst + dstLen;
char16_t* u16cur = dst;
while (u8cur < u8end && u16cur < u16end) {
size_t u8len = utf8_codepoint_len(*u8cur);
uint32_t codepoint = utf8_to_utf32_codepoint(u8cur, u8len);
const uint8_t* const in_end = src + srcLen;
const uint8_t* in = src;
const char16_t* const out_end = dst + dstLen;
char16_t* out = dst;
uint8_t c, c2, c3, c4;
char32_t w;
// Convert the UTF32 codepoint to one or more UTF16 codepoints
if (codepoint <= 0xFFFF) {
// Single UTF16 character
*u16cur++ = (char16_t) codepoint;
} else {
// Multiple UTF16 characters with surrogates
codepoint = codepoint - 0x10000;
*u16cur++ = (char16_t) ((codepoint >> 10) + 0xD800);
if (u16cur >= u16end) {
// Ooops... not enough room for this surrogate pair.
return u16cur-1;
}
*u16cur++ = (char16_t) ((codepoint & 0x3FF) + 0xDC00);
auto err_in = [&c, &out]() {
ALOGW("Unended UTF-8 byte: 0x%02x", c);
return out;
};
while (in < in_end && out < out_end) {
c = *in++;
if (LIKELY((c & 0x80) == 0)) {
*out++ = (char16_t)(c);
continue;
}
if (UNLIKELY(c < 0xc0)) {
ALOGW("Invalid UTF-8 leading byte: 0x%02x", c);
*out++ = (char16_t)(c);
continue;
}
if (LIKELY(c < 0xe0)) {
if (UNLIKELY(in + 1 > in_end)) {
return err_in();
}
c2 = *in++;
*out++ = (char16_t)(((c & 0x1f) << 6) | (c2 & 0x3f));
continue;
}
if (LIKELY(c < 0xf0)) {
if (UNLIKELY(in + 2 > in_end)) {
return err_in();
}
c2 = *in++; c3 = *in++;
*out++ = (char16_t)(((c & 0x0f) << 12) |
((c2 & 0x3f) << 6) | (c3 & 0x3f));
continue;
} else {
if (UNLIKELY(in + 3 > in_end)) {
return err_in();
}
if (UNLIKELY(c >= 0xf8)) {
ALOGW("Invalid UTF-8 leading byte: 0x%02x", c);
}
// Multiple UTF16 characters with surrogates
c2 = *in++; c3 = *in++; c4 = *in++;
w = utf8_4b_to_utf32(c, c2, c3, c4);
if (UNLIKELY(w < 0x10000)) {
*out++ = (char16_t)(w);
} else {
if (UNLIKELY(out + 2 > out_end)) {
// Ooops.... not enough room for this surrogate pair.
return out;
}
*out++ = (char16_t)(((w - 0x10000) >> 10) + 0xd800);
*out++ = (char16_t)(((w - 0x10000) & 0x3ff) + 0xdc00);
}
continue;
}
u8cur += u8len;
}
return u16cur;
return out;
}
}