【问题标题】:Strange performance observed with memoized function使用记忆函数观察到的奇怪性能
【发布时间】:2012-07-13 01:15:02
【问题描述】:

我正在玩弄一些使用欧几里得算法来计算两个数字的 GCD 的东西。我像往常一样实现了标准的单线,它工作得很好。它用于计算序列并在n 变大时每个元素调用gcd() 多次的算法。我决定看看我是否可以通过记忆做得更好,所以我尝试了以下方法:

size_t const gcd(size_t const a, size_t const b) {
  return b == 0 ? a : gcd(b, a % b);
}

struct memoized_gcd : private std::unordered_map<unsigned long long, size_t> {
  size_t const operator()(size_t const a, size_t const b) {
    unsigned long long const key = (static_cast<unsigned long long>(a) << 32) | b;
    if (find(key) == end()) (*this)[key] = b == 0 ? a : (*this)(b, a % b);
    return (*this)[key];
  }
};

//std::function<size_t (size_t, size_t)> gcd_impl = gcd<size_t,size_t>;
std::function<size_t (size_t, size_t)> gcd_impl = memoized_gcd();

稍后我通过std::function 实例调用所选函数。有趣的是,例如当 n = 10,000 时,在这台计算机上的计算运行时间为 8 秒,而使用记忆的版本接近一分钟,其他一切都相同。

我是否遗漏了一些明显的东西?我将key 用作权宜之计,这样我就不需要专门针对哈希映射使用std::hash。我唯一能想到的可能是 memoized 版本没有获得 TCO 而gcd() 获得,或者通过std::function 调用对于仿函数来说很慢(即使我同时使用它),或者也许我迟钝了。大师们,给我指路。

注意事项

我已经在带有 g++ 4.7.0 的 win32 和 win64 以及带有 g++ 4.6.1 和 4.7.1 的 linux x86 上尝试过这个。

我还尝试了一个带有std::map&lt;std::pair&lt;size_t, size_t&gt;, size_t&gt; 的版本,其性能与未记忆的版本相当。

【问题讨论】:

  • 你可能想好好看看记忆版本的内存使用情况。一旦它滑出缓存,你的基于哈希的随机访问就会受到伤害 - 非常糟糕。
  • Euclid 的算法在 a 和 b 的不同值的复杂度上反弹了很多...我感觉 gcd 的最坏情况输入通常不在缓存中,但我认为其他输入将从记忆中获得一些好处。我想我是唯一一个对我观察到的时间差异感到惊讶的人。
  • static_cast&lt;unsigned long long&gt;(a) &lt;&lt; 32 基本上是无意义的——在 x64 上,size_t 通常已经是unsigned long long 的类型定义,所以你只是在丢弃位。此外,显示您的实际基准测试的完整代码(包括n 的最高值);这和这个一样可能是错误的。
  • 好点,在我最初看到这种行为的 x86 上很快就完成了,所以程序的其余部分在 x64 上给出了正确的结果实际上是非常令人惊讶的。没有严格的基准,只是我认为是奇怪的行为。就像我在问题中所说的那样,我主要使用 n = 10000 进行了测试......并且在 x86 上,该语句 not 荒谬,运行时间的差异更大,尽管肯定有其他未考虑的因素。
  • ...呃,我的意思是,不,这一点都不荒谬,我可以在这个例子中移出这些位,因为我正在处理相对较小的 a 和 b 值,这严格

标签: c++ performance optimization c++11


【解决方案1】:

您的 GCD 版本的主要问题是它可能会占用大量内存,具体取决于使用模式。

例如,如果您计算所有对 0

当然,如果您使用 >=10,000 的值评估 GCD,您可以使表任意大...至少在您用完地址空间或提交限制之前。

总结:一般来说,记忆 GCD 是个坏主意,因为它会导致无限的内存使用。

有一些细节可以讨论:

  • 由于表超过各种大小,它将存储在越来越慢的内存中:首先是 L1 缓存,然后是 L2 缓存,L3 缓存(如果存在),物理内存,磁盘。显然,随着表格的增长,记忆的成本会急剧增加。
  • 如果您知道所有输入都在一个小范围内(例如,0
  • 可能还有其他优化 GCD 的方法。例如,我不确定 g++ 在这个例子中是否自动识别尾递归。如果没有,您可以通过将递归重写为循环来提高性能。

但正如我所说,您发布的算法表现不佳一点也不奇怪。

【讨论】:

  • 我没有提到,但这是基于使用 -O3 构建的。我假设递归在简单版本中得到尾调用优化。我的要求仅适用于较小的 n 值,但我想看看是否可以更有效地计算更大的值。有趣的是,当我将策略更改为仅在 a 和 b 小于 256 时缓存并使用 unsigned char 向量时,n = {5000, 10000, 20000} 的性能无法区分。看起来小 a 和 b 的内存访问成本几乎平衡了计算成本。
【解决方案2】:

这并不奇怪。在现代 CPU 上,内存访问非常慢,尤其是当它不在缓存中时。重新计算一个值通常比将其存储在内存中要快。

【讨论】:

    【解决方案3】:

    频繁的堆分配(创建新条目时)。还有 std::unordered_map 查找开销(虽然它可能是常数时间,但肯定比普通数组偏移慢)。缓存也未命中(访问模式和大小的函数)。

    如果您想进行“纯”比较,可以尝试将其转换为使用静态的、堆栈分配的普通数组;这可能是一个使用更多内存的稀疏查找表,但它更能代表记忆化如果您可以将整个记忆化数组放入您的 CPU 缓存中。

    【讨论】:

      猜你喜欢
      • 2013-01-05
      • 1970-01-01
      • 1970-01-01
      • 2018-05-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-02-14
      相关资源
      最近更新 更多