为什么 std::count 和 std::find 没有针对使用 memchr 进行优化?

Why aren't std::count and std::find optimised to use memchr?

本文关键字:std memchr 优化 count find 为什么      更新时间:2023-10-16

我正在阅读 sehe 对这个问题的回答,并惊讶地发现使用std::memchr的手写循环比使用std::count3 倍以上(见评论)。使用std::count的代码可以在 edit 2 中看到,但它基本上可以归结为:

const auto num_lines = std::count(f, l, 'n');

uintmax_t num_lines = 0;
while (f && f != l)
if ((f = static_cast<const char*>(memchr(f, 'n', l - f))))
num_lines++, f++;

我本来希望std::count版本至少与std::memchr版本一样快 - 原因与为什么使用std::copy应该至少与std::memcpy一样快。

我检查了我的标准库(libc ++)的std::count实现,没有尝试针对char输入类型进行优化(std::find也是如此)。

这是为什么呢?如果提供char*迭代器和char值,实现是否不能调度到std::memchr

只有当比赛之间的平均距离不小时,使用实际的函数调用来memchr才是胜利。

特别是对于count,如果您计算t个字符,当它们平均每 2 或每 4 出现一次时,调用memchr可能会慢得多。 (例如,使用ACGT字母表的DNA碱基对)。

我对使用memchr循环作为std::countchar数组的默认实现持怀疑态度。 更有可能有其他方法可以调整源代码,以便它编译为更好的asm。

对于find来说,这将更有意义,即使它确实可能会显着增加开销,而不是在前几个字节中有命中时,一次简单的字节循环。


您也可以将其视为编译器错过的优化。 如果编译器为std::countstd::find中的循环制作了更好的代码,那么调用手写的asm库函数的收益就会减少。

GCC 和 Clang 在进入循环之前不知道行程计数时,从不自动矢量化循环。 (即它们不执行搜索循环,这对于小至字节的元素大小是一个主要的遗漏优化)。 ICC 没有此限制,并且可以矢量化搜索循环。 不过,我还没有看过libc ++的std::count或find是如何做到的。

std::count必须检查每个元素,因此它应该自动矢量化。 但是,如果 gcc 或 clang 甚至没有-O3,那就很不幸了。 它应该在 x86 上使用pcmpeqb(打包的比较字节)很好地矢量化,然后paddb0/-1 比较结果。 (至少每 255 次迭代一次,psadbw与零相对以水平对字节元素求和)。

库函数调用开销至少是使用来自内存的函数指针的间接调用(可能会缓存未命中)。 在具有动态链接的 Linux 上,通常也会通过 PLT 进行额外的jmp(除非您使用-fno-plt进行编译)。memchrstrchr更容易优化,启动开销低,因为您可以快速检查 16B 矢量加载是否可以超过末尾(而不是对齐指针strchrstrlen以避免越过页面或缓存行边界)

如果调用memchr是在asm中实现某些东西的最佳方式,那么理论上这就是编译器应该发出的。 GCC/CLANG 已经优化了对 libcmemcpy调用的大型复制循环,具体取决于目标选项(-march=)。 例如,当副本足够大时,libc 版本可能会决定在 x86 上使用 NT 存储。