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

2024-04-15

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

为什么这需要这么长时间? (除std::vectorPush_back 语句的编译时间不到 1 秒,因为其他代码很少。)


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

我使用 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();
}

-ftime-report各种 N (wall由于PC上的后台负载,时间不准确,所以请查看user time, usr):

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

因此,对于“中的巨大 N”有一些额外的工作展开变量”阶段。这个阶段正是这一行:cfgexpand.c:4463 http://code.metager.de/source/xref/DragonFly-BSD/contrib/gcc-4.7/gcc/cfgexpand.c#4463(TV_VAR_EXPAND 宏之间)。

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

我的 g++ 和 ubuntu g++ 有什么区别?其一是默认开启 https://wiki.ubuntu.com/ToolChain/CompilerFlagsGcc 堆栈保护 (-fstack-protect选项)在 Ubuntu 中。这种保护只是添加到“扩展变量”阶段(在源代码中找到)cfgexpand.c:1644,expand_used_vars() http://code.metager.de/source/xref/DragonFly-BSD/contrib/gcc-4.7/gcc/cfgexpand.c#1644;提及here http://en.wikibooks.org/wiki/GNU_C_Compiler_Internals/Stackguard_4_1):

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

运行时间从 800 秒缩短为 24 秒。

UPDATE:

里面启动gcc之后callgrind(来自 Valgrind 的调用图分析工具),我可以说有 N 个堆栈变量。如果启用了堆栈保护器,它们将在“扩展变量”阶段使用三个 O(N^2) 算法进行处理。实际上有 N^2 成功的冲突检测和 1,5 * N^2 位操作完成加上一些嵌套循环逻辑。

为什么堆栈变量的数量如此之多?因为代码中的每个双常量都保存到堆栈中的不同槽中。然后它从其槽中加载并按照调用约定进行传递(通过 x86 中的堆栈顶部;通过 x86_64 中的寄存器)。有趣的事实:所有代码都带有push_back编译为-fstack-protector或与-fno-stack-protector是一样的;常量的堆栈布局也相同。只有非push_back代码的一些堆栈布局偏移量受到影响(检查了两次运行-S and diff -u)。启用的堆栈保护器没有创建任何额外的代码。

启用堆栈保护器会致命地改变编译器内部的某些行为。不能确切地说在哪里(注意:通过比较堆栈跟踪可以找到这个转折点调用图.tar.gz https://stackoverflow.com/a/1980770/196561作者:胡安·M·贝洛·里瓦斯)。

第一个大 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);
    }
}

The add_stack_var_conflict(i,j)转向

  • 分配(每个变量一次)大小为...哦,动态大小将增长到 N 位的位图
  • 在第 i 个变量的位掩码中设置一个位以防止与 j 冲突
  • 在第 j 个变量的位掩码中设置一个位以防止与 i 冲突

