【问题标题】:Why deallocating heap memory is much slower than allocating it?为什么释放堆内存比分配它慢得多?
【发布时间】:2016-10-28 01:59:38
【问题描述】:

这是一个经验假设(分配比取消分配更快)。

这也是一个的原因,我猜,为什么基于堆的存储(如 STL 容器或其他)选择不将当前未使用的内存返回给系统(这就是 shrink-to-fit 成语诞生的原因)。

当然,我们不应该将“”内存与“”类数据结构混淆。


那么为什么释放速度较慢

它是特定于 Windows(我在 Win 8.1 上看到的)还是独立于 OS

在使用 'new' / 'delete' 或整个内存时是否会自动涉及某些 C++ 特定的内存管理器。管理是否完全依赖于操作系统? (我知道 C++11 引入了一些垃圾收集支持,我从未真正使用过,更好地依赖旧的 stackstatic 持续时间或自我管理的容器RAII)。

另外,在 FOLLY 字符串 的代码中,我看到使用旧的 C 堆分配/释放,它是否比 C++ 'new' / 'delete 更快'?


P. S.请注意,问题不是关于虚拟内存机制,我知道用户空间程序没有使用真正的内存。地址。

【问题讨论】:

  • 请发布您的真实代码。
  • Allocation/Deallocation 是系统调用,它们具有内核实现,您可以使用syscall() 调用它们。所有在库中声明的分配/释放函数,如 c++ 的 new/delete 或 c++ 的 malloc,都可以扩展系统调用或直接调用它。性能取决于操作系统,因为您无法在其根目录重写分配的内核实现。
  • 什么是经验假设?
  • @PierreEmmanuelLallemant:不,分配 (malloc & new) 和释放 (free & delete) 是不是系统调用(在 Linux 上,列出在syscalls(2)....)。C 运行时(例如malloc)可能有时调用mmapsbrk,但并非总是如此(它尝试重用以前的free-d区)。见this
  • malloc/free 处理一些相当复杂的数据结构,因此它们可以回收内存并且不会造成太多碎片。这有点武断,取决于确切的算法,哪些部分记账在 malloc 期间完成,哪些部分在 free 期间完成。

标签: c++ memory memory-management heap-memory


【解决方案1】:

分配内存比释放内存更快的断言对我来说似乎有点奇怪,所以我测试了它。我运行了一个测试,我在 32 字节的块中分配了 64MB 的内存(所以对 new 进行了 2M 次调用),我尝试按照分配的顺序和随机顺序删除该内存。我发现线性顺序释放比分配快 3%,而随机释放比线性分配慢 10%

然后我运行了一个测试,我从分配的 64MB 内存开始,然后 2M 次分配新内存或删除现有内存(随机)。在这里,我发现释放比分配慢了大约 4.3%。

所以,事实证明你是对的——解除分配比分配慢(尽管我不会称它“慢得多”)。我怀疑这只是与更多的随机访问有关,但除了线性释放更快之外,我没有其他证据证明这一点。

回答您的一些问题:

在使用 'new' / 'delete' 时是否会自动涉及某些 C++ 特定的内存管理器?

是的。操作系统有系统调用,它为进程分配内存页面(通常为 4KB 块)。将这些页面划分为对象是流程的工作。尝试查找“GNU 内存分配器”。

我看到使用旧的 C 堆分配/释放,它比 C++ 'new' / 'delete' 更快吗?

大多数 C++ new/delete 实现只是在底层调用 mallocfree。然而,这不是标准所要求的,因此最好始终对任何特定对象使用相同的分配和释放函数。

我使用 Visual Studio 2015 中提供的本机测试框架在 Windows 10 64 位机器上运行我的测试(测试也是 64 位)。代码如下:

#include "stdafx.h"
#include "CppUnitTest.h"

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

