【问题标题】:std::pair<int, int> vs struct with two int'sstd::pair<int, int> 与带有两个 int 的结构
【发布时间】:2010-12-09 01:52:47
【问题描述】:

在 ACM 示例中,我必须为动态编程构建一个大表。我必须在每个单元格中存储两个整数,所以我决定使用std::pair&lt;int, int&gt;。然而,分配一个巨大的数组需要 1.5 秒:

std::pair<int, int> table[1001][1001];

后来,我把这段代码改成了

struct Cell {
    int first;
    int second;
}

Cell table[1001][1001];

分配用时 0 秒。

是什么解释了这种巨大的时间差异?

【问题讨论】:

  • 我相信你的意思是 ACM ICPC
  • 您是否在启用优化的情况下对此进行了测试?
  • 如果给Cell添加一个无参构造函数会有什么表现?
  • 在法官服务器上启用的优化。 (认为​​我听说过 O3)
  • @Etan 通过 clang (900.0.39.2),-O0 需要 0.007 秒,-O3 需要 2e-07 秒。通过 gcc (8.1),-O0 需要 0.005 秒,-O3 需要 0 秒。这个问题会随着时间的推移而失效吗?

标签: c++ performance std-pair


【解决方案1】:

std::pair&lt;int, int&gt;::pair() 构造函数使用默认值初始化字段(在 int 的情况下为零),而您的 struct Cell 没有(因为您只有一个自动生成的默认构造函数,它什么都不做)。

初始化需要写入每个字段,这需要大量相对耗时的内存访问。使用struct Cell,什么都不做,什么都不做会快一点。

【讨论】:

  • 将大约 8 MB 设置为零需要 1.5 秒吗?
  • 不,问题是对构造函数的函数调用。如果您的数组是全局的,则无论如何它都会初始化为零。可能编译器没有优化构造函数调用。
  • 你需要调用构造函数,找到内存,设置它等等......这不仅仅是通过memset将N位连续内存设置为0。
  • @Lemurik:我不相信。所有pair 构造函数所做的就是委托给它的(POD)成员的构造函数。编译器可以(恕我直言,应该)识别这一点,省略构造函数调用并简单地将内存归零。做到这一点的分析肯定不会很难吗?
  • 我们是否 100% 确定使用 std::pair 是造成性能的原因?当它使代码更具可读性时,我倾向于使用pair,但这让我重新考虑......
【解决方案2】:

到目前为止的答案并不能解释问题的全部程度。

正如sharptooth 所指出的,对解决方案将值初始化为零。正如 Lemurik 指出的那样,pair 解决方案不仅仅是初始化一个连续的内存块,而是为表中的每个元素调用 pair 构造函数。但是,即使这样也不能说明它需要 1.5 秒。其他事情正在发生。

这是我的逻辑:

假设您在一台古老的机器上运行,假设以 1.33ghz 运行,那么 1.5 秒就是 2e9 个时钟周期。你有 2e6 对要构造,所以不知何故,每个对构造函数都需要 1000 个周期。 调用仅将两个整数设置为零的构造函数不需要 1000 个周期。我看不出缓存未命中如何使其花费这么长时间。如果这个数字少于 100 个周期,我会相信。

我认为看看所有这些 CPU 周期的其他用途会很有趣。我使用了我能找到的最糟糕的最古老的 C++ 编译器,看看我是否能达到所需的浪费水平。该编译器是 VC++ v6。在调试模式下,它会做一些我不明白的事情。它有一个大循环,为表中的每个项目调用 pair 构造函数——这很公平。该构造函数将这两个值设置为零 - 足够公平。但在此之前,它将 68 字节区域中的所有字节设置为 0xcc。该区域就在大桌子开始之前。然后它用 0x28F61200 覆盖该区域的最后一个元素。对构造函数的每次调用都会重复此操作。大概这是编译器的某种簿记,因此它知道在运行时检查指针错误时初始化了哪些区域。我很想知道这到底是干什么用的。

无论如何,这可以解释额外时间的去向。显然另一个编译器可能没有这么糟糕。当然,优化的发布版本也不会。

【讨论】:

  • 这不是 VC++ V6 的错。在调试模式下,所有 VC 编译器都会将堆栈上分配的所有字节初始化为陷阱值(默认为 0xCC),并且所有分配的堆内存将类似地初始化为 0xCD。目的有两个:导致任何在(假设为零)未初始化变量上运行的代码大声失败,并让您在内存调试器中看到未初始化的堆栈。您看到写入的最后一个值是用于检测堆栈溢出的“堆栈金丝雀值”(.com giggle)s.
【解决方案3】:

这些都是很好的猜测,但众所周知,猜测并不可靠。

我会在 1.5 秒内随机说 pause it,但你必须非常快。如果您将每个维度增加约 3 倍,您可能会花费 10 多秒,因此更容易暂停。

或者,您可以在调试器下获取它,在 pair 构造函数代码中对其进行分解,然后单步查看它在做什么。

无论哪种方式,您都会得到问题的确切答案,而不仅仅是猜测。

【讨论】:

    【解决方案4】:

    我猜这是创建 std::pair 的方式。与仅分配内存范围相比,在 1001x1001 次调用 pair 构造函数时开销更大。

    【讨论】:

      【解决方案5】:

      这确实是一个很好的例子,说明应该编写 C++ 并谨慎使用 STL。有什么想法吗?

      我的项目正在开发一个 C&C++ 代码级基准测试工具,我们将在其中制作大量代码示例,以找出什么是“好”代码,什么是“坏”编码习惯。请参阅http://effodevel.googlecode.com 以了解有关 C9B.M 的更多信息。规划。如果您遇到过很多这样的案例,请加入该项目以帮助我们。

      【讨论】:

      • 这不是问题的答案。
      • 有什么想法吗?是的。您的项目似乎认同优化就是低级优化(和大O)的一般理念。我认为问题远不止于此,即泛滥的普遍性,并且您可能会考虑更广泛的范围。 stackoverflow.com/questions/926266/…
      • 目前还没有做更大的计划。我会一步一步地进步,比如代码级别、算法级别和架构级别等等。我们现在正在理解语言和编译器。感谢您的 cmets
      • 让我建议,当你想要更雄心勃勃时,你放弃任何先前关于更高层次可能是什么的想法。相反,在您的组织中四处搜寻存在性能问题的真实(大型)代码,并从中学习。这就是我发现担心低级优化就像在路灯下寻找钥匙一样,因为那是灯所在的地方。
      • 我的软件没有性能问题 (80K LOC);所以你的问题不是我的意思。我们正在做的是一个帮助开发人员了解并理解什么是“好”代码和什么是“坏”编码习惯的工具,比如编码指南。就是这样。
      猜你喜欢
      • 1970-01-01
      • 2014-09-01
      • 1970-01-01
      • 2020-10-22
      • 1970-01-01
      • 2011-10-29
      • 1970-01-01
      • 2014-03-03
      • 1970-01-01
      相关资源
      最近更新 更多