为什么释放堆内存比分配堆内存慢得多?

5

这是一种经验性的假设(分配比释放更快)。

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

当然,我们不应混淆“堆”内存和类似“堆”的数据结构。


那么“为什么释放比分配慢”?

它是特定于Windows(我在Win 8.1上看到它)还是与操作系统无关的?

在使用“new”/“delete”时是否有某些C++特定的内存管理器自动参与,还是整个内存管理完全依赖于操作系统?(我知道C++11引入了一些垃圾收集支持,但我从来没有真正使用过,更好地依靠旧的堆栈和静态持续时间或自管理的容器和RAII)。

此外,在“FOLLY string”的代码中,我看到使用旧的C堆分配/释放,它比C++的“new”/“delete”更快吗?


P.S.请注意,问题不涉及虚拟内存机制,我理解用户空间程序不使用实际的内存寻址。


5
请贴出您真实的代码。 - o11c
分配/释放是系统调用,它们具有内核实现,您可以使用syscall()进行调用。所有在库中声明的分配/释放函数,例如C++的new/delete或C++的malloc,都可以扩展系统调用或直接调用它。这取决于操作系统的性能,因为您无法在其根本上重写分配的内核实现。 - Pierre Emmanuel Lallemant
1
什么是经验假设? - juanchopanza
3
@PierreEmmanuelLallemant:不,分配(mallocnew)和释放(freedelete不是系统调用(在Linux上,列在syscalls(2)中...)。C运行时(例如malloc)有时可能会调用mmapsbrk,但不总是(它尝试重用先前的free区域)。请参阅这里 - Basile Starynkevitch
@AmaboCarab:你确定你的初始声明吗?你编写并基准测试了一个程序,它使用malloc分配许多随机大小的区域,并以某种随机顺序释放它们吗? - Basile Starynkevitch
3
malloc/free 处理一些相当复杂的数据结构,以便回收内存并且不会引起过多的碎片。这有点随意,取决于确切的算法,在 malloc 和 free 中哪些簿记部分是完成的。 - Marc Glisse
5个回答

3
我觉得分配内存比释放内存更快这个说法有些奇怪,所以我进行了测试。我进行了一个测试,其中我以32字节为单位分配了64MB的内存(因此调用了2M次new),并尝试按照分配顺序和随机顺序删除该内存。我发现线性顺序释放比分配要快大约3%,而随机顺序释放比线性顺序分配要慢约10%。
然后,我进行了一项测试,一开始有64MB的已分配内存,然后每次使用随机方式分配新内存或删除现有内存(共2M次)。在这里,我发现释放比分配要慢约4.3%。
所以,事实证明你是正确的-释放比分配慢(虽然我不会称其为“非常”慢)。我怀疑这只是由于更多的随机访问,但我没有其他证据,除了线性释放更快这一点。
回答一些你的问题: 在使用'new' / 'delete'时是否涉及某些特定于C ++的内存管理器? 是的。操作系统具有向进程分配页面内存(通常为4KB块)的系统调用。将这些页面划分为对象是进程的工作。尝试查找“GNU内存分配器”。 我看到使用旧的C堆分配/释放,它比C ++的'new' / 'delete'更快吗? 大多数C ++的new/delete实现只是在幕后调用mallocfree。虽然这不是标准所要求的,但在任何特定对象上始终使用相同的分配和释放函数是一个好主意。
我在Windows 10 64位计算机上使用Visual Studio 2015提供的本地测试框架运行了我的测试(测试也是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];
                }
            }
        }
    };
}

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


这非常有趣,因为_Basile Starynkevitch_在他的GCC和Debian示例中得出的结论是:“free似乎比malloc快两倍”。 - Amabo Carab
在我的例子中,我确保分配了一些相当随机的大小,并以随机顺序释放。 - Basile Starynkevitch

2

