Skip to content

搞英语 → 看世界

翻译英文优质信息和名人推特

Menu
  • 首页
  • 作者列表
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
Menu

快速计算 Latin 1 字符串的 UTF-8 大小(AVX 版)

Posted on 2023-02-17

肖像2018facebook.jpg

计算机使用字节表示字符串。大多数情况下,我们使用 Unicode 标准以字节表示字符。在线交换字符串的通用格式称为 UTF-8。它可以表示超过一百万个字符,同时保持与古老的 ASCII 格式的兼容性。

尽管我们的大部分软件堆栈已转移到 Unicode 字符串,但仍有较旧的标准,如用于欧洲字符串的 Latin 1。因此我们经常需要将 Latin 1 字符串转换为 UTF-8。首先计算最终 UTF-8 字符串的大小(以字节为单位)很有用。

幸运的是,这是一个简单的问题:对于每个设置了最高有效位的输入字节,计算两个输出字节就足够了,对于其他字节计算一个输出字节就足够了。一个相对简单的 C++ 函数就足够了:

 size_t scalar_utf8_length ( const char * c , size_t len ) {  
   
size_t答案= 0 ;  
   
对于( size_t i = 0 ; i < len ; i + + ) {  
   
如果( ( c [ i ] > > 7 ) ) {回答+ + ; }  
   
}  
   
返回答案+ len ;  
   
}  
   
  
   

为了更快,我们可能想使用奇特的“SIMD”指令:一次处理多个字节的指令。您的编译器可能已经使用简单的 C 函数进行了一些自动向量化。在编译时,它会使用一些 SIMD 指令。但是,您可以尝试手动编写您自己的版本。

我们有多种指令集可供选择,但让我们选择 AVX2 指令集,它在当今大多数 x64 处理器上可用。 AVX2 有一个可以提取所有最高有效位的快速掩码函数,然后是另一个可以对它们进行计数的快速函数(popcount)。因此,以下例程应该很好(归功于 Anna Henningsen):

 size_t avx2_utf8_length_basic ( const uint8_t * str , size_t len ) {  
   
size_t answer = len / sizeof ( __m256i ) * sizeof ( __m256i ) ;  
   
大小我;  
   
对于( i = 0 ; i + sizeof ( __m256i ) < = len ; i + = 32 ) {  
   
    __m256i 输入= _mm256_loadu_si256 ( ( const __m256i * ) ( str + i ) ) ;  
   
   answer + = __builtin_popcount ( _mm256_movemask_epi8 (输入) ) ;  
   
}  
   
返回答案+ scalar_utf8_length ( str + i , len - i ) ;  
   
}  
   
  
   

你能做得更好吗?在 Intel 处理器上,“移动掩码”和人口计数指令都很快,但它们有一些延迟:它们需要几个周期才能执行。它们也可能有额外的执行约束。对于像 AVX-512 这样的指令集,部分延迟和约束不会变得更好,因为它需要在每次迭代时从 SIMD 寄存器移动到通用寄存器。将此例程移植到 ARM 处理器同样会有些挑战。

因此我们希望依靠更便宜的指令,并留在 SIMD 寄存器中直到结束。即使它没有提高 AVX 代码的速度,它也可能在算法上与其他指令集一起工作得更好。

为此,我们可以从论文Faster Population Counts Using AVX2 Instructions (Computer Journal 61 (1), 2018) 中借用一个方法。这个想法是快速提取位,并将它们添加到 SIMD 寄存器内的计数器中,并且只在最后精确计算值。

代码稍微复杂一些,因为我们有一个内部循环。在内循环中,我们使用 8 位计数器,在内循环结束时才移动到 64 位计数器。为保证不溢出,内循环只能运行255次迭代。代码如下所示……

 size_t avx2_utf8_length_mkl ( const uint8_t * str , size_t len ) {  
   
size_t answer = len / sizeof ( __m256i ) * sizeof ( __m256i ) ;  
   
尺寸_t我= 0 ;  
   
  __m256i four_64bits = _mm256_setzero_si256 ( ) ;  
   
while ( i + sizeof ( __m256i ) < = len ) {  
   
    __m256i 转轮= _mm256_setzero_si256 ( ) ;  
   
    size_t迭代次数= ( len - i ) / sizeof ( __m256i ) ;  
   
如果(迭代次数> 255 ) {迭代次数= 255 ; }  
   
size_t max_i = i + iterations * sizeof ( __m256i ) - sizeof ( __m256i ) ;  
   
对于( ; i < = max_i ; i + = sizeof ( __m256i ) ) {  
   
      __m256i 输入= _mm256_loadu_si256 ( ( const __m256i * ) ( str + i ) ) ;  
   
      转轮= _mm256_sub_epi8 (  
   
        转轮, _mm256_cmpgt_epi8 ( _mm256_setzero_si256 ( ) ,输入) ) ;  
   
}  
   
    four_64bits = _mm256_add_epi64 ( four_64bits ,   
   
      _mm256_sad_epu8 (转轮, _mm256_setzero_si256 ( ) ) ) ;  
   
}  
   
  答案+ = _mm256_extract_epi64 ( four_64bits , 0 ) +  
   
    _mm256_extract_epi64 ( four_64bits , 1 ) +  
   
    _mm256_extract_epi64 ( four_64bits , 2 ) +  
   
    _mm256_extract_epi64 ( four_64bits , 3 ) ;  
   
返回答案+ scalar_utf8_length ( str + i , len - i ) ;  
   
}  
   

