标签 C 下的文章

四谈内存对齐

最近在思考改进项目中一模块的实现,该模块维护起来让我很是头疼,所有才有了整体换掉它的想法。设计和实现中利用了内存对齐的技术。关于内存对齐,我曾经写过三篇文章,第一篇介绍了计算内存对齐的方法和例子,第二篇说了一个内存对齐的应用;三谈内存对齐时,则从其本质上做了阐述,而这次实际上是继续在其本质上的做文章,结合本质谈谈为什么内存对齐的计算方法就应该是第一篇中所讲的那两条。

如果对内存对齐本质还不清楚的话,就看看我的内存对齐系列的第三篇吧。如果你清楚了本质,那么我们结合第一篇中交待的内存对齐计算方法来进一步探究,为什么计算的方法就是这个样子的。

再理解一下对齐系数/模数,众所周知Alignment module反映了CPU 取数据时对数据起始地址的限制-这个地址值必须能被Alignment module所整除,但继续仔细考虑下去,你会想到CPU在下一次取数据依然要从下一个能被Alignment module所整除的地址的地方去取,这显而易见,又能说明什么呢?如果说CPU第一次取数据的地址是first_read_address,那么连续下一次的地址就应该是first_read_address + Alignment module了,也就是说每次取数据的量就是Alignment module这么多,这样通过Alignment module我们又可以知道一个量,那就是:在Alignement module限制下,CPU一次能取Alignment module个字节;在“Data alignment: Straighten up and fly right”一文中作者也称之为"memory access granularity"。从应用层开发人员的角度理解:被访问变量的长度,就是CPU要去读取的字节数;而对齐系数又是限制CPU读取字节数的一个指标,有了这两个理解,解决下面的疑问就有了基础了。

在'也谈内存对齐'一篇中介绍了内存对齐的计算方法,这里不妨再引用一下:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

疑问:对于数据成员对齐规则:为什么每个成员的对齐都要按照Min(指定对齐模数,数据成员自身长度)来确定呢?为了不用Max(指定对齐模数,数据成员自身长度)呢,用Max值对齐的不更加完美么?同样对于结构的整体对齐规则也一样有此疑问。这里我们还是举个例子更加直观:
#pragma pack(8)
struct Foo {
    char    a;
    int    b;
    short    c;
};
#pragma pack()
我们先来看数据成员对齐,以b为例子,按照规则的说法,sizeof(b) = 4 < 8,那么Address_of_b = Start_Address_of_Foo + 4; 我们来看看当应用的代码里访问b的时候,CPU做了些什么?Address_of_b一半情况下是不能被8整除的,在不能被8整除的情况下,我们去访问b,这里我要提两个问题:
1) 访问b的时候是否会因内存没有对齐到8上而触发core呢?(在Sun SPARC上因访问未对齐地址上的变量时会出core)
2) 为什么不将b放到Start_Address_of_Foo + 8这个地址上呢?

下面一一说说我的理解:
根据前面所说,程序在访问b的时候,CPU实际不一定是从Address_of_b这个地址上开始读取的。如果b这个地址恰巧既能被4整除,也能被8整除(如地址24),那就无可厚非了。但是如果这个地址只能被4整除,而不能被8整除(如地址12),那么此时CPU读取的地址肯定是从Address_of_b – 4开始读取8个字节的,也就是说实际上CPU都是从能被8整除的地址上读取的,而且一次读了8个字节,b所在的位置恰是这个8个字节中的后4个字节,所以不存在触发core的可能。

第二个问题,sizeof(b) = 4 < 8,为什么就要按照4而不是按照8去安排b的地址呢?我们不妨按照8去给b分配地址,Address_of_b' = Start_Address_of_Foo + 8,这样的话CPU也能一次将b读取,而且是从b的起始地址开始读,似乎更完美。但你看出问题了么?这么做浪费的空间显然大了很多。将b安排在Address_of_b'比安排在Address_of_b多浪费了一半空间。

同样整体对齐原则也是同样的道理。内存对齐计算显然有两个目标:一是减少CPU的访存次数;第二个就是还要保持存储空间的效率足够高。

代码评审·CC2e·断言·其它

一口气读了七章"Code Complete 2nd(以下称CC2e)"中的内容,从第七章的"高质量的子程序"到第十三章的"不常见的数据类型"。之所以一口气读这么多,是因为被其中的内容吸引了。这两天的下午一直在做代码评审,所以晚上看CC2e的时候,思维不停的在项目代码和书中内容之间跳转。一直把"代码大全2nd"当作一门百科全书式的手册类图书,买回来后一直陈放在书架上没有问津。直到今天在考虑一个关于断言使用的问题时,才想起来去查查这本百科全书,想看看书中是如何阐述断言的。于是便拿起了这本书。

细致的看了2页后才发现,这本书真是很棒啊。其实我们在平时编码过程中遇到的问题在书中基本都覆盖到了,而且说的很到位。如果你能提前看看书中的陈述,想必你在开发过程中会少走很多弯路。这里列举几点,也是我们项目代码里处理得不到位的地方。

