为什么Windows中重复的内存分配会减慢?

时间:2016-04-21 07:22:53

标签: c++ windows

我想了解为什么以下代码在我的linux和Windows 7机器上表现不同: 在linux上,每次迭代需要大约120ms。 在Windows 7上,第一次迭代需要0.4秒,后续迭代需要更长的时间。迭代8已经需要大约11秒,迭代22大约需要1分钟。

我在不同的硬件上观察到了这种行为。它似乎与windows有关。

#include <iostream>
#include <time.h>
#include <chrono>

void iteration() {
  int n = 25000;
  // Allocate memory
  long** blocks = new long*[n];
  for( int i = 0; i<n; ++i )
  {
    blocks[i] = new long[100008];
  }
  // Free all allocated memory
  for( int i = 0; i<n; ++i )
  {
    delete[] blocks[i];
  }
  delete[] blocks;
}

int main(int argc, char **argv) {
  int nbIter = 30;
  for( int i = 0; i < nbIter; ++i )
  {
    auto start = std::chrono::system_clock::now();
    iteration();
    auto end = std::chrono::system_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "Iteration #" << i << ": time=" << elapsed.count() << "ms" << std::endl;
  }
  return 0;
}

有谁能告诉我这里发生了什么,以及如何让代码在Windows上运行稳定?

编辑:我在Windows上的VS2013中发布了一个版本,我从VS外部执行了该程序。 这里有一些更准确的运行时间(以秒为单位):

Iteration #0: time=0.381000
Iteration #1: time=0.391000
Iteration #2: time=0.451000
Iteration #3: time=1.507000
Iteration #4: time=1.711000
Iteration #5: time=2.938000
Iteration #6: time=4.770000
Iteration #7: time=7.840000
Iteration #8: time=10.563000
Iteration #9: time=14.606000
Iteration #10: time=20.732000
Iteration #11: time=24.948000
Iteration #12: time=30.255000
Iteration #13: time=34.608000
Iteration #14: time=38.114000
Iteration #15: time=43.217000
Iteration #16: time=39.926000
Iteration #17: time=43.506000
Iteration #18: time=43.660000
Iteration #19: time=45.291000
Iteration #20: time=50.003000

3 个答案:

答案 0 :(得分:3)

关于Windows上Diggingaround中的references heap(以及some infosec articles上的common heap slow downs some ),有一些关于articles的花絮说明少数是

  • 由于分配操作而减速。
  • 由于免费操作而减速。
  • 由于频繁的分配和重新分配而减速。

这有助于解释为什么存在减速(即频繁的分配和重新分配),但它并没有真正解释为什么存在减速。

应该注意的第一件事是sizeof(long) != sizeof(long),即在我使用g++和Visual Studio 12进行64位构建的测试中,在Windows上sizeof(long) 4 ,在Linux上它是 8 。这是分配/释放内存时的一个重要注意事项。如果您将代码从long类型更改为sizeof(T) == 8类型(如long long),那么问题就会消失,迭代次数也会保持一致。例如:

void iteration() {
    int n = 25000;
    // Allocate memory
    long long** blocks = new long long*[n];
    for (int i = 0; i < n; ++i) {
        blocks[i] = new long long[100008];
    }
    // Free all allocated memory
    for (int i = 0; i < n; ++i) {
        delete[] blocks[i];
    }
    delete[] blocks;
}
// sizeof(long long) == 8 on my Linux/Unix and Windows 64-bit machines

还应该注意,时间问题仅在完全 代码时消失。

如果您保持long long的类型,但将100008调整为16666,则问题会再次出现;此外,如果您将其更改为16668并立即运行long long次迭代,然后运行long版本,那么long long的时间将 up } {function},然后 down long,例如:

template < typename T >
void iteration() {
    int n = 25000;
    // Allocate memory
    T** blocks = new T*[n];
    for (int i = 0; i < n; ++i) {
        blocks[i] = new T[16668];
    }
    // Free all allocated memory
    for (int i = 0; i < n; ++i) {
        delete[] blocks[i];
    }
    delete[] blocks;
}

for (int i = 0; i < nbItrs; ++i) {
    iteration<long long>(); // time goes UP
}
for (int i = 0; i < nbItrs; ++i) {
    iteration<long>(); // time goes DOWN
}

此外,发布的代码使用malloc / freeLocalAlloc / LocalFree和/或HeapAlloc / HeapFree生成类似的结果,因为{ {1}} / new(在Windows上)调用malloc。原因与Windows如何管理堆内存和释放内存的位置有关。当必须删除页面时,需要清理空闲块列表,并且可能需要相应地调整列表。

在搜索和替换或删除列表中的旧内存块期间,这种调整可能需要一些时间。如果块没有落在干净的边界上,则可能需要对可用堆块列表进行其他调整。

深入了解Windows堆管理的方式和原因将涉及解释Windows内核的设计及其内存管理。进入这个问题超出了这个问题/答案的范围,但above answer与{{3}} {{3}}有关,并且很好地解释了如何以及为什么。

但是,您确实要求

  

如何让代码在Windows上运行稳定?

如上所述,更改类型将允许更一致的时间,另外,如另一个{{3}}中所述,以相反的顺序删除列表也将实现更一致的时序;

HeapAlloc

这是因为Windows内存管理器使用单个链接列表来维护堆位置,因此为什么在遍历列表时for (int i = n; i > 0; --i ) { delete[] blocks[i-1]; } 时间可以上升,为什么时间可以与Linux相比,在Windows上要慢一些(尽管我的测试在进行这些更改时实际上产生了相似的时间)。

我希望可以提供帮助。

答案 1 :(得分:2)

有趣的问题。我能够重现。

我得到了一致 - 虽然仍然有些迟钝 - delete[]按照分配的相反顺序执行这些功能:

for( int i = 0; i<n; ++i )
    delete[] blocks[n - 1 - i];

我怀疑它可能都与合并开销有关 - 来自MSDN here

  

由于免费操作而放缓。自由操作消耗更多周期,主要是在启用合并的情况下。在合并期间,每个自由操作应该“找到”它的邻居,将它们拉出来构造一个更大的块,然后将更大的块重新插入到空闲列表中。在该查找过程中,可能会以随机顺序触摸内存,从而导致缓存未命中和性能下降。

虽然有一些奇怪的事情:

  • 我的测量结果表明,虽然delete[]在第一次迭代中占用了大约80%的时间,或者在第一次迭代中花费了大约80%的时间,但是经过了几半的new[]花了差不多的时间。

  • 当我从new long[91134]转到...... 91135时,这个问题突然爆发:那已经接近356kb,但我没有设法谷歌任何相关内容。

答案 2 :(得分:0)

非常有趣的问题。我无法在带有MS Visual Studio Community 2013的Windows 10上重现它,但是如果要分配/解除分配大量固定大小的内存块,可以尝试用固定大小的内存块分配算法替换new / delete,也称为内存池。它的工作速度更快,速度更快。在这里,您可以找到基于BSD许可证的示例:https://github.com/intelmm/FixMemAlloc/blob/master/Sources/MemoryPool.h。也许这可能有所帮助。