namespace AllocationSpeedTest
{       
    class Obj32 {
        uint64_t a;
        uint64_t b;
        uint64_t c;
        uint64_t d;
    };
    constexpr int len = 1024 * 1024 * 2;
    Obj32* ptrs[len];
    TEST_CLASS(UnitTest1)
    {
    public:
        TEST_METHOD(Linear32Alloc)
        {
            for (int i = 0; i < len; ++i) {
                ptrs[i] = new Obj32();
            }
        }
        TEST_METHOD(Linear32AllocDealloc)
        {
            for (int i = 0; i < len; ++i) {
                ptrs[i] = new Obj32();
            }
            for (int i = 0; i < len; ++i) {
                delete ptrs[i];
            }
        }
        TEST_METHOD(Random32AllocShuffle)
        {
            for (int i = 0; i < len; ++i) {
                ptrs[i] = new Obj32();
            }
            srand(0);
            for (int i = 0; i < len; ++i) {
                int pos = (rand() % (len - i)) + i;
                Obj32* temp = ptrs[i];
                ptrs[i] = ptrs[pos];
                ptrs[pos] = temp;
            }
        }
        TEST_METHOD(Random32AllocShuffleDealloc)
        {
            for (int i = 0; i < len; ++i) {
                ptrs[i] = new Obj32();
            }
            srand(0);
            for (int i = 0; i < len; ++i) {
                int pos = (rand() % (len - i)) + i;
                Obj32* temp = ptrs[i];
                ptrs[i] = ptrs[pos];
                ptrs[pos] = temp;
            }
            for (int i = 0; i < len; ++i) {
                delete ptrs[i];
            }
        }
        TEST_METHOD(Mixed32Both)
        {
            for (int i = 0; i < len; ++i) {
                ptrs[i] = new Obj32();
            }
            srand(0);
            for (int i = 0; i < len; ++i) {
                if (rand() % 2) {
                    ptrs[i] = new Obj32();
                }
                else {
                    delete ptrs[i];
                }
            }
        }
        TEST_METHOD(Mixed32Alloc)
        {
            for (int i = 0; i < len; ++i) {
                ptrs[i] = new Obj32();
            }
            srand(0);
            for (int i = 0; i < len; ++i) {
                if (rand() % 2) {
                    ptrs[i] = new Obj32();
                }
                else {
                    //delete ptrs[i];
                }
            }
        }
        TEST_METHOD(Mixed32Dealloc)
        {
            for (int i = 0; i < len; ++i) {
                ptrs[i] = new Obj32();
            }
            srand(0);
            for (int i = 0; i < len; ++i) {
                if (rand() % 2) {
                    //ptrs[i] = new Obj32();
                }
                else {
                    delete ptrs[i];
                }
            }
        }
        TEST_METHOD(Mixed32Neither)
        {
            for (int i = 0; i < len; ++i) {
                ptrs[i] = new Obj32();
            }
            srand(0);
            for (int i = 0; i < len; ++i) {
                if (rand() % 2) {
                    //ptrs[i] = new Obj32();
                }
                else {
                    //delete ptrs[i];
                }
            }
        }
    };
}

以下是多次运行的原始结果。所有数字都以毫秒为单位。

【讨论】:

  • 这很有趣,因为他的 GCC 和 Debian 上的 Basile Starynkevitch 示例导致“free 的速度似乎是 malloc 的两倍” .
  • 在我的示例中,我确实注意分配了一些相当随机的大小和随机顺序的空闲。
【解决方案2】:

我的想法与@Basile 大致相同:我想知道您的基本假设是否实际上(甚至接近)正确。由于您标记了 C++ 问题,因此我用 C++ 编写了一个快速基准测试。

#include <vector>
#include <iostream>
#include <numeric>
#include <chrono>
#include <iomanip>
#include <locale>

int main() {
    std::cout.imbue(std::locale(""));

    using namespace std::chrono;
    using factor = microseconds;

    auto const size = 2000;

    std::vector<int *> allocs(size);

    auto start = high_resolution_clock::now();

    for (int i = 0; i < size; i++)
        allocs[i] = new int[size];

    auto stop = high_resolution_clock::now();
    auto alloc_time = duration_cast<factor>(stop - start).count();

    start = high_resolution_clock::now();

    for (int i = 0; i < size; i++)
        delete[] allocs[i];

    stop = high_resolution_clock::now();

    auto del_time = duration_cast<factor>(stop - start).count();

    std::cout << std::left << std::setw(20) << "alloc time: " << alloc_time << " uS\n";
    std::cout << std::left << std::setw(20) << "del time: " << del_time << " uS\n";
}

我还在 Windows 上使用 VC++,而不是在 Linux 上使用 gcc。结果并没有太大的不同:释放内存比分配内存花费的时间要少得多。这是三个连续运行的结果。

alloc time:         2,381 uS
del time:           1,429 uS

alloc time:         2,764 uS
del time:           1,592 uS

alloc time:         2,492 uS
del time:           1,442 uS

不过,我要警告的是,分配和释放(主要)由标准库处理,因此一个标准库和另一个标准库之间可能会有所不同(即使使用相同的编译器)。我还注意到,如果这在多线程代码中有所改变,我不会感到惊讶。尽管它实际上并不正确,但似乎有一些作者误解了在多线程环境中释放需要锁定一个堆以进行独占访问。这可以避免,但这样做的方法并不一定立即显而易见。

