使用记忆功能观察到的奇怪性能

Strange performance observed with memoized function

本文关键字:性能 观察 记忆 功能      更新时间:2023-10-16

我在玩一个用欧几里得算法计算两个数字的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=10000时,计算在这台计算机上运行8秒,而对于记忆版本,它接近一分钟,其他一切都是相等的。

我错过了什么显而易见的东西吗?我使用key作为权宜之计,这样我就不需要为哈希图专门化std::hash。我唯一能想到的可能是,记忆版本没有获得TCO,而gcd()获得了TCO,或者通过std::function调用函子的速度很慢(尽管我同时使用它),或者我可能是迟钝了。大师,给我带路。

票据

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

我还尝试了一个带有std::map<std::pair<size_t, size_t>, size_t>的版本,它的性能与未移动版本相当。

GCD版本的主要问题是,根据使用模式的不同,它可能会消耗大量内存。

例如,如果计算所有对0<=的GCD(a,b)a<10000,0<=b<10000,则存储表最终将包含100000000个条目。由于在x86上,每个条目是12个字节,因此哈希表将占用至少1.2GB的内存。使用这么多内存会很慢。

当然,如果你用>=10000的值来评估GCD,你可以让这个表任意大。。。至少在地址空间或提交限制用完之前。

摘要:一般来说,存储GCD是个坏主意,因为它会导致无限制的内存使用。

有一些细节可以讨论:

  • 随着表超过各种大小,它将存储在越来越慢的内存中:首先是L1缓存,然后是L2缓存、L3缓存(如果存在)、物理内存、磁盘。显然,随着表的增长,存储的成本会急剧增加
  • 如果您知道所有输入都在一个小范围内(例如,0<=x<100),那么使用内存化或预计算表仍然是一种优化。很难确定——你必须根据自己的具体情况进行衡量
  • 还有其他可能优化GCD的方法。例如,我不确定g++在这个例子中是否自动识别尾部递归。如果没有,可以通过将递归重写为循环来提高性能

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

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

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

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