有时我们希望要将一个结构对象清零。此时我们常用的方法是调用memset。来看一下代码
#include <cstring> struct pod_struct { #ifdef BIG_POD_STRUCT int a[128]; #else int a; int b; #endif }; int main() { pod_struct pod; std::memset(&pod, 0, sizeof(pod)); }
BIG_POD_STRUCT的定义在后续中会用到。我们知道,memset其实是有开销的,对于比较小的内存块,使用简单的赋值可能更快。因为memset需要根据目标和源地址计算对齐属性,选择最佳的一种方案进行copy。
pod.a = 0; pod.b = 0;
这里简单地赋值,效率其实更高。我们来简单做一下对比。分别使用memset和赋值的方法,迭代1000, 0000次,计算总时间。
#include <cstring> #include <clock.h> struct pod_struct { #ifdef BIG_POD_STRUCT int a[128]; #else int a; int b; #endif }; int main() { using namespace my_util; const int COUNT = 10000000; pod_struct pod; SCOPED_CLOCK_BEGIN() for (int i = 0; i < COUNT; ++i) { std::memset(&pod, 0, sizeof(pod)); } SCOPED_CLOCK_END() SCOPED_CLOCK_BEGIN() for (int i = 0; i < COUNT; ++i) { pod.a = 0; pod.b = 0; } SCOPED_CLOCK_END() }
这里的scoped_clock_*宏只是用来测量时间的,实现使用clock()函数完成的,可以精确到毫秒。测量的结果是
memset | assignment | |
time(ms) | 109 | 31 |
很显然,用赋值的方法所用的cpu时间更短。由此我们想到能不能有什么方法可以根据对象的大小来选择这两种方法呢?其实编译器可以帮我们做这些事情。
使用一个initializer对一个pod对象进行初始化,就可以很简单地把一个对象清零。有时我们会这么把数组进行清零。
int a[100] = {0};
这里,第一个元素初始化为0,剩余元素自动被编译器清零。其实更简单的方法是,连0都不指定,直接使用一个空的初始化列表。标准规定,
If there are fewer initializers in the list than there are members in the aggregate, then each member not
explicitly initialized shall be value-initialized (8.5).
value-initialized对于内建的类型而言,就是zero-initialized,也就是简单地清零。所以,我们可以这么初始化pod_struct
pod_struct pod = {};
使用这个结构,编译器会根据目标对象的大小选择最佳的初始化方法,即使用memset或者赋值。来看一下编译器在BIG_POD_STRUCT打开/关闭情况下生存的汇编代码就一目了然了(VC10)。
; BIG_POD_STRUCT not defined xor eax,eax mov dword ptr [pod],eax mov dword ptr [ebp-8],eax
这里编译器把eax的值赋值给了[pod],也就是pod_struct.a,之后再赋值给了pod_struct.b。再看一下BIG_POD_STRUCT打开后的代码,
; BIG_POD_STRUCT defined push 200h push 0 lea eax,[pod] push eax call @ILT+440(_memset)
这里编译器调用了memset,而由于memset是__cdecl函数,所以参数压栈从右到左。size=200H,value=0,dest=&pod,分别被压栈后调用memset。200H正好是pod_struct的大小。
我测试了一下编译器会将赋值替换为memset实现的阈值,在我的机器上是40个字节,VC就会使用memset对pod_struct进行清零。