首页 文章

为什么malloc memset比calloc慢?

提问于
浏览
226

众所周知 callocmalloc 的不同之处在于它初始化分配的内存 . 使用 calloc ,内存设置为零 . 使用 malloc 时,内存不会被清除 .

所以在日常工作中,我认为 callocmalloc memset . 顺便说一下,为了好玩,我为基准编写了以下代码 .

结果令人困惑 .

代码1:

#include<stdio.h>
#include<stdlib.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)calloc(1,BLOCK_SIZE);
                i++;
        }
}

代码1的输出:

time ./a.out  
**real 0m0.287s**  
user 0m0.095s  
sys 0m0.192s

代码2:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)malloc(BLOCK_SIZE);
                memset(buf[i],'\0',BLOCK_SIZE);
                i++;
        }
}

代码2的输出:

time ./a.out   
**real 0m2.693s**  
user 0m0.973s  
sys 0m1.721s

在代码2中用 bzero(buf[i],BLOCK_SIZE) 替换 memset 会产生相同的结果 .

My question is: 为什么 malloc memsetcalloc 慢得多? calloc 怎么做?

3 回答

  • 0

    简短版本:始终使用 calloc() 而不是 malloc()+memset() . 在大多数情况下,它们将是相同的 . 在某些情况下, calloc() 会减少工作量,因为它可以完全跳过 memset() . 在其他情况下, calloc() 甚至可以作弊而不分配任何内存!但是, malloc()+memset() 将始终完成全部工作 .

    理解这一点需要对内存系统进行简短的浏览 .

    快速浏览内存

    这里有四个主要部分:程序,标准库,内核和页表 . 你已经了解你的程序,所以......

    malloc()calloc() 这样的内存分配器主要用于获取小的分配(从1字节到100的KB),并将它们分组到更大的内存池中 . 例如,如果您分配16个字节, malloc() 将首先尝试从其中一个池中获取16个字节,然后在池运行时从内核请求更多内存 . 但是,由于您要问的程序是一次分配大量内存, malloc()calloc() 将直接从内核请求该内存 . 此行为的阈值取决于您的系统,但我已经看到1 MiB用作阈值 .

    内核负责为每个进程分配实际的RAM,并确保进程不会干扰其他进程的内存 . 这称为内存保护,自20世纪90年代以来一直很常见,它只是占用内存,而是使用系统调用(如 mmap()sbrk() )从内核请求内存 . 内核将通过修改页表为每个进程提供RAM .

    页表将内存地址映射到实际物理RAM . 您的进程's addresses, 0x00000000 to 0xFFFFFFFF on a 32-bit system, aren' t真实内存,而是虚拟内存中的地址 . 处理器将这些地址划分为4个KiB页面,并且可以通过修改页面表将每个页面分配给不同的物理RAM . 只允许内核修改页表 .

    它怎么行不通

    以下是分配256 MiB不起作用的方法:

    • 您的进程调用 calloc() 并要求256 MiB .

    • 标准库调用 mmap() 并要求256 MiB .

    • 内核找到256 MiB未使用的RAM,并通过修改页表将其提供给您的进程 .

    • 标准库使用 memset() 将RAM归零,并从 calloc() 返回 .

    • 您的进程最终退出,内核回收RAM,以便其他进程可以使用它 .

    它是如何运作的

    上面的过程可行,但它不会以这种方式发生 . 有三个主要差异 .

    • 当您的进程从内核获取新内存时,该内存可能以前被其他一些进程使用 . 这是一种安全风险 . 如果该内存有密码,加密密钥或秘密莎莎食谱怎么办?为了防止敏感数据泄漏,内核总是在将内存提供给进程之前擦除内存 . 我们不妨通过归零来擦除内存,如果新内存归零,我们也可以将其作为保证,因此 mmap() 保证它返回的新内存始终为零 .

    • 有很多程序可以分配内存,但根本不需要触摸页面表,也不会为你的进程提供任何内存 . 相反,它会在你的进程中找到一些地址空间,记下应该去的地方,并承诺如果你的程序实际使用它,它会把RAM放在那里 . 当程序尝试从这些地址读取或写入时,处理器会触发页面错误,内核会将RAM分配给这些地址并恢复程序 . 如果你从不使用内存,页面错误永远不会发生,程序永远不会真正获得内存 .

    • 某些进程分配内存,然后从中读取而不进行修改 . 这意味着不同进程的内存中的很多页面可能会填充从 mmap() 返回的原始零 . 由于这些页面都是相同的,因此内核使所有这些虚拟地址指向一个用零填充的单个共享4 KiB内存页面 . 如果您尝试写入该内存,处理器会触发另一个页面错误,内核会介入,为您提供一个新的页面,这些零点不与任何其他内容共享程式 .

    最后的过程看起来更像是这样的:

    • 您的进程调用 calloc() 并要求256 MiB .

    • 标准库调用 mmap() 并要求256 MiB .

    • 内核找到256 MiB的未使用地址空间,记下现在使用的地址空间,然后返回 .

    • 标准库知道 mmap() 的结果总是用零填充(或者一旦它实际上得到一些RAM),所以它不会触及内存,因此没有页面错误,并且RAM永远不会被赋予你的过程 .

    • 您的进程最终退出,内核不需要回收RAM,因为它从未首先分配过 .

    如果使用 memset() 将页面归零, memset() 将触发页面错误,导致RAM被分配,然后将其归零,即使它已经填充了零 . 这是一项巨大的额外工作,并解释了为什么 calloc()malloc()memset() 更快 . 如果最终仍然使用内存, calloc() 仍然比 malloc()memset() 更快,但差别并不是那么荒谬 .


    这并不总是有效

    并非所有系统都有分页虚拟内存,因此并非所有系统都可以使用这些优化 . 这适用于非常古老的处理器,如80286以及嵌入式处理器,这些处理器对于复杂的内存管理单元来说太小了 .

    这也不总是适用于较小的分配 . 使用较小的分配, calloc() 从共享池获取内存而不是直接进入内核 . 通常,共享池可能有旧存储器中存储的垃圾数据,该存储器使用并通过 free() 释放,因此 calloc() 可以占用该存储器并调用 memset() 清除它 . 常见的实现将跟踪共享池的哪些部分是原始的并且仍然用零填充,但并非所有实现都这样做 .

    消除一些错误的答案

    根据操作系统的不同,内核在空闲时间内可能会或可能不会将内存归零,以防您以后需要获取一些归零内存 . Linux并没有提前将内存零和Dragonfly BSD recently also removed this feature from their kernel . 但是,其他一些内核会提前做零内存 . 无论如何,空闲的归零页面还不足以解释大的性能差异 .

    calloc() 函数没有使用 memset() 的某些特殊的内存对齐版本,但这无论如何都不会更快 . 现代处理器的大多数 memset() 实现看起来像这样:

    function memset(dest, c, len)
        // one byte at a time, until the dest is aligned...
        while (len > 0 && ((unsigned int)dest & 15))
            *dest++ = c
            len -= 1
        // now write big chunks at a time (processor-specific)...
        // block size might not be 16, it's just pseudocode
        while (len >= 16)
            // some optimized vector code goes here
            // glibc uses SSE2 when available
            dest += 16
            len -= 16
        // the end is not aligned, so one byte at a time
        while (len > 0)
            *dest++ = c
            len -= 1
    

    所以你可以看到, memset() 非常快,你不会真正为大块内存获得更好的东西 .

    事实上, memset() 将已归零的内存归零,这意味着内存被归零两次,但这只能解释2倍的性能差异 . 这里的性能差异要大得多(我在 malloc()+memset()calloc() 之间测量了我的系统超过三个数量级) .

    党的把戏

    编写一个分配内存的程序,直到 malloc()calloc() 返回NULL,而不是循环10次 .

    如果你添加 memset() 会怎么样?

  • 11

    因为在许多系统上,在备用处理时间内,操作系统会将自由内存设置为零,并将其标记为安全 calloc() ,因此当您调用 calloc() 时,它可能已经有空闲的零内存给您 .

  • 396

    在某些模式的某些平台上,malloc在返回之前将内存初始化为某些通常为非零的值,因此第二个版本可以很好地初始化内存两次

相关问题