c++ - 为什么编译超过100,000行的std::vector::push_back需要很长时间?

标签 c++ gcc compilation stdvector

我正在编译一个C++库,该库定义了一个从一组数据点中随机采样的函数。数据点存储在std::vector中。有126,272个std::vector push_back语句,其中所涉及的 vector 的类型为double。编译需要很长时间。

为什么要花这么长时间? (除了std::vector push_back语句外,所有其他代码的编译时间都将少于1秒,因为其他代码很少。)

最佳答案

gcc中有-ftime-report选项,可打印每个编译器阶段浪费的时间的详细报告。

我将ubuntu 12.04 64位和gcc 4.6.3一起使用,此代码可重现您的情况:

#include <vector>
using namespace std;

int main()
{
  vector<double> d;

 d.push_back(5.7862517058766);
/* ... N lines generated with 
  perl -e 'print(" d.push_back(",rand(10),");\n") for 1..100000'
*/
 d.push_back(3.77195464257674);

  return d.size();
}

有各种N的-ftime-report输出(由于PC上的背景负载,wall时间不准确,因此请查看user timeusr):

N = 10000
$ g++ -ftime-report ./pb10k.cpp

Execution times (seconds)
...
 expand vars           :   1.48 (47%) usr   0.01 ( 7%) sys   1.49 (44%) wall    1542 kB ( 2%) ggc
 expand                :   0.11 ( 3%) usr   0.01 ( 7%) sys   0.10 ( 3%) wall   19187 kB (30%) ggc
...
 TOTAL                 :   3.18             0.15             3.35              64458 kB

N = 100000
$ g++ -ftime-report ./pb100k.cpp

Execution times (seconds)
....
 preprocessing         :   0.49 ( 0%) usr   0.28 ( 5%) sys   0.59 ( 0%) wall    6409 kB ( 1%) ggc
 parser                :   0.96 ( 0%) usr   0.39 ( 6%) sys   1.41 ( 0%) wall  108217 kB (18%) ggc
 name lookup           :   0.06 ( 0%) usr   0.07 ( 1%) sys   0.24 ( 0%) wall    1023 kB ( 0%) ggc
 inline heuristics     :   0.13 ( 0%) usr   0.00 ( 0%) sys   0.20 ( 0%) wall       0 kB ( 0%) ggc
 integration           :   0.03 ( 0%) usr   0.00 ( 0%) sys   0.04 ( 0%) wall    4095 kB ( 1%) ggc
 tree gimplify         :   0.22 ( 0%) usr   0.00 ( 0%) sys   0.23 ( 0%) wall   36068 kB ( 6%) ggc
 tree eh               :   0.06 ( 0%) usr   0.00 ( 0%) sys   0.14 ( 0%) wall    5678 kB ( 1%) ggc
 tree CFG construction :   0.08 ( 0%) usr   0.01 ( 0%) sys   0.10 ( 0%) wall   38544 kB ( 7%) ggc
....
 expand vars           : 715.98 (97%) usr   1.62 (27%) sys 718.32 (83%) wall   18359 kB ( 3%) ggc
 expand                :   1.04 ( 0%) usr   0.09 ( 1%) sys   1.64 ( 0%) wall  190836 kB (33%) ggc
 post expand cleanups  :   0.09 ( 0%) usr   0.01 ( 0%) sys   0.15 ( 0%) wall      43 kB ( 0%) ggc
....
 rest of compilation   :   1.94 ( 0%) usr   2.56 (43%) sys 102.42 (12%) wall   63620 kB (11%) ggc
 TOTAL                 : 739.68             6.01           866.46             586293 kB

因此,在“ expand vars ”阶段中,对于巨大的N还有一些额外的工作。此阶段正好在以下行中:cfgexpand.c:4463(在TV_VAR_EXPAND宏之间)。

有趣的事实:我使用自定义编译的32位g++ 4.6.2的编译时间非常短(N = 100000约为20秒)。

我的g++和ubuntu g++有什么区别?一个是turning on by default Ubuntu中的Gcc堆栈保护(-fstack-protect选项)。并且此保护仅添加到“expand vars”阶段(在源cfgexpand.c:1644,expand_used_vars()中找到;提到了here):

N = 100000,使用选项 -fno-stack-protector 禁用了堆栈保护器(将其用于您的代码):
$ g++ -ftime-report -fno-stack-protector pb100k.cpp 2>&1 |egrep 'TOTAL|expand vars'
 expand vars           :   0.08 ( 0%) usr   0.01 ( 1%) sys   0.09 ( 0%) wall   18359 kB ( 3%) ggc
 TOTAL                 :  23.05             1.48            24.60             586293 kB

运行时间从24秒降低到24秒。

更新:

callgrind(Valgrind的调用图分析工具)中启动gcc之后,我可以说有N个堆栈变量。如果启用了堆栈保护器,则会使用三种O(N ^ 2)算法在“扩展变量”阶段对其进行处理。实际上,已经完成了N ^ 2个成功的冲突检测,并且完成了1,5 * N ^ 2位操作以及一些嵌套循环逻辑。

