【问题标题】:Impact of the prior loop iteration on the execution time of the current iteration先前循环迭代对当前迭代执行时间的影响
【发布时间】:2019-11-29 23:35:39
【问题描述】:

我正在尝试测量folly hashmap 中并发插入的性能。这里提供了一个用于这种插入的程序的简化版本:

#include <folly/concurrency/ConcurrentHashMap.h>
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

const int kNumMutexLocks = 2003;
std::unique_ptr<std::mutex[]> mutices(new std::mutex[kNumMutexLocks]);
__inline__ void
concurrentInsertion(unsigned int threadId, unsigned int numInsertionsPerThread,
                    unsigned int numInsertions, unsigned int numUniqueKeys,
                    folly::ConcurrentHashMap<int, int> &follyMap) {
  int base = threadId * numInsertionsPerThread;
  for (int i = 0; i < numInsertionsPerThread; i++) {
    int idx = base + i;
    if (idx >= numInsertions)
      break;
    int val = idx;
    int key = val % numUniqueKeys;
    mutices[key % kNumMutexLocks].lock();
    auto found = follyMap.find(key);
    if (found != follyMap.end()) {
      int oldVal = found->second;
      if (oldVal < val) {
        follyMap.assign(key, val);
      }
    } else {
      follyMap.insert(key, val);
    }
    mutices[key % kNumMutexLocks].unlock();
  }
}

void func(unsigned int numInsertions, float keyValRatio) {
  const unsigned int numThreads = 12; // Simplified just for this post
  unsigned int numUniqueKeys = numInsertions * keyValRatio;
  unsigned int numInsertionsPerThread = ceil(numInsertions * 1.0 / numThreads);
  std::vector<std::thread> insertionThreads;
  insertionThreads.reserve(numThreads);
  folly::ConcurrentHashMap<int, int> follyMap;

  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < numThreads; i++) {
    insertionThreads.emplace_back(std::thread([&, i] {
      concurrentInsertion(i, numInsertionsPerThread, numInsertions,
                          numUniqueKeys, follyMap);
    }));
  }
  for (int i = 0; i < numThreads; i++) {
    insertionThreads[i].join();
  }
  auto end = std::chrono::steady_clock::now();

  auto diff = end - start;
  float insertionTimeMs =
      std::chrono::duration<double, std::milli>(diff).count();
  std::cout << "i: " << numInsertions << "\tj: " << keyValRatio
            << "\ttime: " << insertionTimeMs << std::endl;
}

int main() {
  std::vector<float> js = {0.5, 0.25};
  for (auto j : js) {
    std::cout << "-------------" << std::endl;
    for (int i = 2048; i < 4194304 * 8; i *= 2) {
      func(i, j);
    }
  }  
}

问题是在 main 中使用这个循环会突然增加func 函数中的测量时间。也就是说,如果我直接从 main 调用该函数而没有任何循环(如下所示),某些情况下的测量时间会突然减少 100 倍以上。

int main() {
  func(2048, 0.25); // ~ 100X faster now that the loop is gone.
}

可能的原因

  1. 我在构建 hasmap 时分配了大量内存。我相信当我在循环中运行代码时,在执行循环的第二次迭代时,计算机正忙于为第一次迭代释放内存。因此,程序变得慢得多。如果是这种情况,我将不胜感激,如果有人能提出一个改变,我可以通过循环获得相同的结果。

更多详情

请注意,如果我在 main 中展开循环,我也会遇到同样的问题。也就是下面的程序也有同样的问题:

int main() {
  performComputation(input A);
  ...
  performComputation(input Z);
} 

样本输出

第一个程序的输出如下所示:

i: 2048     j: 0.5  time: 1.39932
...
i: 16777216 j: 0.5  time: 3704.33
------------- 
i: 2048     j: 0.25 time: 277.427 <= sudden increase in execution time
i: 4096     j: 0.25 time: 157.236
i: 8192     j: 0.25 time: 50.7963
i: 16384    j: 0.25 time: 133.151
i: 32768    j: 0.25 time: 8.75953
...
i: 2048     j: 0.25 time: 162.663

使用i=2048 和j=0.25 在main 中单独运行func 会产生:

i: 2048     j: 0.25 time: 1.01

非常感谢任何评论/见解。