【讨论】:

  • 好吧,我拿了你的代码,在我的 MS VS 2013 社区更新 5 中用 size = 2000 编译它,速度优化完全开启在我的 Windows 8.1 Lenovo IdeaPad S400 上,我得到了 alloc time: 16 |删除时间:109 (x100 000)
  • 是的,在我的情况下,dealloc 慢了 10 倍。
  • @AmaboCarab:我完全不确定这是否属实。可能是——但众所周知,在 VS 2013 中,high_resolution_clock 的实现被彻底破坏了。如果你真的关心这个编译器的结果,你可能想要重写测试以使用不同的计时器(例如,GetPerformanceCounter)。
  • Jerry Coffin,是的,确实时钟有点坏,我知道,好的,我会编译代码uisng MS VS 2015 更新2,但我不认为我会有完全不同的结果。
  • @AmaboCarab:也就是说,我无法重现任何与您的结果相近的东西——尽管我并不完全相信它们,但我使用 VS 2013 得到的结果大致遵循相同的模式我在其他编译器中看到:删除的速度是分配的 左右 的两倍。
【解决方案3】:

我不确定你的观察。我编写了以下程序(在 Linux 上,希望您可以将其移植到您的系统中)。

// public domain code
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
#include <string.h>
#include <assert.h>


const unsigned possible_word_sizes[] = {
  1, 2, 3, 4, 5,
  8, 12, 16, 24,
  32, 48, 64, 128,
  256, 384, 2048
};

long long totalsize;

// return a calloc-ed array of nbchunks malloced zones of
// somehow random size
void **
malloc_chunks (int nbchunks)
{
  const int nbsizes =
    (int) (sizeof (possible_word_sizes)
       / sizeof (possible_word_sizes[0]));
  void **ad = calloc (nbchunks, sizeof (void *));
  if (!ad)
    {
      perror ("calloc chunks");
      exit (EXIT_FAILURE);
    };
  for (int ix = 0; ix < nbchunks; ix++)
    {
      unsigned sizindex = random () % nbsizes;
      unsigned size = possible_word_sizes[sizindex];
      void *zon = malloc (size * sizeof (void *));
      if (!zon)
    {
      fprintf (stderr,
           "malloc#%d (%d words) failed (total %lld) %s\n",
           ix, size, totalsize, strerror (errno));
      exit (EXIT_FAILURE);
    }
      ((int *) zon)[0] = ix;
      totalsize += size;
      ad[ix] = zon;
    }
  return ad;
}

void
free_chunks (void **chks, int nbchunks)
{
// first, free the two thirds of chunks in random order
  for (int i = 0; 3 * i < 2 * nbchunks; i++)
    {
      int pix = random () % nbchunks;
      if (chks[pix])
    {
      free (chks[pix]);
      chks[pix] = NULL;
    }
    }
// then, free the rest in reverse order
  for (int i = nbchunks - 1; i >= 0; i--)
    if (chks[i])
      {
    free (chks[i]);
    chks[i] = NULL;
      }
}

int
main (int argc, char **argv)
{
  assert (sizeof (int) <= sizeof (void *));
  int nbchunks = (argc > 1) ? atoi (argv[1]) : 32768;
  if (nbchunks < 128)
    nbchunks = 128;
  srandom (time (NULL));
  printf ("nbchunks=%d\n", nbchunks);
  void **chks = malloc_chunks (nbchunks);
  clock_t clomall = clock ();
  printf ("clomall=%ld totalsize=%lld words\n",
      (long) clomall, totalsize);
  free_chunks (chks, nbchunks);
  clock_t clofree = clock ();
  printf ("clofree=%ld\n", (long) clofree);
  return 0;
}   

我在我的 Debian/Sid/x86-64 (i3770k, 16Gb) 上使用 gcc -O2 -Wall mf.c -o mf 编译它。我运行time ./mf 100000 并得到:

nbchunks=100000
clomall=54162 totalsize=19115681 words
clofree=83895
./mf 100000  0.02s user 0.06s system 95% cpu 0.089 total

在我的系统上 clock 给 CPU 微秒。如果对random 的调用可以忽略不计(我不知道是否如此)w.r.t. malloc & free 时间,我倾向于不同意你的观察。 free 似乎是 malloc 的两倍。我的 gcc 是 6.1,我的 libc 是 Glibc 2.22。

请花时间在您的系统上编译上述基准并报告时间。

FWIW,我拿了Jerry's code

 g++ -O3 -march=native jerry.cc -o jerry
 time ./jerry;  time ./jerry; time ./jerry

