关于gcc:为什么c ++ std :: max_element这么慢?

why is c++ std::max_element so slow?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <string>
#include <iostream>
#include <vector>
#include

#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;
}

输出:

1
2
3
4
5
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

G++-O3-墙-C-FEMIT长度=0 -STD= C++0X


在对该答案进行投票之前,请在您的计算机上测试(并验证)并评论/添加结果。注意,我的测试使用了1000*1000*1000的向量大小。目前,这个答案有19个赞成票,但只有一个公布的结果,这些结果没有显示下面描述的效果(虽然获得了不同的测试代码,见评论)。

似乎存在优化器错误/工件。比较以下时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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;
}

第一个是最初的libstdc++实现,第二个应该是一个没有任何行为或需求变化的转换。clang++为这两个函数生成非常相似的运行时间,而G++4.8.2的运行速度是第二个版本的四倍。

根据Maxim的建议,将矢量从int改为int64_t,修改后的版本不是4,而是比原来的版本(g++4.8.2)快1.7倍。

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

1
2
3
4
5
6
7
8
w/o commoning     with commoning
*                 *
**                 *
 **                 *
  **                 *
  * *                 *
  *  *                 *
  *   *                 *

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

使用while循环(2.88743 ms;gist):

1
2
3
4
5
6
7
8
9
10
    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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    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时,我强制将*result显式存储到变量prev中,并且在比较中使用prev而不是*result,我会得到更快的循环(377.601μs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    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说,如果分支不可预测,cmov只是一个好主意)。请注意,对于随机分布的数据,分支预计取hntimes,这是一个可忽略的比例(hn以对数方式增长,因此hn/n快速接近0)。条件移动代码只在病理数据上更好,例如[1,0,3,2,5,4,…]。


您可能在64位模式下运行测试,其中sizeof(int) == 4,但sizeof(std::vector<>::iterator) == 8,因此循环中分配给int(my_max_element所做的)比std::vector<>::iterator更快(这是std::max_element所做的)。

如果将std::vector改为std::vector结果改为std::max_element有利:

1
2
3
4
5
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不会改变结果…


这是缓存的一个简单问题。也就是说,第一次加载内存时,在本例中是向量的内容,它总是比最近访问时慢得多。我用GCC4.9复制并粘贴了您的代码。

当函数反转时,比率为1。当它们按原始顺序排列时,比率为1.6。

对于我来说,在max_元素的情况下,这仍然是gcc的一个基本错误优化。但是,您的功能时间非常短,它们将由CPU噪声控制,如上面的缓存效果,而不是任何有意义的比较。

反转,原始