有一次吃饭,跟同事聊某个项目的代码实现,同事讲里面有太多的malloc与memset(bzero),对性能的影响比较大。
而在平常我们在程序中,申请内存后要初始化,防止使用未经初始化的内存导致不可预知的结果,否则在系统长时间运行后,可能新申请的这段内存里面的数据可能是杂乱的。对大段内存进行 memset操作的话,其实是很耗费性能的。
** memset置零的性能比较**
是不是平常经常使用 memset或者bzero的你完全没有感受到这个问题,如果你使用的内存在小于1024字节左右,那么你几乎感受不到它的性能问题; 但如果是操作的内存大小在 1M字节以上,那性能影响就很明显了。
#include <stdio.h>
#include <sys/time.h>
#include <string.h>
#define TIMEDIFF(s, e) (((e.tv_sec)-(s.tv_sec))*1000000 + (e.tv_usec) - (s.tv_usec))
int main(int argc, char **argv){
struct timeval ts, te;
char num_1K[1024], num_1M[1024*1024], num_4M[1024*1024*4];
int i ;
gettimeofday(&ts, NULL);
memset(num_1K, 0, sizeof(num_1K));
gettimeofday(&te, NULL);
printf("memset 1k: %d\n", TIMEDIFF(ts, te));
gettimeofday(&ts, NULL);
memset(num_1M, 0, sizeof(num_1M));
gettimeofday(&te, NULL);
printf("memset 1M: %d\n", TIMEDIFF(ts, te));
gettimeofday(&ts, NULL);
memset(num_4M, 0, sizeof(num_4M));
gettimeofday(&te, NULL);
printf("memset 4M: %d\n", TIMEDIFF(ts, te));
gettimeofday(&ts, NULL);
for(i=0; i<sizeof(num_1K); ++i)
num_1K[i]=0;
gettimeofday(&te, NULL);
printf("for 1k: %d\n", TIMEDIFF(ts, te));
gettimeofday(&ts, NULL);
for(i=0; i<sizeof(num_1M); ++i)
num_1M[i]=0;
gettimeofday(&te, NULL);
printf("for 1M: %d\n", TIMEDIFF(ts, te));
gettimeofday(&ts, NULL);
for(i=0; i<sizeof(num_4M); ++i)
num_4M[i]=0;
gettimeofday(&te, NULL);
printf("for 4M: %d\n", TIMEDIFF(ts, te));
return 0;
}
编译输出,以下的运行结果的数值单位是微秒(gettimeofday的默认单位)。
运行的结果基本上是,在数组较小的情况下,bzero的效率比memset高;当数组超过一定大小之后,bzero的效率开始比memset低;数组越 大,memset的性能优势越明显。而在数组较小的情况下,memset的性能甚至不如直接for循环对数组中的每一个字节置零的方法。
如果你在代码里循环调用memset,或者每次流程处理都要调用memset,而且每次都要memset 1M以上,那么你就要小心了,你可以考虑用其它替代方法哈。如果memset 1k以下,次数也不多,则可以用。
ltrace调试memset
ltrace命令是用来跟踪进程调用库函数的情况,ltrace其实也是基于ptrace。我们知道,ptrace能够主要是用来跟踪系统调用,那么它是如何跟踪库函数呢?可以查看一下ltrace -h
yum -y install ltrace
来看看个例子,在栈内存中申请。
#include <stdio.h>
int main(int argc, char **argv){
char buffer[1024 * 1024] = {0};
return 0;
}
但就是这么一行简单的代码,却隐藏了一个陷阱:初始化的时候调用了memset。我们用ltrace工具来看看:
是不是疑惑,全部都初始化为0了,当然慢了,如果我只初始化第一个元素,是不是会没有这个问题呢?在看看只初始化一个元素。
#include <stdio.h>
int main(int argc, char **argv){
char buffer[1024 * 1024] = {0,};
return 0;
}
从上面的ltrace输出可以看出,不管你是全部设置为0,还是只设置第一个,都会调用memset。不管char buffer[1024 * 8] = {}里面是什么内容,都会调用memset,将所有内存设置为0。 . 还有一个问题就是,数组的大小小于char buffer[1024 * 8] = {0}时不会调用memset。
#include <stdio.h>
int main(int argc, char **argv){
char buffer[1024*8] = {0};
return 0;
}
各种赋值的情况我们已经验证过了,当然也要验证一下不赋值的情况,下面是验证情况:
#include <stdio.h>
int main(int argc, char **argv){
char buffer[1024*1024];
return 0;
}
建议大家在编写代码的时候不要用char buffer[1024 * 1024] = XXX;这种方式,而要使用char buffer[1024 * 1024];除非确定一定要memset。
总结
如果 memset的内存大小在1024字节以下,仅仅是在申请内存的时候使用,那就没有什么性能和效率上的问题。 对于那些我们申请的结构体,一般C语言的做法就是申请完就使用 memset进行操作。但是对于 C++来说,可以定义默认构造函数来完成初始化,这样既可以任意指定结构体中变量的初始化值,也可以减少 memset的使用。