我不确定你的观察是否正确。我写了以下程序(在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 的调用可以忽略不计(我不确定是否可以),相对于 mallocfree 的时间,我倾向于不同意你的观察结果。 free 似乎比 malloc 快两倍。我的 gcc 版本是 6.1,我的 libc 版本是 Glibc 2.22。

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

顺便说一句,我使用了Jerry的代码

 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,这也很重要。我看到这里呈现的测试结果差异很大,所以我认为答案实际上取决于情况。 - Amabo Carab

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 Commuinty Update 5中使用大小为2000的编译,并且完全开启了速度优化,然后在我的Windows 8.1 Lenovo IdeaPad S400上运行,得到了alloc时间:16 | del时间:109(x100,000)。 - Amabo Carab
所以,是的,在我的情况下dealloc慢了10倍。 - Amabo Carab
@AmaboCarab:我并不确定这是真的。虽然有可能,但已知在VS 2013中,high_resolution_clock的实现非常糟糕。如果你真的关心这个编译器的结果,你可能需要重写测试,使用另一个计时器(例如GetPerformanceCounter)来代替。 - Jerry Coffin
好的,同样的代码、同一个项目、同一台电脑等都是相同的,但是在 MS VS 2015 Community Update 2 中我得到了alloc time: 2 127 917 | del time: 1 537 209,所以没错,几乎是一样的。 - Amabo Carab
@AmaboCarab:我一直在使用/O2b2 /GL作为编译器选项。我尝试了各种其他选项,但没有找到任何能够产生像你引用的结果的选项。我很难相信这会有太大的区别 - 我们计时的大部分是标准库中的代码,所以唯一重要的是链接哪个库。 - Jerry Coffin
显示剩余3条评论

1
当您分配小内存块时,您指定的块大小直接映射到该大小的子分配器,通常表示为包含相同大小记录的内存“slab”,以避免内存碎片化。这可以非常快速,类似于数组访问。但是释放这样的块并不那么简单,因为您正在传递指向未知大小内存的指针,需要额外的工作来确定它属于哪个slab,然后才能将该块返回到其正确位置。
当您分配大块虚拟内存时,在您的进程空间中设置一个内存页范围,而不实际映射任何物理内存到其中,这需要非常少的工作来完成。但是释放这样的大块可能需要更多的工作,因为首先必须将释放的指针与该范围的页表匹配,然后通过遍历跨越其所涵盖的内存范围的所有页面条目,并通过介入的页面故障释放分配给该范围的所有物理内存页面。
当然,这方面的细节会因使用的实现而异,但原则基本相同:已知块大小的内存分配需要比释放未知大小的内存块的指针更少的努力。我对此的了解直接来自我开发高性能商业级RAII内存分配器的经验。
我还应该指出,由于每个堆分配都有相应的释放,这对操作代表了一个单独的分配周期,即作为同一枚硬币的两面。它们的执行时间可以准确地测量,但是分开测量却很难确定,因为它取决于块大小、类似大小的先前活动、缓存和其他运行考虑因素的广泛变化。但最终,分配/释放差异可能并不重要,因为你不能只做其中之一。

0
这里的问题是堆碎片化。使用显式指针算术语言编写的程序没有实际的方法来整理堆。
如果堆被碎片化,就无法将内存返回给操作系统。除虚拟内存外,操作系统依赖于类似于 `brk(2)` 的机制 - 即您设置所有内存地址的上限。但是,当您有一个分配的缓冲区仍在现有边界附近使用时,即使您释放了程序中99%的所有内存,也无法明确地将内存返回给操作系统。
解除分配并不一定比分配更慢。但是,手动解除堆碎片化使得分配变得更加缓慢和复杂。
GC 通过压缩堆来解决这个问题。这样,对于它们而言,分配只是增加指针,大多数对象不需要解除分配。

你的假设是除非进程终止,否则内存永远不会返回给操作系统?还是我理解错了?这真的很奇怪,那长时间运行的服务器程序怎么办?系统资源,比如套接字呢?那看起来就像一个大内存泄漏... - Amabo Carab
程序使用的系统资源通常是操作系统内存中的结构,由不透明的数字“句柄”或“fds”引用。操作系统在分配和重用它们方面做得很好。现在,程序不会将内存返回给操作系统,但它们可以重用内存。如果它们释放内存的速度与分配内存的速度相同,它们就不会耗尽内存。 - alamar
你的意思是程序堆总是私有堆?并且一些特定的进程片段只有自己的堆,而不是主要的通用堆?这个私有堆只能随着时间增长,永远不会缩小?因此,操作系统管理这个堆,而不是运行时库或者它是未知的、未指定的和基于实现的? - Amabo Carab
这个答案似乎并没有解释为什么释放内存会更慢。实际上,如果程序没有释放内存,它应该会更快,因为系统调用更少。 - IanPudney
2
зҺ°д»ЈеҲҶй…ҚеҷЁеӨ§еӨҡж•°жғ…еҶөдёӢдҪҝз”Ёmmap()иҖҢдёҚжҳҜbrk()пјҢиҝҷж ·жӣҙеҘҪең°и§ЈеҶідәҶзўҺзүҮй—®йўҳгҖӮ - gudok
@gudok 如果你分配大缓冲区 - 可能是这样。如果你分配了无数小缓冲区,不要认为mmap()会有所帮助。 - alamar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接