【问题标题】:Why malloc is faster than static memory allocation in my test program?为什么 malloc 在我的测试程序中比静态内存分配快?
【发布时间】:2016-04-03 01:05:03
【问题描述】:

我有一个测试程序。我在 ubuntu 可信赖的 64 位中执行它时得到这个结果。

malloc 时间:9571

静态时间:45587

为什么 malloc 比静态内存分配快,还是我的测试程序有问题?

测试程序是这样的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>

#define TIME 10000

int data[1024] = { 1,2,3,4,5,6,6,7,8,5,4,3,2,3 };
int st[TIME][1024];
int main(void) {
    int i = 0;
    int time = 0;
    struct timeval tv1,tv2;

    /* test for malloc */
    memset(&tv1,0,sizeof(tv1));
    memset(&tv2,0,sizeof(tv2));
    gettimeofday(&tv1,NULL);
    for(i=0;i<TIME;i++) {
        void * p = malloc(4096);
        memset(p,0,4096);
        memcpy(p,data,sizeof(data));
        free(p);
        p = NULL;
    }
    gettimeofday(&tv2,NULL);
    time = ((tv2.tv_sec - tv1.tv_sec) * 1000000 +
        (tv2.tv_usec - tv1.tv_usec));
    printf("malloc time:%d\n",time);

    /* test for static memory allocation */
    memset(&tv1,0,sizeof(tv1));
    memset(&tv2,0,sizeof(tv2));
    gettimeofday(&tv1,NULL);
    for(i=0;i<TIME;i++) {
        memset(st[i],0,4096);
        memcpy(st[i],data,sizeof(data));
    }
    gettimeofday(&tv2,NULL);
    time = ((tv2.tv_sec - tv1.tv_sec) * 1000000 +
        (tv2.tv_usec - tv1.tv_usec));
    printf("static time:%d\n",time);

    return 0;
}

【问题讨论】:

  • 尝试使用 free() 调用重复您的测试。
  • @AndréPuel 我猜你的意思是“没有”
  • 首先要做的是检查汇编代码,看看发生了什么。编译器可以优化这两个测试,因为它们没有可观察的行为。
  • st 是 40MB,p 指向 4KB。如果你真的想测试 malloc 与静态内存,你要么需要 malloc 40MB,要么需要让 st 4KB。
  • @M.M 当没有free()调用时,malloc时间为39129,静态时间为45362。可能是malloc重复使用同一段内存

标签: c linux performance memory-management x86-64


【解决方案1】:

该基准基本上没有意义,因为它所测量的大部分内容与两个内存区域的使用关系不大。

当您的程序启动时(即开始执行main 时),默认初始化的数据段(即st[40000][1024] 的40 兆字节)尚未映射到物理内存。虚拟内存地址已被标记为延迟映射到零初始化内存,但这在程序实际尝试引用这些地址之前不会发生。每个这样的引用都需要内核干预来调整虚拟内存映射(以及对物理内存页面进行零初始化)。在该干预之后,该虚拟内存页面被映射,但同一数据段中的任何其他页面都不会被映射。因此,当您传递st 数组时,您将生成大量页面错误,每个页面错误都需要大量时间。您在“静态内存”测试中测量的绝大多数时间都包含这些内核陷阱。

另一方面,第一次调用malloc 将导致标准库初始化内存分配系统。虽然初始化并不太复杂,但它也不是免费的。因此,您在“malloc'ed memory”测试中测量的大部分时间都包括初始化。

为了进行有意义的基准测试,您需要确保在开始测量之前已完成所有惰性初始化。一种方法是在同一个可执行文件中多次执行基准测试,并丢弃第一次(或前几次)重复。

作为说明,我通过在 main 的全部内容周围添加以下循环(除了 return 语句)来简单地修改了您的基准:

for (int reps = 0; reps < 4; ++reps) {
  printf("Repetition %d\n", reps);
  /* Body of main goes here */
}

这导致以下输出:

Repetition 0:
malloc time:9584
static time:26923
Repetition 1:
malloc time:2467
static time:4360
Repetition 2:
malloc time:2463
static time:4332
Repetition 3:
malloc time:2413
static time:4609

注意“热身”迭代(重复 0)中测量的时间与其余时间之间的差异。

这仍然在动态和静态内存测试之间留下了差异。在这里,值得注意的是,这两个测试以不同的方式使用内存。 malloc 测试(可能)在每次迭代中重用相同的缓冲区,因为在您free 一块内存之后,该大小的下一个malloc 可能会立即返回它。另一方面,静态内存测试在整个 40MB 分配中循环。更好的比较是 mallocst 大小相同的缓冲区并使用与静态测试相同的代码循环通过它,或者使 st 成为 1024 个整数的单个向量(如 malloc)和使用与 malloc 测试相同的代码重用它。换句话说,要比较两种可能的方法,请尽量减少两者之间的差异。 (我将把它留作练习。)