给予

alloc time:         1940516
del time:           602203
./jerry  0.00s user 0.01s system 68% cpu 0.016 total
alloc time:         1893057
del time:           558399
./jerry  0.00s user 0.01s system 68% cpu 0.014 total
alloc time:         1818884
del time:           527618
./jerry  0.00s user 0.01s system 70% cpu 0.014 total

【讨论】:

  • 事实上,我在 C++ 程序中看到了上述行为(即释放堆内存比分配它慢得多),它使用了 STL 容器(它是一个哈希映射或集合),它然后被填充并销毁。需要提一点:我猜当应用程序堆已经被大量使用/碎片化时会观察到这种行为。这可能很关键,因为那里的所有人都在重新执行测试。我也使用 Windows,而不是 *nix,这也很重要。我看到这里提供的测试给出了不同的结果,所以我猜答案实际上取决于。
【解决方案4】:

当您分配小内存块时,您指定的块大小直接映射到该大小的子分配器,通常表示为包含相同大小记录的内存“平板”,以避免内存碎片。这可以非常快,类似于数组访问。但是释放这些块并不是那么简单,因为你传递了一个指向未知大小内存的指针,需要额外的工作来确定它属于哪个slab,然后才能将块返回到正确的位置。

当您分配大块虚拟内存时,会在您的进程空间中设置一个内存页面范围,而实际上并未将任何物理内存映射到它,这需要很少的工作即可完成。但是释放这么大的块可能需要更多的工作,因为释放的指针必须首先与该范围的页表匹配,然后遍历它跨越的内存范围的所有页条目,并释放所有物理由中间页错误分配给该范围的内存页。

当然,这方面的细节会根据所使用的实现而有所不同,但原理大致相同:已知块大小的内存分配比释放指向未知大小的内存块的指针需要更少的努力。我对此的了解直接来自我开发高性能商业级 RAII 内存分配器的经验。

我还应该指出,由于每个堆分配都有一个匹配和对应的释放,这对操作代表一个分配周期,即作为一个硬币的两个面。总之,可以准确地测量它们的执行时间,但单独这样的测量很难确定,因为它根据块大小、类似大小的先前活动、缓存和其他操作考虑因素而有很大差异。但归根结底,分配/释放的差异可能并不重要,因为你不会一个没有另一个。

【讨论】:

    【解决方案5】:

    这里的问题是堆碎片。用具有显式指针算法的语言编写的程序没有实际的堆碎片整理方法。

    如果您的堆碎片化,则无法将内存返回给操作系统。操作系统,除了虚拟内存,依赖于brk(2)-like 机制——即你为你将引用的所有内存地址设置一个上限。但是,当您甚至分配了一个缓冲区并且仍在现有边界附近使用时,您就无法将内存显式返回给操作系统。是否释放了程序中 99% 的内存也没关系。

    解除分配不必比分配慢。但是,您通过堆碎片手动解除分配这一事实使分配变得更慢且更复杂。

    GCs 通过压缩堆来解决这个问题。这样,分配只是为它们增加指针,而大量对象不需要释放。

    【讨论】:

    • 您认为除非进程终止,否则没有内存会返回到操作系统?还是我理解错了?那真的很奇怪,长时间运行的服务器程序是怎么回事?系统资源,如套接字?那似乎是一次大的内存泄漏......
    • @AmaboCarab 程序使用的系统资源通常是操作系统内存中的结构,由不透明的数字“句柄”或“fds”引用。操作系统在分配和重用它们方面做得很好。现在,程序不会将内存返回给操作系统,但它们可以重用内存。如果它们碰巧以与分配内存相同的速度释放内存,它们就不会耗尽内存。
    • 你的意思是程序堆总是私有堆?并且一些特定的进程碎片只有它自己的堆,而不是主要的通用堆?而这个私有堆只能随时间增长,永远不会收缩?所以操作系统管理这个堆,而不是运行时库,或者它是未知的、未指定的和基于实现的?
    • 这个答案似乎并没有提供任何解释为什么解除分配会更慢。事实上,如果程序没有释放内存,它应该更快因为更少的系统调用。
    • 现代分配器大部分时间使用mmap(),而不是brk(),后者更能对抗碎片化。
    猜你喜欢
    • 2019-12-07
    • 2011-01-21
    • 2013-02-28
    • 2014-11-07
    • 1970-01-01
    • 2016-12-02
    • 2013-05-10
    • 2013-08-02
    • 2021-10-03
    相关资源
    最近更新 更多