【问题标题】:why is c++ std::max_element so slow?为什么 c++ std::max_element 这么慢?
【发布时间】:2014-10-26 15:26:04
【问题描述】:

我需要找到向量中的最大元素,所以我使用std::max_element,但我发现它是一个非常慢的函数,所以我编写了自己的版本并设法获得 x3 更好的性能,这里是代码:

#include <string>
#include <iostream>
#include <vector>
#include <algorithm>

#include <sys/time.h>

double getRealTime()
{
    struct timeval tv;
    gettimeofday(&tv, 0);
    return (double) tv.tv_sec + 1.0e-6 * (double) tv.tv_usec;
}

inline int my_max_element(const std::vector<int> &vec, int size)
{
    auto it = vec.begin();
    int max = *it++;
    for (; it != vec.end(); it++)
    {
        if (*it > max)
        {
            max = *it;
        }
    }
    return max;
}

int main()
{
    const int size = 1 << 20;
    std::vector<int> vec;
    for (int i = 0; i < size; i++)
    {
        if (i == 59)
        {
            vec.push_back(1000000012);
        }
        else
        {
            vec.push_back(i);
        }
    }

    double startTime = getRealTime();
    int maxIter = *std::max_element(vec.begin(), vec.end());
    double stopTime = getRealTime();
    double totalIteratorTime = stopTime - startTime;

    startTime = getRealTime();
    int maxArray = my_max_element(vec, size);
    stopTime = getRealTime();
    double totalArrayTime = stopTime - startTime;

    std::cout << "MaxIter = " << maxIter << std::endl;
    std::cout << "MaxArray = " << maxArray << std::endl;
    std::cout << "Total CPU time iterator = " << totalIteratorTime << std::endl;
    std::cout << "Total CPU time array = " << totalArrayTime << std::endl;
    std::cout << "iter/array ratio: = " << totalIteratorTime / totalArrayTime << std::endl;
    return 0;
}

输出:

MaxIter = 1000000012
MaxArray = 1000000012
Total CPU time iterator = 0.000989199
Total CPU time array = 0.000293016
iter/array ratio: = 3.37592

平均而言,std::max_elementmy_max_element 多花 3 倍的时间。 那么为什么我能够如此轻松地创建一个更快的 std 函数呢?既然 std 太慢了,我应该停止使用 std 并编写自己的函数吗?

注意:起初我认为这是因为我在 for 循环中使用整数 i 而不是迭代器,但现在这似乎无关紧要了。

编译信息:

g++ (GCC) 4.8.2

g++ -O3 -Wall -c -fmessage-length=0 -std=c++0x

【问题讨论】:

  • 你在编译优化吗?
  • 您是否尝试过颠倒通话顺序?缓存可能在这里起作用。
  • this 是我得到的。 max_element 太慢了,甚至 max_element 也快了 70%
  • my_max_element 在空向量上中断,而std::max_element 需要检测和处理这种情况
  • 您的计时代码实现了灾难性取消,这使得整个问题变得毫无意义。请正确处理时间值(并且,在它们有意义的范围内进行,而不是在调度程序的变化范围内)。也就是说,从不 做任何事情,例如减去两个几乎相同的 float/double 值,并且永远不要使用 double 作为时间。此外,执行至少运行 2-3 秒(最好是 10 或 20 秒)而不是几微秒的基准测试。单个中断等很容易导致您看到的内容。

标签: c++ gcc vector iterator max


【解决方案1】:

您可能在 64 位模式下运行测试,其中sizeof(int) == 4,但sizeof(std::vector&lt;&gt;::iterator) == 8,因此循环中分配给intmy_max_element 所做的)比分配给std::vector&lt;&gt;::iterator(这就是std::max_element 所做的)。

如果您将std::vector&lt;int&gt; 更改为std::vector&lt;long&gt;,结果将更改为std::max_element

MaxIter = 1000000012
MaxArray = 1000000012
Total CPU time iterator = 0.00429082
Total CPU time array = 0.00572205
iter/array ratio: = 0.749875

一个重要的注意事项:当基准测试禁用 CPU 频率缩放时,CPU 不会在基准测试中间切换档位。


但我认为这里还有其他因素在起作用,因为只是将循环变量从 int 更改为 long 不会改变结果...

【讨论】:

  • 您是否建议在本机 64 位架构上 64 位对齐写入比 32 位对齐写入慢? (哎呀,更不用说寄存器写入了。)
  • 你是对的,即使不改变 for while,使用 std 的 long 也快得多。
  • 我重写了 my_max_element 以在内部使用迭代器,就像 max_element 所做的那样,它并没有太大变化 (coliru.stacked-crooked.com/a/367efade62603da0)。
  • @Mgetz,啊。我忘记了那个废话。谢谢。
【解决方案2】:

这是一个简单的缓存问题。也就是说,第一次加载内存时,在这种情况下是向量的内容,它总是比最近访问的要慢得多。我使用 GCC 4.9 复制并粘贴了您的代码。

函数反转时,比例为1,原序时,比例为1.6。

在我看来,就 max_element 而言,这仍然是 GCC 的根本性错误优化。但是,您的函数时间太短了,它们会像上面的缓存效果一样被 CPU 噪音所支配,而不是任何有意义的比较。

Reversed, Original

