现代软件中的文本可以预期是 Unicode。 Unicode 以两种格式存储:UTF-8 和 UTF-16。
UTF-16 是多种平台和应用程序用来表示 Unicode 字符的编码系统。值得注意的是,Microsoft Windows 使用 UTF-16 进行内部操作、文件名和注册表项,而 Java 和 JavaScript 使用它来表示字符串。
UTF-16 是一种 Unicode 字符的编码方法,其中每个字符由一个或两个 16 位代码单元表示。对于基本多语言平面 (BMP) 中的字符(包括世界各地最常用的字符),单个 16 位单元就足够了。然而,对于该平面之外的字符(在补充平面中),UTF-16 使用一对称为代理项对的 16 位单元。这种双重方法允许 UTF-16 表示所有 Unicode 超过一百万个可能的字符,同时将大多数字符保留在 16 位结构内以提高效率。
组成代理对的值可以是高代理(U+D800 到 U+DBFF),也可以是低代理(U+DC00 到 U+DFFF)。一对总是由高代理和低代理组成。否则,我们就会出错。
UTF-16 中替换字符的需要源于该编码系统的复杂性。我们经常在高代理项后面没有低代理项以及低代理项前面没有高代理项的地方放置替换字符(通常为 U+FFFD 或 �)。使用替换字符在一定程度上是一个安全问题将文本从一种编码转换为另一种编码时,或者处理可能损坏的数据时,并非所有字符都可能正确映射或存在于两种编码中。在这种情况下,使用替换字符可确保文本处理继续进行,而不会崩溃或产生无意义的输出。它还向用户或开发人员发出信号,表明编码或传输过程中出现问题,从而通过防止误解或利用格式错误的数据来更好地管理数据错误或安全考虑。
布尔is_high_surrogate ( char16_t c ) { 返回( c > = 0xD800 && c < = 0xDBFF ) ; } 布尔is_low_surrogate ( char16_t c ) { 返回( c > = 0xDC00 && c < = 0xDFFF ) ; } 无效replace_invalid_utf16 ( char16_t *缓冲区, size_t长度) { for ( size_t i = 0 ; i <长度; + + i ) { if ( is_high_surrogate (缓冲区[ i ] ) ) { if ( i + 1 <长度&& is_low_surrogate (缓冲区[ i + 1 ] ) ) { 我++ ; }别的{ 缓冲区[ i ] = 0xFFFD ; // 替换字符 } } else if ( is_low_surrogate ( buffer [ i ] ) ) { 缓冲区[ i ] = 0xFFFD ; // 替换字符 } } }
Replace_invalid_utf16 函数扫描 char16_t 字符的缓冲区,确保任何高位代理后面跟着一个低位代理以形成有效对;如果不是,或者如果出现低代理项而前面没有高代理项,则它将用 Unicode 替换字符 (U+FFFD) 替换无效字符,从而有效地纠正 UTF-16 编码。该功能应该合理有效。
我们的大多数处理器都具有能够处理寄存器的指令,每个寄存器有 8 个 16 位字。如今大多数移动处理器都是 64 位 ARM 处理器,具有强大的 ARM NEON 指令。
我们可以使用内部函数编写一个针对 ARM NEON 的函数。这些特殊函数使我们能够低级访问 ARM NEON 的独特功能。其他处理器系列(例如 Intel/AMD、RISC-V、Loonson 等)也有类似的内在函数。
无效replace_invalid_utf16_neon ( char16_t *缓冲区, size_t长度) { 常量size_t vec_size = 8 ; 大小_t i = 0 ; 如果(长度> = vec_size ) { uint16x8_t 替换= vdupq_n_u16 ( 0xFFFD ) ; uint16x8_t previous_high_surrogate_mask = vdupq_n_u16 ( 0 ) ; for ( ; i + vec_size < =长度; i + = vec_size ) { uint16x8_t vec = vld1q_u16 ( ( const uint16_t * )缓冲区+ i ) ; uint16x8_t low_surrogate_mask = vcleq_u16 ( vaddq_u16 ( vec , vdupq_n_u16 ( 0x2400 ) ) , vdupq_n_u16 ( 0x03ff ) ) ; uint16x8_t high_surrogate_mask = vcleq_u16 ( vaddq_u16 ( vec , vdupq_n_u16 ( 0x2800 ) ) , vdupq_n_u16 ( 0x03ff ) ) ; uint16x8_t offset_high_surrogate_mask = vextq_u16 ( previous_high_surrogate_mask , high_surrogate_mask , 7 ) ; uint16x8_t offset_low_surrogate_mask = ( i + vec_size <长度 && is_low_surrogate (缓冲区 [ i + vec_size ] ) ) ? vextq_u16 (低代理掩码, vdupq_n_u16 ( 0xFFFF ) , 1 ) : vextq_u16 ( low_surrogate_mask , vdupq_n_u16 ( 0 ) , 1 ) ; uint16x8_t low_not_preceded_by_high = vbicq_u16 (低代理掩码,偏移高代理掩码) ; uint16x8_t high_not_followed_by_low = vbicq_u16 (高代理掩码,偏移低代理掩码) ; uint16x8_t invalid_pair_mask = vorrq_u16 ( low_not_preceded_by_high , high_not_followed_by_low ) ; uint16x8_t 结果= vbslq_u16 ( invalid_pair_mask , replacement , vec ) ; vst1q_u16 ( ( uint16_t * )缓冲区+ i ,结果) ; previous_high_surrogate_mask = high_surrogate_mask ; } } // 处理剩余元素或小缓冲区 for ( ; i <长度; + + i ) { if ( is_high_surrogate (缓冲区[ i ] ) ) { if ( i + 1 <长度&& is_low_surrogate (缓冲区[ i + 1 ] ) ) { 我++ ; }别的{ 缓冲区[ i ] = 0xFFFD ; // 替换字符 } } else if ( is_low_surrogate ( buffer [ i ] ) ) { 缓冲区[ i ] = 0xFFFD ; // 替换字符 } } }
此函数,replace_invalid_utf16_neon,使用 ARM NEON 指令有效地验证和纠正 8 个字符块中的 UTF-16 编码文本。它通过设置替换字符 (0xFFFD) 的向量和用于跟踪高代理的掩码来进行初始化。对于每个块,它加载数据,创建掩码来识别高代理项和低代理项,移动这些掩码以检查跨向量边界的有效代理项对,然后替换任何无效字符(那些是单独的低代理项或后面没有跟随的高代理项)低代理)与替换字符。在处理尽可能多的完整块后,它使用标量操作处理任何剩余或较小的块,以确保纠正所有无效的 UTF-16 序列。虽然效率相当高,但我希望它可以比这个函数做得更好。
为了对这些函数进行基准测试,我使用了由 1000 万个空格字符组成的单个字符串。这是最简单的情况,因为没有替换也没有代理对。我怀疑这也代表了一个典型的情况:大多数文本中的代理对相对较少。使用 LLVM 16 和 Apple M2 处理器,我得到以下结果:
氖 | 5.5GB/秒 |
常规的 | 1.7GB/秒 |
因此 ARM NEON 代码速度快了 3 倍以上。我怀疑更好的优化可能会让我们接近 10 GB/s。
原文: https://lemire.me/blog/2024/12/29/efficient-in-place-utf-16-unicode-correction-with-arm-neon/