如果您做出这些建议的更改,您可能会发现差异会减少为噪音。但有可能会保持一些一致(但很小)的差异,这将反映两个循环之间难以控制的差异的多样性,包括代码和数据的对齐,以及 CPU 缓存的精确细节。正如@seb's answer to this question 中明确阐述的那样,这些细微的差异将属于“巧合”的范畴。 (虽然我认为了解基准测试中可避免的陷阱很重要,但我要强调@seb 在该问题上的建议毫无疑问是正确的。)

【讨论】:

  • 谢谢,当我添加循环并且不释放 malloc 的指针时,我得到这个输出:Repetition 0 malloc time:39767 static time:47314 Repetition 1 malloc time:36651 static time:27431 Repetition 2 malloc time:35223 static time:26704 Repetition 3 malloc time:35417 static time:28719
  • @devin:如果您从不释放任何分配,那么您仍在将苹果与橙子进行比较。 (如果你不进行优化,结果就更没有意义了。)你需要考虑你在测量什么……实际上,你不需要考虑这个。正如 seb 建议的那样,考虑编写好的代码。一旦你搞定了,你就可以考虑压缩 CPU 周期了。
  • malloc 从操作系统获取新页面时,它们通常是“惰性”映射的并且会出现页面错误。我怀疑这里发生的事情是free 没有将内存返回给操作系统,因此它仍然在空闲列表中,因此下一个 malloc 获得了在 L1d 缓存和 TLB 中仍然很热的相同数组。哦,您确实在答案的后半部分这么说。但是你答案的前半部分似乎暗示malloc 返回后,你不会因为触摸内存而出现软页面错误。大分配通常不是这样。
  • 或者你只是说 malloc 的簿记工作在第一次调用后相当便宜。顺便说一句,一些初始化工作是由 glibc 初始化函数(从 _startmain 之前的动态链接器挂钩运行的)完成的,而不是由第一个 malloc 调用完成的。但是,如果 glibc init 没有在空闲列表中留下任何内存,则第一个 malloc 调用可能需要从操作系统向brkmmap 一些新内存。无论如何,看起来您正在对比静态分配 = 页面错误与 mmap = 其他东西,这感觉有误导性。
  • @Peter:我不相信我暗示你不会在 malloc 之后出现页面错误,我当然没有明确地说出来。我暗示如果 malloc 返回与前一次调用相同的内存,您将不会再遇到软页面错误,并且我假设大多数读者会自己解决这个问题。不过,我并没有将静态分配与 mmap 进行对比。我正在评论观察到的两个具体程序的行为。我在答案的第一句话中说比较是没有意义的,但是显然节目的时间不同,所以这个问题是合法的。
【解决方案2】:

巧合不是因果关系。您的 CPU 代码缓存可能非常适合第一个循环(测试 malloc),然后需要将第二个循环从较慢的主内存中提取到较快的代码缓存中,这会影响您的第二个时间。

可能存在类似的数据缓存,这意味着静态对象st 将在整个持续时间内缓存在比malloc 分配的内存更快的内存中(反之亦然)。重点应该放在可能上。没有要求是这种情况。正如您所注意到的,一个比另一个快,这纯属巧合。

您不应该通过滚动您自己的分析器来测试理论瓶颈的速度来执行瓶颈分析,例如确定什么存储持续时间更快,因为这只会导致有利于整个解决方案的混乱过早优化的结论,而不是大部分是干净的代码,可能有点混乱。

相反,您应该专注于使用干净、可维护的代码解决实际问题。一旦你有一个解决实际问题的程序,你应该使用你的分析器来确定它是否足够快,如果不是,你的代码的哪些部分需要优化,这样你就可以希望保持大部分代码干净。

【讨论】:

  • 所有公平点,但这种情况并非巧合,所以我不认为这是一个答案。正如评论中所暗示的,free() 调用意味着在 malloc 情况下将重复使用相同的内存,而另一个循环使用 40 MB 的 bss,将延迟映射到 VM;时间差异可能主要是页面错误和一些改进的缓存使用率。
  • +nci 你错了。巧合的是,具有分配存储持续时间的内存比具有自动存储持续时间的内存“更快”,并且毫无疑问这是一个巧合。巧合的原因是缓存局部性等实现细节,但并不要求这会导致巧合。
  • @Seb 谢谢你的建议,我会尽量用干净、可维护的代码来关注实际问题,然后分析和优化
  • @seb:让我明确一点,我 100% 同意你的建议。尽管如此,我注意到编写好的基准测试并非易事。有许多常见的错误;这就是其中之一。大多数巨大差异的实际原因是在开始基准测试之前未能完全加载可执行映像。这是一个可以避免的错误,它产生的后果,恕我直言,不是“巧合”。请参阅我之前关于将 bss 段延迟映射到虚拟内存的评论。我同意在这种情况下缓存位置可以被认为是“巧合”(甚至是噪音)。
猜你喜欢
  • 2012-11-22
  • 2018-01-02
  • 2011-03-31
  • 2016-05-08
  • 2019-12-07
  • 1970-01-01
  • 1970-01-01
  • 2020-08-24
相关资源
最近更新 更多