【讨论】:

  • 实际上,使用 g++ 4.8 和 -O3 在循环中运行函数((原始数组没有变化)我得到与 OP 相同的结果(std::max_element 慢三倍);即使我交换两个函数的执行顺序,结果仍然成立。
  • @Matteo:G++ 4.9 似乎并没有完全重现这里的错误优化。
  • 禁用 CPU 频率缩放。
【解决方案3】:

在对此答案投票之前,请在您的机器上测试(并验证)并评论/添加结果。请注意,我在测试中使用了 1000*1000*1000 的矢量大小。目前,这个答案有 19 人点赞,但只发布了一个结果,这些结果并没有显示出下面描述的效果(虽然是用不同的测试代码获得的,见 cmets)。


似乎有一个优化器错误/工件。比较时间:

template<typename _ForwardIterator, typename _Compare>
_ForwardIterator
my_max_element_orig(_ForwardIterator __first, _ForwardIterator __last,
_Compare __comp)
{
  if (__first == __last) return __first;
  _ForwardIterator __result = __first;

  while(++__first != __last)
    if (__comp(__result, __first))
      __result = __first;

  return __result;
}

template<typename _ForwardIterator, typename _Compare>
_ForwardIterator
my_max_element_changed(_ForwardIterator __first, _ForwardIterator __last,
_Compare __comp)
{
  if (__first == __last) return __first;
  _ForwardIterator __result = __first;
  ++__first;

  for(; __first != __last; ++__first)
    if (__comp(__result, __first))
      __result = __first;

  return __result;
}

第一个是original libstdc++ implementation,第二个应该是没有任何行为或要求变化的转换。 Clang++ 为这两个函数产生了非常相似的运行时间,而 g++4.8.2 的第二个版本快了四倍。


按照Maxim的提议,将向量从int改成int64_t,改后的版本不是4,而是比原版(g++4.8.2)快1.7倍。


区别在于*result的预测共用,即存储当前最大元素的值,这样就不必每次都从内存中重新加载。这提供了一个更清晰的缓存访问模式:

w/o commoning     with commoning
*                 *
**                 *
 **                 *
  **                 *
  * *                 *
  *  *                 *
  *   *                 *

这是用于比较的 asm(rdi/rsi 分别包含第一个/最后一个迭代器):

使用 while 循环(2.88743 毫秒;gist):

    movq    %rdi, %rax
    jmp .L49
.L51:
    movl    (%rdi), %edx
    cmpl    %edx, (%rax)
    cmovl   %rdi, %rax
.L49:
    addq    $4, %rdi
    cmpq    %rsi, %rdi
    jne .L51

使用 for 循环(1235.55 μs):

    leaq    4(%rdi), %rdx
    movq    %rdi, %rax
    cmpq    %rsi, %rdx
    je  .L53
    movl    (%rdi), %ecx
.L54:
    movl    (%rdx), %r8d
    cmpl    %r8d, %ecx
    cmovl   %rdx, %rax
    cmovl   %r8d, %ecx
    addq    $4, %rdx
    cmpq    %rdx, %rsi
    jne .L54
.L53:

如果我通过在开始时将*result 显式存储到变量prev 中以及每当更新result 并在比较中使用prev 而不是*result 来强制共享,我会得到一个更快的循环(377.601 μs):

    movl    (%rdi), %ecx
    movq    %rdi, %rax
.L57:
    addq    $4, %rdi
    cmpq    %rsi, %rdi
    je  .L60
.L59:
    movl    (%rdi), %edx
    cmpl    %edx, %ecx
    jge .L57
    movq    %rdi, %rax
    addq    $4, %rdi
    movl    %edx, %ecx
    cmpq    %rsi, %rdi
    jne .L59
.L60:

这比for 循环更快的原因是上面的条件移动(cmovl)是一种悲观,因为它们执行得很少(Linus says 那个 cmov 只是一个好主意,如果分支是不可预测的)。请注意,对于随机分布的数据,该分支预计会占用Hn 次,这是一个可以忽略不计的比例(Hn 以对数方式增长,因此 Hn/n 迅速接近0)。条件移动代码只会在病理数据上更好,例如[1, 0, 3, 2, 5, 4, ...]。

【讨论】:

  • 如果您可以在不同的机器/g++ 版本上运行它,请评论/添加您的结果。
  • 我在测试中使用了 1000*1000*1000 个元素,但我不太相信 OP 的测量代码。每个测试都进行了多次,并且按照正常 + 倒序进行。
  • 提示本地化差异,已在 cmets 中提到:-fno-tree-vectorize。请不要忘记在某个时候向 gcc 的 bugzilla 报告。
  • gcc-4.9.1,Intel Core i5:平均 2.88743 ms for while,1235.55 μs for for。我正在使用函数指针来防止内联和 volatile 以防止无操作消除;这导致除了对 max_element 函数的精确调用之外,测试主体具有相同的 asm。
  • 使用 GCC 9.2.0 显示 my_max_element_orig() 和 my_max_element_changed() 都没有问题(来自接受的答案) - 两者都显示相同的结果。但是,如果我使用指针和迭代器,我发现存在显着差异 - 使用迭代器对原始指针来说要差大约 2 倍。看我的问题stackoverflow.com/questions/58810888/…
猜你喜欢
  • 1970-01-01
  • 2014-11-23
  • 2020-02-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-12-15
相关资源
最近更新 更多