【问题标题】:Explaining performance difference in two nearly identical algorithms解释两种几乎相同算法的性能差异
【发布时间】:2014-04-16 18:29:14
【问题描述】:

这个问题比较模糊,我真的需要回答它,但我很好奇答案可能是什么,所以我还是会问它。

我有一个生成大量矩阵的算法。它稍后会在其上运行第二个算法,该算法会生成一个解决方案。我运行了 100 次,平均耗时约 17 秒。

第二种算法几乎完全相同,唯一的区别是,第二种算法在每个矩阵生成后立即运行,因此它们实际上不需要存储在任何地方。这个变体显然需要更少的空间,这就是我制作它的原因,但对于同样的问题,它也只需要平均约 2 秒。

我没想到它会跑得更快,尤其是没有那么快。

代码相当大,所以我会尝试用类似伪代码的东西来概述区别:

recursiveFill(vector<Matrix> &cache, Matrix permutation) {
  while(!stopCondition) {
    // generate next matrix from current permutation
    if(success)
      cache.push_back(permutation);
    else
      recursiveFill(cache, permutation);
    // some more code
  }
}

recursiveCheck(Matrix permutation) {
  while(!stopCondition) {
    // alter the matrix some
    if(success)
      checkAlgorithm(permutation);
    else
      recursiveCheck(permutation);
    // some more code
  }
}

在递归填充之后,循环对缓存中的所有元素运行 checkAlgorithm。我没有包含在代码中的所有内容在两种算法中都是相同的。我猜想向量中的存储一直在消耗,但如果我没记错的话,每次过度填充时,c++ 向量的大小都会翻倍,因此重新分配不应该经常发生。 有什么想法吗?

【问题讨论】:

  • 是的,不知道你在问什么。然而,std::vector 的开销不仅仅是在数组被重新调整大小并因此被复制时(尽管这会影响它),而且你的元素必须是可复制的,甚至可以使用 std::vector,所以除非你'正在存储指针或引用,您会将完整的深层副本推入向量中。
  • cachegrindvalgrind的缓存检查工具,你通过在命令行中添加--tool=cachegrind来激活它)对你的两个代码说什么?
  • 这真的取决于实现。我猜你不仅会产生缓存未命中,还会产生页表未命中,其中操作系统被颠簸以不断地来回转换虚拟地址,因为第一个中的递归调用是不同的,顺序比
  • 什么是Matrix?它使用堆分配吗?它是可移动的(并且您的编译器是否支持移动语义)?交换是否正确重载并有效实施?

标签: c++ performance vector benchmarking


【解决方案1】:

我猜额外的时间是由于vector 中的矩阵复制所致。根据您给出的时间,遍历数据需要 20 或 170 毫秒,这对于大量复制来说是正确的数量级。

请记住,即使由于向量的重新分配而导致的复制开销是线性的,但每个插入的矩阵平均被复制两次,一次是在插入期间,一次是在重新分配期间。结合复制大量数据的缓存破坏效应,这会产生额外的运行时间。

现在您可能会说:但是当我将它们传递给递归调用时,我也在复制矩阵,我不应该期望第一个算法最多花费第二个算法的三倍吗?
答案是,任何递归体面的完美缓存友好,如果它不受堆上数据的缓存利用率的阻碍。因此,几乎所有在递归体面中完成的复制甚至都没有到达 L2 缓存。如果您不时通过执行vector 重新分配来破坏整个缓存,则之后您将使用完全冷的缓存恢复。

【讨论】:

  • 这听起来很有趣。那么用递归填充容器可能比用简单循环填充容器要慢得多?另外,我解释了一些错误,17 秒和 2 秒不是一个函数调用的时间,而是完整的问题,即检查分支因子 5 和深度 10 的树,所以生成的总量(和一个案例缓存)矩阵是 19.531.249 每次运行算法。
  • 感谢您提供的数字,这正是我认为的数量级:-) 而且,是的,我正确阅读了您的 17 秒,这就是为什么我将这个时间除以 100 得到到 170 毫秒。
  • 啊,那我理解错了。感谢您的宝贵时间!
【解决方案2】:

这里的罪魁祸首可能是时间局部性。您的 CPU 缓存只有这么大,因此当您在每次运行后保存所有内容并稍后返回时,它同时离开了您的 CPU 缓存并且需要更长的时间(10 到 100 秒的周期)才能访问。使用第二种方法,它就在 L1(或者可能是 MMX 寄存器)中,并且只需要一两个周期即可访问。

在优化中,你通常想像 Wu-Tang Clan: Cache Rules Everything around Me。

Some people have done testing on this,缓存中的副本通常比取消引用到主内存便宜很多

【讨论】:

  • 我认为现在下结论还为时过早。不是你错了,但 15 秒的差异是相当大的,我们对他的缓存大小一无所知,我们对他的复制语义一无所知,等等。
  • 缓存中的副本可能比取消引用到主内存中更便宜,但 您通过回答好像副本无关紧要来得出错误的二分法。我们不知道他复制到哪里或从哪里复制;我们还没有看到他的实现或他的数据集。这个答案可能确实是最终解决方案的一部分,但就目前而言,这个答案类似于医生建议患者因打喷嚏而患流感。
【解决方案3】:

严格来说,vector 不必每次增长翻倍,它只需要几何增长以提供所需的摊销常数时间。

在这种情况下,如果您有足够多的矩阵,增长和所需的数据副本仍然可能是问题所在。或者它可能正在交换以分配足够的内存。唯一确定的方法是profile在您遇到这种差异的系统上。

【讨论】:

    猜你喜欢
    • 2021-03-07
    • 1970-01-01
    • 2023-03-09
    • 1970-01-01
    • 1970-01-01
    • 2017-10-03
    • 1970-01-01
    • 2010-10-23
    • 1970-01-01
    相关资源
    最近更新 更多