【问题标题】:Stack allocation feature (performance)堆栈分配功能(性能)
【发布时间】:2016-12-14 21:24:22
【问题描述】:

在我的小性能问题调查中,我注意到一个有趣的堆栈分配功能,这里是测量时间的模板:

#include <chrono>
#include <iostream>

using namespace std;
using namespace std::chrono;

int x; //for simple optimization suppression
void foo();

int main()
{   
    const size_t n = 10000000; //ten millions
    auto start = high_resolution_clock::now();

    for (size_t i = 0; i < n; i++)
    {
        foo();
    }

    auto finish = high_resolution_clock::now();
    cout << duration_cast<milliseconds>(finish - start).count() << endl;
}

现在都是foo() 实现,在每个实现中将总共分配500000 ints:

  1. 分配在一个块中:

    void foo()
    {
        const int size = 500000;
        int a1[size];
    
        x = a1[size - 1];
    }  
    

    结果:7.3 秒;

  2. 分配在两个块中:

    void foo()
    {
        const int size = 250000;
        int a1[size];
        int a2[size];
    
        x = a1[size - 1] + a2[size - 1];
    }
    

    结果:3.5 秒;

  3. 分配在四个块中:

    void foo()
    {
        const int size = 125000;
        int a1[size];
        int a2[size];
        int a3[size];
        int a4[size];
    
        x = a1[size - 1] + a2[size - 1] +
            a3[size - 1] + a4[size - 1];
    } 
    

    结果:1.8 秒。

等等...我将它分成 16 个块并得到结果时间 0.38 秒。


请向我解释一下,为什么以及如何发生这种情况?
我使用了 MSVC 2013 (v120),发布版本。

UPD:
我的机器是x64平台。而且我是用Win32平台编译的。
当我使用 x64 平台编译它时,它在所有情况下都会产生大约 40 毫秒。
为什么平台选择会产生如此大的影响?

【问题讨论】:

  • 盲目:除了可能的编译器优化。您看到的性能肯定会因 Cache Misses 而受损...通常,您应该在最高优化级别进行基准测试。 :-)
  • 您的 PC 规格是什么?编译器版本和编译标志?
  • 请不要发布非标准void main的代码。 FTFY。
  • 我一直在 Linux 上的 GCC 4.9.2 上查看它,它产生的结果正是您所期望的。在所有优化级别,使用您的代码分配单个数组a1 比分配四个数组a1...a4 更快。在所有情况下,整个函数调用都需要不到 40 毫秒,而不是您所经历的几秒钟。上述所有时间之间的差异是微不足道的 - 最多大约 15 毫秒。
  • 编译器编写者love UB,它提供了很多让代码运行得更快的方法。您正在读取一个从未写入的值。优化器注意到这一点,知道任何值都足够好,所以 only 读取 a1[0]。这反过来又允许消除 a2、a3 和 a4。这使得堆栈帧更小。这使得它在 _chkstk 上花费的时间更少。所以执行时间与size的值成正比。

标签: c++ c performance stack allocation


【解决方案1】:

查看 VS2015 Update 3 的反汇编,在 foo 的 2 和 4 数组版本中,编译器优化了未使用的数组,以便它只为每个函数中的 1 个数组保留堆栈空间。由于后面的函数具有较小的数组,因此需要较少的时间。对 x 的赋值为两个/所有 4 个数组读取相同的内存位置。 (由于数组未初始化,因此读取它们是未定义的行为。)如果不优化代码,则会读取 2 或 4 个不同的数组。

这些函数花费的时间很长是由于__chkstk 作为堆栈溢出检测的一部分执行的堆栈探测(当编译器需要超过 1 页的空间来保存所有局部变量时是必需的)。

【讨论】:

  • 是的,就是这样!但是,如果我在 x64 上更改目标平台,为什么速度会如此显着提高呢?我希望将平台更改为 x64 会增加两倍的页面大小,即变为 8K。所以速度必须提高,但是有两种情况有问题(急剧增加)另一种情况如果你添加到每个数组声明= { 0 }(并从 n 中删除两个空值以减少等待)那么平台之间的速度不会增加(但所有3个案例一次工作)。也许是因为相对于其他的分配成本太小了?
【解决方案2】:

您应该查看生成的汇编代码,以了解您的编译器对代码的实际作用。对于 gcc/clang/icc,您可以使用Matt Godbolt's Compiler Explorer。

clang 优化了一切,因为 UB,结果是(foo - 第一个版本,foo2 - 第二个版本:

foo:                                    # @foo
        retq

foo2:                                   # @foo2
        retq

icc 对两个版本的处理非常相似:

foo:
        pushq     %rbp                                          #4.1
        movq      %rsp, %rbp                                    #4.1
        subq      $2000000, %rsp                                #4.1
        movl      -4(%rbp), %eax                                #8.9
        movl      %eax, x(%rip)                                 #8.5
        leave                                                   #10.1
        ret                                                     #10.1

foo2:
        pushq     %rbp                                          #13.1
        movq      %rsp, %rbp                                    #13.1
        subq      $2000000, %rsp                                #13.1
        movl      -1000004(%rbp), %eax                          #18.9
        addl      -4(%rbp), %eax                                #18.24
        movl      %eax, x(%rip)                                 #18.5
        leave                                                   #19.1
        ret 

和 gcc 为不同的版本创建不同的汇编代码。 6.1 版生成的代码会显示与您的实验类似的行为:

foo:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $2000016, %rsp
        movl    1999996(%rsp), %eax
        movl    %eax, x(%rip)
        leave
        ret
foo2:
        pushq   %rbp
        movl    $1000016, %edx  #only the first array is allocated
        movq    %rsp, %rbp
        subq    %rdx, %rsp
        leaq    3(%rsp), %rax
        subq    %rdx, %rsp
        shrq    $2, %rax
        movl    999996(,%rax,4), %eax
        addl    999996(%rsp), %eax
        movl    %eax, x(%rip)
        leave
        ret

因此,了解差异的唯一方法是查看您的编译器生成的汇编代码,其他一切都只是猜测。

【讨论】:

    猜你喜欢
    • 2011-02-11
    • 2012-12-23
    • 2020-04-20
    • 2017-11-22
    • 2011-03-05
    • 1970-01-01
    • 2014-06-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多