假设你有一个长字符串,并且希望每 72 个字符插入一个换行符。如果你需要将公钥写入文本文件,则可能需要这样做。
一个简单的 C 函数就足够了。我用字母 K 来表示每行的长度。我将数据从输入缓冲区复制到输出缓冲区。
void insert_line_feed ( const char *缓冲区, size_t长度, int K , char *输出) { 如果( K = = 0 ) { memcpy (输出,缓冲区,长度) ; 返回; } size_t输入位置= 0 ; size_t下一行进给= K ; while (输入位置<长度) { 输出[ 0 ] =缓冲区[输入位置] ; 输出++ ; 输入位置++ ; 下一行进纸-- ; 如果( next_line_feed == 0 ) { 输出[ 0 ] = '\n' ; 输出++ ; 下一行进纸= K ; } } }
这种逐个字符复制的过程可能效率不高。为了加快速度,我们可以调用memcpy来复制数据块。
void insert_line_feed_memcpy ( const char *缓冲区, size_t长度, int K , char *输出) { 如果( K = = 0 ) { memcpy (输出,缓冲区,长度) ; 返回; } size_t输入位置= 0 ; while (输入位置+ K <长度) { std :: memcpy (输出,缓冲区+输入位置, K ) ; 输出+ = K ; 输入位置+ = K ; 输出[ 0 ] = '\n' ; 输出++ ; } std :: memcpy (输出,缓冲区+输入位置,长度-输入位置) ; }
memcpy函数很可能被简化成几条指令。例如,如果你针对最新的 AMD 处理器 (Zen 5) 进行编译,当行数 (K) 为 64 时,它可能只会生成两条指令(两条vmovups 指令)。
我们可以做得更好吗?
总的来说,我认为你不会比使用memcpy函数做得更好。编译器只是对其进行了很好的优化。
然而,探索是否应该刻意使用 SIMD 指令来优化这段代码或许会很有趣。SIMD(单指令多数据)指令可以用一条指令同时处理多个数据元素: memcpy函数会自动使用它。我们可以通过内部函数来利用 SIMD 指令,这些内部函数是编译器提供的接口,可以直接访问特定于处理器的指令,从而优化性能,同时保持高级代码的可读性。
让我重点介绍 AVX2,这是所有 x64(Intel 和 AMD)处理器实际上都支持的指令集。我们可以加载 32 字节寄存器并写入 32 字节寄存器。因此,我们需要一个函数,该函数接受一个 32 字节寄存器并在其中的某个位置( N )插入换行符。对于N小于 16 的情况,该函数会使用_mm256_alignr_epi8和_mm256_blend_epi32将输入向量右移一个字节以正确对齐数据,然后再应用重排掩码并插入换行符。当N等于或大于 16 时,它会直接使用预先计算的shuffle_masks数组中的重排掩码来重新排序输入字节并插入换行符,利用与“0x80”的比较来识别插入点,并将结果与换行符向量混合,以实现高效的并行处理。
内联__m256i insert_line_feed32 ( __m256i 输入, int N ) { __m256i 行进_向量= _mm256_set1_epi8 ( '\n' ) ; __m128i 身份= _mm_setr_epi8 ( 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9、10、11、12、13、14、15 ) ; 如果( K > = 16 ) { __m128i maskhi = _mm_loadu_si128 ( shuffle_masks [ N - 16 ] ) ; __m256i 掩码= _mm256_set_m128i ( maskhi ,身份) ; __m256i lf_pos = _mm256_cmpeq_epi8 (掩码, _mm256_set1_epi8 ( 0x80 ) ) ; __m256i shuffled = _mm256_shuffle_epi8 (输入,掩码) ; __m256i 结果= _mm256_blendv_epi8 (打乱, 行进给向量, lf_pos ) ; 返回结果; } // 将输入右移 1 个字节 __m256i 移位= _mm256_alignr_epi8 ( 输入, _mm256_permute2x128_si256 (输入,输入, 0x21 ) , 15 ) ; 输入= _mm256_blend_epi32 (输入,移位, 0xF0 ) ; __m128i masklo = _mm_loadu_si128 ( shuffle_masks [ N ] ) ; __m256i 掩码= _mm256_set_m128i (身份, masklo ) ; __m256i lf_pos = _mm256_cmpeq_epi8 (掩码, _mm256_set1_epi8 ( 0x80 ) ) ; __m256i shuffled = _mm256_shuffle_epi8 (输入,掩码) ; __m256i 结果= _mm256_blendv_epi8 (打乱, 行进给向量, lf_pos ) ; 返回结果; }
使用这样一个奇特的函数能提高速度吗?让我们测试一下。我编写了一个基准测试。我在装有 GCC 12 的 Intel Ice Lake 处理器上输入了一个较大的字符串。
逐个字符 | 1.0 GB/秒 | 8.0 英寸/字节 |
内存复制 | 11 GB/秒 | 0.46 英寸/字节 |
AVX2 | 16 GB/秒 | 0.52 英寸/字节 |
在我的测试中,尽管使用了更多指令,但手工编写的 AVX2 方法比memcpy方法速度更快。然而,手工编写的 AVX2 方法使用更少的指令将数据存储到内存中。
原文: https://lemire.me/blog/2025/09/07/splitting-a-long-string-in-lines-efficiently/