有第二个 N^2 走进add_alias_set_conflicts。它对每一对进行类型检查objects_must_conflict_p。它检查两个变量是否属于同一类型(大多数对都是;这是基于类型的别名分析,TBAA http://en.wikipedia.org/wiki/Alias_analysis#Type-based_alias_analysis)。如果不,add_stack_var_conflict叫做;这个 N^2 循环嵌套中只有 N 个这样的调用。

最后一次大型步行是在partition_stack_vars()功能与qsort堆栈变量( O(NlogN) )和 N*(N-1)/2 = O(N^2) 步行查找所有非冲突对。这是伪代码partition_stack_vars来自 cfgexpand.c 文件:

        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 ,在检查堆栈上变量的立即分配或延迟分配时:

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 http://gcc.gnu.org/ml/gcc-patches/2005-05/txt00029.txt添加了这个逻辑。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么编译超过 100,000 行的 std::vector::push_back 需要很长时间? 的相关文章

  • 将指针转换为浮点数?

    我有一个unsigned char 通常 这指向一块数据 但在某些情况下 指针就是数据 即 铸造一个int的价值unsigned char 指针 unsigned char intData unsigned char myInteger 反
  • 隐式方法组转换陷阱

    我想知道为什么给定代码的输出 在 LinqPad 中执行 void Main Compare1 Action Main Dump Compare2 Main Dump bool Compare1 Delegate x return x Ac
  • 泛型与接口的实际优势

    在这种情况下 使用泛型与接口的实际优势是什么 void MyMethod IFoo f void MyMethod
  • C++ 非类型参数包扩展

    我正在编写由单一类型参数化的模板函数 并且具有可变数量的相同类型 而不是不同类型 的参数 它应该检查第一个值是否在其余值中 我想这样写 include
  • 将列表(对象)转换为列表(字符串)

    有没有办法转换List of Object to a List of String 在 c 或 vb net 中而不迭代所有项目 幕后迭代很好 我只想要简洁的代码 Update 最好的方法可能就是进行新的选择 myList Select f
  • 返回指向 std::vector 中的对象的 a

    我有一个关于返回对向量元素的引用的非常基本的问题 有一个向量vec存储类的实例Foo 我想访问这个向量中的一个元素 不想使用向量索引 我应该如何编码该方法getFoo here include
  • 使用成员作为实现者来实现接口

    我有实现 IA 的 A 类 现在我需要创建也应该实现 IA 的类 B B 类有 A 类的实例作为成员 有什么方法可以定义A的实例实现B类中的IA吗 interfase IA void method1 void method2 void me
  • 控制器中的异常处理 (ASP.NET MVC)

    当您自己的代码抛出异常并从控制器中的操作调用时 应该如何处理 我看到很多最佳实践的例子 其中根本没有 try catch 语句 例如 从存储库访问数据 public ViewResult Index IList
  • 替换 JSON 中的转义字符

    我想用空格替换 JSON 字符串中的 字符 我怎样才能做到这一点 我发现从 JSON 字符串中删除所有转义字符的最简单 最好的方法是将字符串传递到正则表达式 Unescape 方法 此方法返回一个没有转义字符的新字符串 甚至删除了 n t
  • 套接字:监听积压并接受

    listen sock backlog 在我看来 参数backlog限制连接数量 这是我的测试代码 server initialize the sockaddr of server server sin family AF INET ser
  • 在不使用 Thread.Sleep c# 的情况下延迟发送电子邮件

    我有一个 for 循环 它循环并每个循环发送一封电子邮件 现在我正在使用 thread sleep 但我希望用户仍然能够与程序交互 只需取消该循环即可 是否可以在不使用 thread sleep 的情况下做到这一点 您是否在 UI 线程上运
  • 为什么 std::ranges::filter_view 对象必须是非常量才能查询其元素?

    include
  • 如何解释“错误C2018:未知字符'0x40'?[关闭]

    Closed 这个问题需要调试细节 help minimal reproducible example 目前不接受答案 在编译一些代码时 我收到以下信息 错误 C2018 未知字符 0x40 我想知道如何解决这样的问题 这是我要开始的地方
  • 我应该使用多个 HttpClient 来进行批量异步 GET 请求吗?

    我有一个场景 我需要在尽可能短的时间内发出大量 GET 请求 想想大约 1000 个 我知道通常最好保留一个客户端并尽可能重用它 Create Single HTTP Client HttpClient client new HttpCli
  • C 中的 N 依赖注入 - 比链接器定义的数组更好的方法?

    Given a 库模块 在下文中称为Runner 它作为可重复使用的组件 无需重新编译 即静态链接库 中应用程序分区架构的 而不是主分区 请注意 它仅包含main 出于演示目的 Given a set 顺序无关 调用的其他模块 对象Call
  • 需要使用 openssl 加密和解密文件的示例 C 代码

    我正在用 Linux C 编写代码 我需要使用以下命令来加密和解密文件 openssl 目前 我使用系统命令 des3 e nosalt k 0123456789012345 in inp file out out file 进行加密 使用
  • double 类型的静态类成员的常量表达式初始值设定项

    在 C 11 和 C 14 中 为什么我需要constexpr在下面的代码片段中 class Foo static constexpr double X 0 75 而这会产生编译器错误 class Foo static const doub
  • 实体框架代理创建

    我们可以通过使用来停止在上下文构造函数中创建代理 this Configuration ProxyCreationEnabled false 在 EF 4 1 中创建代理有哪些优点和缺点 代理对于两个功能是必需的 延迟加载 导航属性在第一次
  • C/C++ 通过 Android NDK 在 JNI 中看不到 Java 方法

    我正在尝试从使用 NDK 构建的 C 类文件调用 Java 方法 它不断抛出常见的 未找到非静态方法 错误并导致整个 Android 应用程序崩溃 下面的代码片段 有些东西可能不需要 但我按原样保留它们 因为焦点 问题在于refreshJN
  • FakeItEasy 代理方法调用实际实现

    我正在尝试将对假对象的调用代理到实际的实现 这样做的原因是我希望能够使用 Machine Specifications 的 WasToldTo 和 WhenToldTo 它们仅适用于接口类型的伪造 因此 我正在执行以下操作来代理对我的真实对

随机推荐