【问题标题】:Cache locality when iterating through two container at the same time同时迭代两个容器时缓存局部性
【发布时间】:2018-10-31 18:45:29
【问题描述】:

我知道,当对容器的每个元素进行一些计算时,如果内存是连续的,则可以获得最佳性能。 但是,如果必须同时处理两个或多个大容器(这样它们就不能完全适应缓存)怎么办?

int main()
{
    const std::size_t BIG_SIZE = 2000000;  // A number such that both ivec and fvec won't fit the cache

    std::vector<int> ivec(BIG_SIZE, 0);
    int start = 0;
    for (auto& i : ivec)
        i = start++;


    std::vector<float> fvec(BIG_SIZE, 0.f);


    auto iit = ivec.cbegin();
    auto fit = fvec.begin();
    for (; iit != ivec.cend() && fit != fvec.end(); ++iit, ++fit) 
        *fit = *iit * 3.14;  // What happens here?
}

在最后一个循环中,缓存会同时加载*iit 附近的内存块和*fit 附近的内存块,还是每次访问*iit*fit 时都会错过缓存?

如果是后者,我是否应该以交错模式自定义分配 ivecfvec 以防止这些失误?

【问题讨论】:

  • 有几个缓存行。
  • 在某个级别上,这一切都非常简单——每次你从内存中读取时,CPU 预取(和后取)比你实际读取的多——整个缓存行都被获取了。但是 CPU 中存在不止一个缓存行,因此从不同内存地址读取应该填充不同的缓存行。我不希望您受到这种模式的惩罚,但使用专门的工具进行验证总是值得的。
  • 很大程度上取决于您使用的 什么 CPU 以及它的内存/缓存层次结构是什么样的(有些根本没有缓存,也存在 NUMA 架构)。不同的 CPU 也有不同的预取机制和不同的缓存驱逐策略。没有“一个真正的答案”,这完全取决于所讨论的硬件。
  • 哪个 CPU?它是非常特定于 CPU 的。
  • 一个典型的高速缓存行是 64 字节,这将提供一个 32KB 的一级高速缓存 512 个高速缓存行

标签: c++ memory-management cpu-cache


【解决方案1】:

查看更快的最简单方法是进行基准测试。答案将取决于:硬件、输入大小和其他东西(编译器、标志等)。但是,出于本示例的目的,我将使用带有 clang-6.0、C+ 的网站 quick-bench.com +17、-O3 和 libstdc++。这是比较的代码:

static void One(benchmark::State& state) {
  for (auto _ : state) {
    const std::size_t BIG_SIZE = 20000000;

    std::vector<int> ivec(BIG_SIZE, 0);
    benchmark::DoNotOptimize(ivec);
    int start = 0;
    for (auto& i : ivec)
        i = start++;

    std::vector<float> fvec(BIG_SIZE, 0.f);
    benchmark::DoNotOptimize(fvec);

    auto iit = ivec.cbegin();
    auto fit = fvec.begin();
    for (; iit != ivec.cend() && fit != fvec.end(); ++iit, ++fit) 
        *fit = *iit * 3.14;
  }
}
BENCHMARK(One);

static void Two(benchmark::State& state) {
  for (auto _ : state) {
    const std::size_t BIG_SIZE = 20000000;

    std::vector<int> ivec(BIG_SIZE, 0);
    std::vector<float> fvec(BIG_SIZE, 0.f);
    benchmark::DoNotOptimize(ivec);
    benchmark::DoNotOptimize(fvec);
    int start = 0;
    auto fit = fvec.begin();
    for (auto& i : ivec) {
        i = start++;
        *fit = i * 3.14;
        ++fit;
    }
  }
}
BENCHMARK(Two);

第一个函数是您的原始代码,而第二个函数是修改后的版本。 benchmark::DoNotOptimize 只是防止两个向量被优化掉。 N 为 2000 的结果:

N 为 20000000 的结果:

如您所见,对于较大的 N,第二个示例会受到影响。您需要仔细编写代码并进行基准测试,而不是做出假设(Google 基准测试是 quick-bench.com 的基础技术)


您实际上可以通过使用标准库函数来提高性能。据推测,这是因为他们已经针对不同的场景进行了优化,并且委托给了比您可以手动优化的更好的代码。这是一个例子:

static void Three(benchmark::State& state) {
  for (auto _ : state) {
    const std::size_t BIG_SIZE = 20000000;

    std::vector<int> ivec(BIG_SIZE, 0);
    std::vector<float> fvec(BIG_SIZE, 0.f);
    benchmark::DoNotOptimize(ivec);
    benchmark::DoNotOptimize(fvec);
    int start = 0;
    auto fit = fvec.begin();
    std::iota(ivec.begin(), ivec.end(), 0);
    std::transform(ivec.begin(), ivec.end(), 
      fvec.begin(),
      [] (const auto a) {
        return a * 3.14;
      });
  }
}
BENCHMARK(Three);

我们已将您的手动循环替换为 std::iotastd::transform。大N的结果:

如您所见,版本 3 比 #1 和 #2 更快(尽管速度稍快)。所以首先使用标准库函数,如果太慢,只能手动滚动它。

【讨论】:

  • 建议:将const std::size_t BIG_SIZE = 20000000; 移到两个基准测试都可以使用的地方。如果您只更改一个然后试图弄清楚为什么基准与预期完全不符,那真的很糟糕。嗯。重读时,我对您使用的基准测试工具不够熟悉,无法知道这是否可行。
  • 非常感谢您提供详尽的回答,我学到了有关如何提高性能的非常有用的知识。但我不确定我是否可以将其标记为问题的答案,因为我上面发布的代码只是一个示例,可以更好地了解缓存的工作原理。真正的答案是缓存有多行,这是我不知道的。
猜你喜欢
  • 1970-01-01
  • 2011-03-09
  • 2018-07-24
  • 2015-05-26
  • 2017-01-14
  • 1970-01-01
相关资源
最近更新 更多