为什么 malloc 在我的测试程序中比静态内存分配更快?

Why malloc is faster than static memory allocation in my test program?

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

malloc time:9571

static time: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;
}

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

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

您不应该通过滚动自己的分析器来测试理论瓶颈的速度来执行瓶颈分析,例如确定更快的存储持续时间,因为这只能得出有利于整个解决方案过早优化的结论,因为反对大多数干净的代码,也许有点混乱。

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

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

当你的程序启动时(即开始执行main时),default-initialized数据段(即st[40000][1024]的40兆字节)还没有被映射到物理内存中。虚拟内存地址已被标记为延迟映射到 zero-initialized 内存,但这不会发生,直到程序实际尝试引用这些地址。每个这样的引用都需要内核干预来调整虚拟内存映射(以及 zero-initializing 物理内存页面)。在那次干预之后,虚拟内存的那个页面被映射,但同一数据段中的任何其他页面都没有。因此,当您传递 st 数组时,您将产生大量页面错误,每个页面错误都会花费大量时间。您在 "static memory" 测试中测量的绝大部分时间都由这些内核陷阱组成。

另一方面,第一次调用 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

注意在 "warm-up" 次迭代(重复 0)和其余次数中测量的时间之间的差异。

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

如果您进行这些建议的更改,您可能会发现差异会减少到噪音。但是可能会保留一些一致的(但很小的)差异,这将反映出 hard-to-control 两个循环之间差异的多样性,包括代码和数据的对齐方式,以及 [=49] 的精确细节=]缓存。正如 中巧妙阐述的那样,这些细微差别属于 "coincidence" 的范畴。 (虽然我认为了解基准测试中可避免的陷阱很重要,但我要强调@seb 在该问题中的建议无疑是正确的。)