直到最近——并且在漫长的等待之后——许多聪明的人才找到了听众,就我们如何编码以及编码什么提出了合理的观点。各种同事多年来一直在敲锣打鼓,试图确保关于编程的明智见解深入人心。本刊和其他出版物上关于编码风格的文章提供了此类倡导的进一步例证。
与许多其他教育努力一样,用于阐明某些观点的例子在很大程度上都是好例子:清晰、具有启发性且易于理解。不幸的是,周末阅读一篇文章所点燃的火焰通常只能持续到星期一早上,届时真实世界的代码出现在屏幕上,并附带一个根本不合理的错误报告——例如,“这甚至不可能发生。”
当我开始编写 Varnish HTTP 加速器时,我的一个设计决策——我认为也是我最好的决策之一——是将我的 OCD 升级为 CDO,这是一种更严重的变体,您坚持字母按字母顺序排列。作为一个实验,我汇集了多年来积累的许多技巧和实践,并在 Varnish 源代码中将它们全部提升到 11。其中一个技巧被称为优秀软件工程的“红发继子”,并因完全错误和过时的原因而被大多数程序员广泛回避。因此,让我尝试用一个例子来使其合法化。
这是一个出乎意料的难题:当 close(2) 失败时,您会怎么做?
是的,close(2) 实际上会返回一个错误代码,但大多数程序员都会忽略它,认为要么:(a)它不会失败;要么(b)如果失败了,您反正也完蛋了,因为显然内核一定有错误。我不认为仅仅忽略它就可以了,因为程序应该始终对报告的错误做一些明智的事情。忽略错误意味着您必须根据它在后续过程中造成的破坏来推断出了什么问题,或者更糟糕的是,一些罪犯稍后会利用您的代码。唯一真正的理想似乎是“保持一致并继续前进”,但在相互连接和交互的现实世界程序中,您必须仔细确定是立即中止程序更好,还是在逆境中继续前进,最终只会走向注定的毁灭。
意识到“我只有一个非常小的脑袋,必须忍受它”,1 必须做出明智的妥协——例如,在失败的可能性和编写代码来处理失败的努力之间进行权衡。代码可读性也确实是一个有效的问题——处理不太可能发生的异常不应在源代码中占据主导地位。
在 Varnish 中,由此产生的折衷方案通常如下所示
AN(vd);
AZ(close(vd->vsm_fd));
AN是一个宏,意思是断言非零,而AZ意思是断言为零,如果条件不成立,程序将立即核心转储。
是的,我想向您推销的“红发继子”是古老的断言,我认为在当今复杂的程序中应该更多地使用它。在我判断失败的可能性相关的地方,我使用了这些宏的另外两个变体,XXXAN和XXXAZ,以表示“这实际上可能会发生,如果发生得太频繁,我们应该更好地处理它。”
retval = strdup(of);
XXXAN(retval);
return(retval);
这种区别也在转储消息中体现出来,对于AZ()是“断言错误” vs.XXXAZ()的“缺少错误处理代码”。
在我想要显式忽略返回值的地方,我会显式地这样做
(void)close(fd);
当然,我也使用“裸”断言来确保没有缓冲区溢出
assert(size < sma->sz);
或者记录代码中的重要假设
assert(sizeof (unsigned short) == 2);
但我们还没有完成。C 程序中一个非常典型的问题是已分配内存的生命周期控制混乱,通常是在将结构体释放回内存池后访问它。
当在 C 中模拟面向对象编程时,被迫通过 void* 指针传递对象,这又打开了一个潘多拉魔盒。以下是我解决这些问题的蛮力方法
struct lru {
unsigned magic;
#define LRU_MAGIC 0x3fec7bb0
...
};
...
struct lru *l;
ALLOC_OBJ(l, LRU_MAGIC);
XXXAN(l);
...
FREE_OBJ(l);
的ALLOC_OBJ和FREE_OBJ宏确保MAGIC字段设置为随机选择的 nonce,当该内存块包含struct lru时,设置为零,当不包含时,设置为零。
在用lru指针调用的代码中,另一个宏检查指针是否指向我们认为它指向的位置
int
foo(struct lru *l)
{
CHECK_OBJ_NOTNULL(l, LRU_MAGIC);
...
如果指针以 void * 形式传入,则宏将其强制转换为所需的类型并断言其有效性
static void *
vwp_main(void *priv)
{
struct vwp *vwp;
CAST_OBJ_NOTNULL(vwp, priv, VWP_MAGIC);
...
就数字而言,Varnish 中 10% 的非注释源代码行受到刚刚显示的断言之一的保护,这还不包括通过宏和内联函数实例化的内容。
所有这些检查在理论上都是多余的,特别是函数 A 在调用函数 B 之前会检查指针的情况,而函数 B 又会再次检查它。
虽然这看起来可能很疯狂,但它是有原因的:这些断言也记录了代码的假设。传统上,该文档出现在注释中:“必须使用指向大于 16 frobozz 的 foobar 的有效指针调用”,等等。注释的问题在于编译器会忽略它们,并且当它们与代码不一致时也不会抱怨;因此,经验丰富的程序员也不信任它们。记录假设以便编译器关注它们是一种更好的策略。所有这些“毫无意义的检查”都让某种性能爱好者抓狂,并且不止一个人试图剥离 Varnish 的所有这些“脂肪”。
如果您尝试使用标准化的 -DNDEBUG 机制,Varnish 根本无法工作。如果您做得更聪明一点,那么您会发现没有相关的差异,甚至在性能上也没有统计学上的显着差异。
断言比以前便宜得多,原因有三
• 编译器变得更加智能,它们的静态分析和优化代码会很乐意删除我的大部分断言,因为它们已经得出结论,这些断言永远不会触发。这很好,因为它意味着我知道我的代码是如何工作的。
• 下一个原因相同,只是反过来:断言对代码施加了约束,静态分析和优化器可以利用这些约束来生成更好的代码。这特别好,因为它意味着我的断言积极地帮助编译器生成更好的代码。
• 最后,可悲的事实是,今天的 CPU 花费大量时间等待从内存中获取数据——同时对缓存中已有的数据执行检查是免费的。我并不声称断言是完全免费的——即使没有其他原因,它们也会浪费一些纳焦耳的电力——但它们不像大多数人认为的那么昂贵,并且它们在程序质量方面提供了非常高的性价比。
从长远来看,您应该不需要像我在 Varnish 中那样大量使用断言,因为归根结底,它们只是用来掩盖编程语言缺陷的 hack。编程的圣杯是“有意的编程”,程序员在其中表达他或她的确切和完整的意图,而编译器理解它。看看今天的编程语言,我仍然看到在进步走得太远之前还有很多时间,我们不再受困于编译器,而是受困于语言。
今天的编译器了解您代码的一些事情,您可能永远不会意识到,因为它们对代码应用了类似国际象棋大师的分析。然而,编程语言并没有成为表达意图的更好工具;事实上,恰恰相反。
过去,您会根据计算机拥有的寄存器大小来选择整数变量的宽度:char、short、int 或 long。但是,如果您不知道 short 和 long 的实际大小,您如何在这两者之间进行选择呢?
答案是您无法选择,因此每个人都对大小做出假设,选择变量类型,并寄希望于最好的结果。我不知道这个特别的错误是如何发生的。如果从一开始基本类型就是 int8、int16、int32 和 int64,我们会处于更好的状态,因为这样程序员就可以声明他们的意图并将优化留给编译器,而不是试图猜测编译器。
一些语言(例如 Ada)的做法有所不同,允许将范围约束作为变量声明的一部分
Month : Integer range 1..12;
这可能是 C 和 C++ 等语言的一个非常平滑且简单的升级,并且将为现代编译器分析提供急需的约束。这种格式的一个特别强大的方面是,您可以在不损失清晰度的情况下节省空间和速度
Door_Height: Integer range 150..400;
这可以舒适地容纳在八位中,并且编译器可以在需要时应用所需的偏移量,而程序员甚至不知道这一点。
然而,与这种意图粒度的提高相反,22 年多的国际标准化产生了<stdint.h>及其uint_least16_t,其中<inttypes.h>贡献了PRIuLEAST16,另一方面<limit.h>与UCHAR_MAX, UINT_MAX, ULONG_MAX,但是,莫名其妙地,USHRT_MAX,这甚至让编写od(1)的 The Open Group 的人都感到困惑。
这种方法有很多问题,我几乎不知道从哪里开始。如果您想探索它,请尝试找出如何可移植地sprintf(3)一个pid_t右对齐到一个八字符的字符串中。
上次我查看时,我们甚至没有找到一种方法来指定协议数据包的确切布局及其字段的字节顺序。但是,嘿,这又不是 CPU 有字节交换指令,或者我们无论如何都会使用打包的协议字段,对吗?
在编程语言赶上来之前,您会发现我在我的源代码中放入以下恐怖的东西,以尝试让我的编译器理解我
#define CTASSERT(x,z) _CTASSERT(x, __LINE__, z)
#define _CTASSERT(x, y, z) __CTASSERT(x, y, z)
#define __CTASSERT(x, y, z) \
typedef char __ct_assert ## y ## __ ## z [(x) ? 1 : -1]
...
CTASSERT(sizeof(struct wfrtc_proto) == 32, \
Struct_wfrtc_proto_has_wrong_size);
1. Dijkstra, E. W. 2010. 将编程视为一种人类活动; http://www.cs.utexas.edu/~EWD/transcriptions/EWD01xx/EWD117.html。
喜欢它,讨厌它?请告诉我们
POUL-HENNING KAMP ([email protected]) 已经编写计算机程序 26 年,并且是 bikeshed.org 背后的灵感来源。他的软件已被广泛采用为开源和商业产品中的“幕后”构建块。他最近的项目是 Varnish HTTP 加速器,它用于加速 Facebook 等大型网站。
© 2012 1542-7730/12/0500 $10.00
最初发表于 Queue vol. 10, no. 5—
在 数字图书馆 中评论本文