提问人:kingkai 提问时间:4/22/2010 最后编辑:Philip Conradkingkai 更新时间:2/10/2022 访问量:65933
为什么 malloc+memset 比 calloc 慢?
Why malloc+memset is slower than calloc?
问:
众所周知,这与它初始化分配的内存不同。使用 ,内存设置为零。使用 ,内存不会被清除。calloc
malloc
calloc
malloc
所以在日常工作中,我视作+。
顺便说一句,为了好玩,我为基准测试编写了以下代码。calloc
malloc
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 中替换为会产生相同的结果。memset
bzero(buf[i],BLOCK_SIZE)
我的问题是:为什么+比?如何做到这一点?malloc
memset
calloc
calloc
答:
因为在许多系统上,在空闲的处理时间里,操作系统会自行将可用内存设置为零,并将其标记为安全,因此当您调用 时,它可能已经有空闲的零内存可以为您提供。calloc()
calloc()
评论
calloc()
calloc()
在某些平台上,在某些模式下,malloc 在返回内存之前会将内存初始化为某个通常为非零的值,因此第二个版本可以很好地初始化内存两次
评论
malloc()
不初始化 RAM。它为您提供了一个指向新分配的 RAM 的指针,其中包含之前的任何垃圾数据。 将其分配并初始化为 0。calloc()
简短版本:始终使用而不是 .在大多数情况下,它们是相同的。在某些情况下,会做更少的工作,因为它可以完全跳过。在其他情况下,甚至可以作弊而不分配任何内存!但是,将始终完成全部工作。calloc()
malloc()+memset()
calloc()
memset()
calloc()
malloc()+memset()
要了解这一点,需要对内存系统进行简短的浏览。
快速浏览内存
这里有四个主要部分:程序、标准库、内核和页表。你已经知道你的程序了,所以......
内存分配器喜欢并且主要用于进行小分配(从 1 字节到 100 KB 的任何内容)并将它们分组到更大的内存池中。例如,如果您分配了 16 个字节,将首先尝试从其中一个池中获取 16 个字节,然后在池干涸时从内核请求更多内存。但是,由于您询问的程序一次分配了大量内存,并且只会直接从内核请求该内存。此行为的阈值取决于您的系统,但我已经看到 1 MiB 用作阈值。malloc()
calloc()
malloc()
malloc()
calloc()
内核负责为每个进程分配实际的 RAM,并确保这些进程不会干扰其他进程的内存。这被称为内存保护,自 1990 年代以来它一直很常见,这就是为什么一个程序可以在不使整个系统瘫痪的情况下崩溃的原因。因此,当程序需要更多内存时,它不能只占用内存,而是使用系统调用(如 或)从内核请求内存。内核将通过修改页表为每个进程提供 RAM。mmap()
sbrk()
页表将内存地址映射到实际的物理 RAM。进程的地址(0x00000000 32 位系统上的0xFFFFFFFF)不是实际内存,而是虚拟内存中的地址。处理器将这些地址划分为 4 KiB 页,通过修改页表,可以将每个页分配给不同的物理 RAM。只允许内核修改页表。
它如何不起作用
以下是分配 256 MiB 不起作用的原因:
您的进程调用并要求 256 MiB。
calloc()
标准库调用并要求 256 MiB。
mmap()
内核找到 256 MiB 未使用的 RAM,并通过修改页表将其提供给您的进程。
标准库将 RAM 归零,并从 返回。
memset()
calloc()
您的进程最终会退出,内核会回收 RAM,以便它可以被另一个进程使用。
它是如何工作的
上述过程会起作用,但它不会以这种方式发生。有三个主要区别。
当您的进程从内核获取新内存时,该内存可能以前被其他进程使用过。这是一个安全风险。如果该内存有密码、加密密钥或秘密莎莎酱食谱怎么办?为了防止敏感数据泄露,内核总是在将内存提供给进程之前清理内存。我们不妨通过清零来擦除内存,如果新内存归零,我们不妨将其作为保证,这样可以保证它返回的新内存始终归零。
mmap()
有很多程序会分配内存,但不会立即使用内存。有时内存被分配,但从未使用过。内核知道这一点,并且是懒惰的。当您分配新内存时,内核根本不会触及页表,也不会为您的进程提供任何 RAM。相反,它会在您的进程中找到一些地址空间,记下应该去那里的内容,并承诺如果您的程序实际使用它,它将把 RAM 放在那里。当您的程序尝试从这些地址读取或写入时,处理器会触发页面错误,内核会介入为这些地址分配 RAM 并恢复您的程序。如果您从不使用内存,则页面错误永远不会发生,并且您的程序永远不会真正获得 RAM。
某些进程分配内存,然后从中读取内存而不对其进行修改。这意味着,跨不同进程的内存中的许多页面可能会填充从 返回的原始零。由于这些页面都是相同的,因此内核使所有这些虚拟地址都指向一个充满零的共享 4 KiB 内存页面。如果您尝试写入该内存,处理器会触发另一个页面错误,内核会介入,为您提供一个不与任何其他程序共享的新零页。
mmap()
最终过程看起来更像是这样:
您的进程调用并要求 256 MiB。
calloc()
标准库调用并要求 256 MiB。
mmap()
内核找到 256 MiB 未使用的地址空间,记下该地址空间现在的用途,然后返回。
标准库知道 的结果总是用零填充(或者一旦它实际获得一些 RAM 就会被填满),所以它不会触及内存,所以没有页面错误,并且 RAM 永远不会提供给你的进程。
mmap()
您的进程最终会退出,内核不需要回收 RAM,因为它从一开始就没有被分配过。
如果您使用将页面归零,将触发页面错误,导致 RAM 被分配,然后将其归零,即使它已经填充了零。这是一项巨大的额外工作,并解释了为什么比 和 快。如果你最终还是使用内存,仍然比 和 快,但差异并不那么荒谬。memset()
memset()
calloc()
malloc()
memset()
calloc()
malloc()
memset()
这并不总是有效
并非所有系统都具有分页虚拟内存,因此并非所有系统都可以使用这些优化。这适用于非常旧的处理器,如 80286 以及嵌入式处理器,这些处理器对于复杂的内存管理单元来说太小了。
这也并不总是适用于较小的分配。使用较小的分配,从共享池获取内存,而不是直接进入内核。通常,共享池中可能存储了使用和释放的旧内存中的垃圾数据,因此可以获取该内存并调用以清除它。常见实现将跟踪共享池的哪些部分是原始的,并且仍然填充零,但并非所有实现都这样做。calloc()
free()
calloc()
memset()
消除一些错误的答案
根据操作系统的不同,内核在其空闲时间内可能会也可能不会将内存归零,以防您以后需要获得一些归零内存。Linux 不会提前将内存归零,Dragonfly BSD 最近也从他们的内核中删除了这个功能。但是,其他一些内核会提前执行零内存操作。无论如何,在空闲期间将页面归零并不足以解释巨大的性能差异。
该函数没有使用一些特殊的内存对齐版本,无论如何这都不会使它更快。现代处理器的大多数实现看起来有点像这样: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()
将已归零的内存归零这一事实确实意味着内存归零两次,但这只能解释 2 倍的性能差异。这里的性能差异要大得多(我在系统上测量了 和 之间的三个数量级以上)。memset()
malloc()+memset()
calloc()
派对技巧
不要循环 10 次,而是编写一个程序来分配内存,直到或返回 NULL。malloc()
calloc()
如果添加会发生什么?memset()
评论
mmap()
malloc()
calloc()
calloc()
mmap()
calloc
large_int * large_int
calloc(large_int, large_int)
NULL
malloc(large_int * large_int)
评论
buf[i] = (char*)memset(malloc(BLOCK_SIZE),0,BLOCK_SIZE);