我们还可以进一步展开内部循环以节省指令数量。

我用 AMD Rome (Zen2) 服务器和 GCC 11 ( -O3 -march=native ) 编写了一个带有8kB 随机输入的小型基准测试。结果将根据您的输入、编译器和处理器而有所不同。

功能 周期/字节 指令/字节 指令/周期
标量(无 autovec) 0.89 3.3 3.8
标量(w. autovec) 0.56 0.71 1.27
AVX2(移动掩码) 0.055 0.15 2.60
AVX2(在 SIMD 中) 0.039 0.15 3.90
AVX2(SIMD 内/展开) 0.028 0.07 2.40

所以我测试中最快的方法比纯标量版本快 30 多倍。如果我允许编译器对标量向量进行“自动向量化”,它的速度会提高大约 50%。

这是一个有趣的场景,因为按周期退出的指令数量变化很大。稍微复杂一点的“in-SIMD”函数比“movemask”函数做得更好,因为在这些测试中,它设法在每个周期退出更多指令。展开版本速度很快,每个周期需要的指令很少,但每个周期退出的指令数量“相对”较少。

我的代码可用。应该可以进一步调整此代码。您需要特权访问才能运行基准测试,因为我依赖性能计数器。

进一步的工作:没有必要明确地使用 SIMD 指令,您可以使用SWAR 来代替可移植性和性能之间的折衷。

原文: https://lemire.me/blog/2023/02/16/computing-the-utf-8-size-of-a-latin-1-string-quickly-avx-edition/

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Abhinav
  • Abigail Pain
  • Adam Fortuna
  • Alberto Gallego
  • Alex Wlchan
  • Answer.AI
  • Arne Bahlo
  • Ben Carlson
  • Ben Kuhn
  • Bert Hubert
  • Bits about Money
  • Brian Krebs
  • ByteByteGo
  • Chip Huyen
  • Chips and Cheese
  • Christopher Butler
  • Colin Percival
  • Cool Infographics
  • Dan Sinker
  • David Walsh
  • Dmitry Dolzhenko
  • Dustin Curtis
  • Elad Gil
  • Ellie Huxtable
  • Ethan Marcotte
  • Exponential View
  • FAIL Blog
  • Founder Weekly
  • Geoffrey Huntley
  • Geoffrey Litt
  • Greg Mankiw
  • Henrique Dias
  • Hypercritical
  • IEEE Spectrum
  • Investment Talk
  • Jaz
  • Jeff Geerling
  • Jonas Hietala
  • Josh Comeau
  • Lenny Rachitsky
  • Liz Danzico
  • Lou Plummer
  • Luke Wroblewski
  • Matt Baer
  • Matt Stoller
  • Matthias Endler
  • Mert Bulan
  • Mostly metrics
  • News Letter
  • NextDraft
  • Non_Interactive
  • Not Boring
  • One Useful Thing
  • Phil Eaton
  • Product Market Fit
  • Readwise
  • ReedyBear
  • Robert Heaton
  • Ruben Schade
  • Sage Economics
  • Sam Altman
  • Sam Rose
  • selfh.st
  • Shtetl-Optimized
  • Simon schreibt
  • Slashdot
  • Small Good Things
  • Taylor Troesh
  • Telegram Blog
  • The Macro Compass
  • The Pomp Letter
  • thesephist
  • Thinking Deep & Wide
  • Tim Kellogg
  • Understanding AI
  • 英文媒体
  • 英文推特
  • 英文独立博客
©2025 搞英语 → 看世界 | Design: Newspaperly WordPress Theme