为什么堆栈变量的数量如此之大?因为代码中的每个 double 常量都保存到堆栈中的其他插槽中。然后按照调用约定的说明(通过x86的堆栈顶部;通过x86_64的寄存器)将其从其插槽加载并传递。有趣的事实:所有使用push_back-fstack-protector编译的-fno-stack-protector的代码都是相同的;常量的堆栈布局也相同。仅会影响non-push_back代码的某些堆栈布局偏移量(已使用-Sdiff -u检查了两次运行)。启用的堆栈保护程序未创建其他代码。

启用堆栈保护程序会致命地更改编译器内部的某些行为。无法确切说出位置(请注意:通过比较Juan M. Bello Rivas的callgraph.tar.gz与堆栈轨迹可以找到此转折点)。

第一个大N *(N + 1)/2 = O(N ^ 2)步是在expand_used_vars_for_block (tree block, level)函数中用于设置有关堆栈变量对之间冲突的信息:
  /* Since we do not track exact variable lifetimes (which is not even
     possible for variables whose address escapes), we mirror the block
     tree in the interference graph.  Here we cause all variables at this
     level, and all sublevels, to conflict.  */
  if (old_sv_num < this_sv_num)
    {
      new_sv_num = stack_vars_num;

      for (i = old_sv_num; i < new_sv_num; ++i)
        for (j = i < this_sv_num ? i : this_sv_num; j-- > old_sv_num ;)
          add_stack_var_conflict (i, j);
    }
}
add_stack_var_conflict(i,j)变成
  • 分配(每个变量一次)大小为...哦的位图,其动态大小将增长到N位
  • 在i'th var的位掩码中设置一些位以与j
  • 冲突
  • 设置第j个var的位掩码中的某个位以与i
  • 冲突

    add_alias_set_conflicts中还有第二个N ^ 2步。它使用objects_must_conflict_p对每个对进行类型检查。它检查两个变量是否具有相同的类型(大多数对是;这是基于类型的别名分析TBAA)。如果不是,则调用add_stack_var_conflict;该N ^ 2循环嵌套中只有N个此类调用。

    最后一个大步是在partition_stack_vars()函数中,堆栈变量(O(NlogN))的qsorting和N *(N-1)/2 = O(N ^ 2)个步可以找到所有非冲突对。这是cfgexpand.c文件中partition_stack_vars的伪代码:
            Sort the objects by size.
            For each object A {
              S = size(A)
              O = 0
              loop {
                Look for the largest non-conflicting object B with size <= S.
                       /* There is a call to stack_var_conflict_p to check for 
                        * conflict between 2 vars */
                UNION (A, B)
                offset(B) = O
                O += size(B)
                S -= size(B)
              }
            }
    

    函数stack_var_conflict_p只是检查第i个变量中是否存在冲突位掩码,以及是否将第j个位设置为具有第j个变量的冲突标志(调用bitmap_bit_p(i->conflict_mask,j))。真正的坏消息是,callgrind说每个冲突检查都成功,并且每对都跳过UNION逻辑。

    因此,O(N ^ 2)位设置和O(N ^ 2/2)位检查浪费了很多时间。所有这些工作无助于优化任何东西。是的,所有这些都是-O0的一部分,并由-fstack-protector触发。

    UPDATE2:

    似乎,转折点是expand_one_var cfgexpand.c from 4.6,用于检查在堆栈上立即或延迟分配变量:
    1110      else if (defer_stack_allocation (var, toplevel))
    1111        add_stack_var (origvar);
    1112      else
    1113        {
    1114          if (really_expand)
    1115            expand_one_stack_var (origvar);
    1116          return tree_low_cst (DECL_SIZE_UNIT (var), 1);
    1117        }
    

    (根据callgrind的说法,expand_one_stack_var仅在快速变体中才被调用)

    启用-fstack-protect时,将强制执行延迟分配(有时需要重新排序所有堆栈变量)。甚至还有一些关于“二次问题”的评论,现在我们似乎太熟悉了:
    969 /* A subroutine of expand_one_var.  VAR is a variable that will be
    970    allocated to the local stack frame.  Return true if we wish to
    971    add VAR to STACK_VARS so that it will be coalesced with other
    972    variables.  Return false to allocate VAR immediately.
    973 
    974    This function is used to reduce the number of variables considered
    975    for coalescing, which reduces the size of the quadratic problem.  */
    976 
    977 static bool
    978 defer_stack_allocation (tree var, bool toplevel)
    979 {
    980   /* If stack protection is enabled, *all* stack variables must be deferred,
    981      so that we can re-order the strings to the top of the frame.  */
    982   if (flag_stack_protect)
    983     return true;
    

    (堆栈分配也推迟到-O2及更高版本)

    这是一个提交:http://gcc.gnu.org/ml/gcc-patches/2005-05/txt00029.txt添加了此逻辑。

    关于c++ - 为什么编译超过100,000行的std::vector::push_back需要很长时间?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13898985/

    相关文章:

    compilation - 错误 127 编译 'Makefile'

    c++ - 我可以使用 printf() 格式的变量吗

    c++ - 使用 srand 生成随机数

    bash - 如何在 docker 容器中生成核心文件?

    c - scanf GCC for long double

    c++ - 通过阅读 gcc 输出来学习汇编

    c++ - 使用未声明的标识符与模板和继承后续

    deployment - 使用 asdf 我可以加载仅提供以前制作的 FASL 的系统吗

    c++ - UNIX/OSX 版本的 semtimedop

    C++ 不发出蜂鸣声