断言 vs.错误处理
在昨天评审代码时,发现一位同事在整个模块代码中对接口参数的防御性代码都很不得当;对于内部接口调用,他对可信赖的参数使用了错误处理的方式,如果参数值未按预期,代码直接返回此次调用的结果状态了。关于到底用断言还是用错误处理,估计很多人都很迷惑。'代码大全'里的总结是我见到的最清晰的了。对于来自系统外部数据(包括数据库、文件、网络等)的校验,我们采用错误处理的方式;对于内部接口之间的参数传递,断言更适合。另外对断言而言,断言失败意味着代码中的bug,你应该做的是停止程序,定位问题,重新编译、发布和启动程序。另外'代码大全'中引入了'Barricade'的概念。它建议你在代码中建立所谓的"安全区",安全区外的数据想通过安全区必须通过严格的合法性检查,当非法时给予敏锐的反应;安全区以内将假定数据都是干净的、安全的。而实际上这个"安全区"在项目里很有可能就是一组函数接口,这组接口采用错误处理的方式对待安全区外的数据,保证非法输入不流入内部系统。

断言是否放入产品代码
关于这个问题,更多人坚守的是"断言一定不能出现在发行版中",即一般的看法:断言只是在开发阶段帮助程序员定位bug的工具,Release阶段断言语句将从代码中自动去除。CC2e作者在书中似乎(也许是我没有看到?)并没有肯定的支持这种观点,只是在其"Guidelines for using assertions"一节中委婉的表达出了一个建议:在生成产品代码时,可以不把断言编译进目标代码里去,以免降低系统性能。在另一本大作"编程珠玑2nd"中,作者Jon Bentley间接引述了Tony Hoare的一个观点:"在测试时使用断言,而在产品发布时将断言关闭的程序员,就像是在岸上操练时穿着救生衣,而下海时将救生衣脱掉的水手",观点不言而明。在当今硬件设备性能已经很好的年代,断言产生的那些开销多数情况下已经"不足挂齿"。那什么才是决定断言是否继续留在产品代码中的最大影响因素呢?我觉得还是因产品而异。前面说过断言如果出现,就意味着程序里是存在bug的。那我们是尽快让产品bug暴露出来呢?还是在程序已经伤痕累累的情况下,继续让其前行呢?产品继续运行带来的后果是否是可忍受的呢?说来说去,还是一个评估和决策的过程。对于类似对癌症病人做化疗的控制软件来说,如果运行异常,想必及早关闭程序是最好的选择,这可是性命攸关的大事。那对于这类程序,断言是一个更好的辅助提前检查到bug的工具,加入Release版也无妨。在我所在的项目产品中,我们选择了将断言保留在产品代码中,我们希望bug能越早暴露越好,千万不能让系统带着缺陷持续运行下去,这样到后期系统将会出现莫名其妙的甚至无法跟踪定位的问题了,到那时可真就不好与客户交代了。当然断言失败后的处理方式也是多种多样的,做好适当记录,保留好现场轨迹,让子进程稳妥地崩溃退出,有时是可以接受的。

函数 vs.过程
说到这个话题,不免会有些"钻牛角尖"的感觉。使用C/C++的人已经习惯了"函数"的这个称谓,少有人去特意区分函数与过程的差异,或者在工作中花心思去考虑到底应该用函数还是过程。在C/C++甚至很多其他现代语言中,函数与过程没有语法上的差异,如果说有什么不同,那就要从纯语义上去区分。我们从小就开始学数学,大约在初中(有些地区在小学的高年级,有否?)开始了函数的学习。回忆一下数学上的函数是什么样子的,多亏手头上有一本"什么是数学",翻看了一下,数学上的函数大致是这样定义的:"对于变量X的任何一个值,都存在另一个变量U的确定的值与它相联系,这时U就称作是X的函数,记为U = F(X),其中X是自变量,U是因变量"。数学上有很多著名且常见的函数,诸如sin(x)、cos(x)等。按照数学上对函数的理解,一个函数有输入变量(参数),有唯一的因变量(返回值),函数名字根据返回值的含义而命名,如sin(x)。对比一下我们平时在编码中设计的函数,发现似乎不那么一样。因为我们没有严格按照数学上理解的函数去定义我们的函数原型。但在我们开发过程中也不乏符合数学上理解的函数,如标准库中math.h中,诸如:
double sin (double x);
double cos (double x);
double tan (double x);

与函数不同,严格意义上的过程应该是一个没有返回值,但接受任意数量输入、修改和输出参数的。这样综合起来,其实我们在平时更多的是在使用纯意义上函数和过程的综合体 — 带返回值的过程,且命名偏向过程。比如strtol。以下应该是我们常见的两种代码里routine的使用方式,第二种则是一个标准的过程的调用。
invoke_status = sub_routine(var_1, var_2, …, var_n);
sub_routine(var_1, var_2, …, var_n, &invoke_status);  

使用以上哪一种是见仁见智的事情,估计也和组织的编码风格有关系。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats