【问题标题】:Why aren't std::count and std::find optimised to use memchr?为什么没有优化 std::count 和 std::find 以使用 memchr?
【发布时间】:2017-04-18 22:41:35
【问题描述】:

我正在阅读 sehe's answerthis question 并惊讶地发现使用 std::memchr 的手写循环比使用 std::count3 倍以上(请参阅 cmets )。使用std::count的代码可以在编辑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 版本一样快——原因与using std::copy should be at least as fast as std::memcpy 类似。

我检查了我的标准库 (libc++) 的 std::count 实现,并没有尝试优化 char 输入类型(std::find 同上)。

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

【问题讨论】:

  • 它是开源的。提交补丁:-)。
  • @rici,哈哈......然而,实际上提交补丁通常是最简单的部分。应用它是真正的挑战。例如,我提交了一个 libstdc++ patch which specializes std::find() 用于简单的随机访问案例,只需调用 __builtin_memchr()。这是 2 年前的事了 - 只是提交它并没有引起任何兴趣。

标签: c++ performance c++-standard-library


【解决方案1】:

只有在匹配之间的平均距离不小的情况下,使用对memchr 的实际函数调用才是胜利。

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

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

对于find,它会更有意义,即使与简单的逐字节循环相比,如果前几个字节有命中,它确实可能会显着增加开销。


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

gcc 和 clang 在进入循环之前不知道行程计数时从不自动矢量化循环。 (即他们不进行搜索循环,这是对小至字节的元素大小的主要优化缺失)。 ICC 没有这个限制,并且可以矢量化搜索循环。不过,我还没有研究过它如何处理 libc++ 的 std::count 或 find 。

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

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

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

【讨论】:

  • @BeeOnRope:嗯,好点。我的答案的那部分没有经过深思熟虑。使用memchr,触摸第二个可能不需要的缓存行仍然是同一对象的一部分。与strlen 不同,memchr 的调用代码也可能在匹配后从内存中加载。我不确定最佳strlen 启动策略。我猜包含字符串开头的对齐向量负载是好的,然后 pcmpeqb / pmovmskb / not / bsr 并与p&amp;-16(字符串开始相对于16B对齐的偏移量)进行比较。
  • 嗯,根据我对 C11 标准的阅读,您实际上无法对您建议的 memchr 进行优化[1]:即使用户传入 n > 16 ,如果要跨页,您可能无法读取 16 个字节,因为之前可以找到该元素(即,如果函数永远不会访问越界区域,则用户可以传递太长的长度,因为它找到了元素)。不过我不确定,所以我问了here。 [1] 我假设您建议阅读用户传递的 n 所暗示的整个区域是安全的。
  • @BeeOnRope:您不喜欢包含缓冲区开头的对齐矢量加载的想法吗?您可以 BSF 它忽略从您应该查看的字节开始之前的匹配。这甚至避免了跨越缓存线。没什么大不了的,但它避免了任何分支。嗯,我想如果矢量加载包含您应该查看的区域之前的 之前的数据,这并不容易。我猜pmovmskb 结果的可变计数移位可能会有所帮助......但也许分支更容易。
  • 我也喜欢这个想法,但是如果您忽略访问下一个缓存行的“问题”,它似乎更糟,因为您 (a) 在第一次加载时检查的字节数较少,这是不好的本身(完成的工作更少),它还意味着(b)分支行为的更糟糕的量化(c)稍微更多的工作来掩盖/检查你在缓冲区之前不匹配。它是 (b) 最能咬你的:如果你的大部分匹配都很短,那么在第一个加载和分支中捕获所有
  • 当然,你不能忽略进入下一个缓存行的成本,所以也许B毕竟更好。很难以一般的方式比较它们(当然对于特定的应用程序,一个可能明显优于另一个)。如果您只进行对齐加载,您还可以避免在页面末尾附近出现特殊情况。
猜你喜欢
  • 2016-04-10
  • 2017-08-13
  • 2016-05-28
  • 1970-01-01
  • 2021-03-16
  • 2013-06-19
  • 2016-09-04
  • 2021-05-10
相关资源
最近更新 更多