为什么c ++ std :: max_element这么慢?

时间:2014-09-02 11:16:09

标签: c++ gcc vector iterator max

我需要在向量中找到max元素,所以我使用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多花费x3倍。 那么为什么我能够如此轻松地创建更快的std函数呢?我应该停止使用std并编写自己的函数,因为std太慢了吗?

注意:起初我虽然是因为我在for循环中使用了整数i而不是迭代器,但现在接缝并不重要。

汇编信息:

g ++(GCC)4.8.2

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

3 个答案:

答案 0 :(得分:28)

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


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

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的预测共通,即存储当前max元素的值,以便每次都不必从内存重新加载。这提供了更清晰的缓存访问模式:

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

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

使用while循环(2.88743 ms; 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次,这是一个可以忽略不计的比例(H n 以对数方式增长,因此H n / n迅速接近0)。条件移动代码仅对病理数据更好,例如[1,0,3,2,5,4,......]。

答案 1 :(得分:10)

您可能正在以64位模式运行测试,其中sizeof(int) == 4只有sizeof(std::vector<>::iterator) == 8,因此循环中的赋值为intmy_max_element做什么)比std::vector<>::iterator更快(这是std::max_element所做的)。

如果您将std::vector<int>更改为std::vector<long>,则将结果更改为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不会改变结果......

答案 2 :(得分:2)

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

当功能反转时,比率为1.当它们处于原始顺序时,比率为1.6。

对于我来说,对于max_element来说,这仍然是GCC的一个基本错误优化。但是,你的功能时间很短,它们将受到CPU噪声的控制,就像上面的缓存效果一样,而不是任何有意义的比较。

ReversedOriginal