【问题讨论】:

  • 我不清楚你在问什么。您是在问为什么,还是为了更好的解决方案?
  • 感谢@Chipster 的评论。我在问为什么以及如何找到解决方案。
  • 有很多可能性,比如与缓存位置相关的事情,或者与您的想法类似的事情——在操作系统中触发与大内存区域的分配/解除分配相关的事情。不太可能有人可以远程诊断。我首先删除/跳过“B”中发生的事情的块,然后比较单个调用和多个调用,看看删除其中的某些内容是否会使执行时间的差异消失。
  • 感谢@SamVarshavchik 的评论。可能不是缓存问题,因为我可以非常随机地访问大量内存占用。我希望有人建议一个函数调用,可以强制程序在每次循环迭代后释放所有资源。你的建议很棒。我一直在这样做,并将继续努力。谢谢。
  • delete 和 free 删除所有分配的内存。向量的析构函数会这样做,因为您提到您正在通过向量分配内存。在这个主题上,没有其他事情可以做。

标签: c++ c++11 time stl folly


【解决方案1】:

如果是内存分配减慢了它的速度并且performComputation(input)之前的内存内容无关紧要,你可以重新使用分配的内存块。

int performComputation(input, std::vector<char>& memory) { 

  /* Note: memory will need to be passed by reference*/

  auto start = std::chrono::steady_clock::now();
  for (int i = 0; i < numThreads; i++) {
    t.emplace_back(std::thread([&, i] {
      func(...); // Random access to memory
    }));
  }

  for (int i = 0; i < numThreads; i++) {
    t[i].join();
  }

  auto end = std::chrono::steady_clock::now();
  float time = std::chrono::duration<double, std::milli>(end - start).count();

}

int main() {

  // A. Allocate ~1GB memory here
  std::vector<char> memory(1028 * 1028 * 1028) //is that 1 gig?

  for (input: inputs)
    performComputation(input, memory);
}

【讨论】:

  • 感谢您的评论。我尝试提前分配内存,但这并没有解决非确定性输出。我相应地更新了帖子,并包含了该程序的完整版本。再次感谢您的回复。如果你有时间,请看一下修改后的帖子。
【解决方案2】:

我对具体细节不能太自信,但在我看来,这是构建地图时内存分配的结果。我使用普通的unordered_map 和单个mutex 复制了您看到的行为,并在func static 中制作地图对象完全修复了它。 (实际上现在第一次运行时会稍微慢一些,因为还没有为地图分配内存,然后每次运行都更快且时间一致。)

我不知道为什么这会有所不同,因为地图已被破坏,内存应该已被释放。出于某种原因,地图释放的内存似乎没有在随后对func 的调用中被重用。也许其他人比我更了解这一点。

编辑:减少最小的、可重现的示例和输出

void func(int num_insertions)
{
    const auto start = std::chrono::steady_clock::now();

    std::unordered_map<int, int> map;
    for (int i = 0; i < num_insertions; ++i)
    {
        map.emplace(i, i);
    }

    const auto end = std::chrono::steady_clock::now();
    const auto diff = end - start;

    const auto time = std::chrono::duration<double, std::milli>(diff).count();
    std::cout << "i: " << num_insertions << "\ttime: " << time << "\n";
}

int main()
{
    func(2048);
    func(16777216);
    func(2048);
}

使用非静态地图:

i: 2048 time: 0.6035
i: 16777216     time: 4629.03
i: 2048 time: 124.44

带静态地图:

i: 2048 time: 0.6524
i: 16777216     time: 4828.6
i: 2048 time: 0.3802

另一个编辑:还应该提到静态版本还需要在最后调用map.clear(),尽管这与插入性能问题并不真正相关。

【讨论】:

    【解决方案3】:

    在测量挂钟时间时使用平均值!

    您正在测量挂钟时间。 jumps 看到的实际时间在这方面有点小范围内,理论上可能是由操作系统延迟或其他处理引起的,或者可能由于您的线程管理(例如清理)而导致更糟程序(请注意,这可能会因平台/系统而有很大差异,请记住,上下文切换可能很容易花费约 10-15 毫秒)有太多的参数在起作用,无法确定。 在使用挂钟进行测量时,通常的做法是在数百或数千次的循环中取平均值以考虑尖峰/等...

    使用分析器

    学习使用分析器 - 分析器可以帮助您快速查看程序实际花费的时间,并一次又一次地节省宝贵的时间。

    【讨论】:

    • 感谢您的回复。这本质上就是我好奇的地方。是花费在内存管理、线程调度、上下文切换或其他内务活动(哪一项)上的时间。在上面的例子中时间很短,但是也有延迟接近 1s 的情况。使用分析器可能是最好的主意。感谢您的建议。
    • @MTMD 归根结底,在这些类型的问题中很难正确“猜测”,而且我们常常会惊讶地发现瓶颈在哪里。
    猜你喜欢
    • 2015-05-04
    • 2019-06-01
    • 2012-06-12
    • 1970-01-01
    • 2021-04-12
    • 1970-01-01
    • 2012-09-02
    • 1970-01-01
    • 2018-12-01
    相关资源
    最近更新 更多