软件随想录(local.joelonsoftware.com/wiki)-2002年12月11日 回归原点 - Back to Basics

本文主要是介绍软件随想录(local.joelonsoftware.com/wiki)-2002年12月11日 回归原点 - Back to Basics,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

2002年12月11日 回归原点 - Back to Basics

 

The Joel on Software Translation Project:回归原点

From The Joel on Software Translation Project

Jump to: navigation, search

回归原点

作者:周思博 (Joel Spolsky)

译:Paul May 梅普华

Tuesday, December 11, 2001

属于Joel on Software, http://www.joelonsoftware.com


我们在这个站花了很多时间讨论让人兴奋的大局概念,像是.NET对Java、XML策略、锁住客户、竞争策略、软件设计、架构等等。这所有的概念就某方面来看就像是个夹心蛋糕。最上层有软件策略。再下来可以想想.NET之类的架构,然后再下面是个别的产品:像Java之类的软件开发产品或Windows之类的平台。

请再向蛋糕下层挖下去。是DLL吗?还是对象或是函数呢?都不是!再下去!下到某个地方就会想到用程序语言撰写,一行又一行的程序。

不过还不够深。今天我想谈的CPU。一片把字节们搬来搬去的小硅晶片。先假装你是个新手程序员,把你所有的程序设计和软件还有管理知识都丢掉,然后回到最底层的冯纽曼的基本结构。暂时先把J2EE由你脑中拿掉,来想想字节。

vancouver-dam.jpg

为什么要这样做呢?我认为人们所犯的某些大错虽然错在最高的架构层,但其实是因为不够了解或想错某些在最底层很简单的事情。你可能建了一座雄伟的宫殿,可是地基却是一塌糊涂。本来应该用好的水泥砖,结果却用了碎石头。所以宫殿看起来虽然华丽,可是浴缸却时常移位,而你根本不知道怎么回事。

所以今天就来个深呼吸。请跟著我来做个用C语言进行的小小练习吧。

记得C的字串用法吧:它们是由一串字节后面接一个null字符(值为0)所组成。这里有两个明显的暗示:

  1. 必须整个字串走一遍找到结尾的null字符,才能知道字串在哪里结束(也就是说字串的长度)。
  1. 字串里不能有任何零。所以你不能用C字串来储存JPEG图片之类的二进制大对象。


为什么C字串要这样运作呢?因为发明UNIX和C语言时用的PDP-7微处理器有一种ASCIZ字串类型。ASCIZ意思是「用Z(零)结尾的ASCII。」

这是储存字串唯一的方法吗?并不是,事实上它是储存字串最烂的方法之一。只要是有作用的程序、API、操作系统、类别库,都应该像躲瘟疫一样避开ASCIZ字串。为什么呢?

让我们由撰写strcat这个把一个字串接到另一个字串的程序开始。

void strcat( char* dest, char* src )
{
     while (*dest) dest++;
     while (*dest++ = *src++);
}

大约读一下这段程序,想想我们这里做些什么。我们先扫描第一个字串寻找结尾的NULL字符。找到之后再扫描第二个字串,扫描时每次把一个字符复制到第一个字串。

这种字串处理和字串连接对Kernighan和Ritchie来说够好了,不过还是有些问题。举例来说。如果你有一大堆名字,全部都要附加到一个大字串后面:

char bigString[1000];     /* 我永远不知道要配多少内存... */
bigString[0] = '\0';
strcat(bigString,"John, ");
strcat(bigString,"Paul, ");
strcat(bigString,"George, ");
strcat(bigString,"Joel ");

这程序能动,对吧?没错,而且看起来不错也很干凈。

它的效能如何?是不是尽可能的快呢?它能处理更多资料吗?如果我有一百万个字串要连接,这样做适合吗?

答案是否。这段程序用了油漆工Shlemiel的演算法。Shlemiel是谁?他是下面这个笑话的主角:

Shlemiel得到一个在路上涂油漆的工作,他要漆在路中间的间断分隔线。第一天他拿了一罐油漆去漆好了300码的路。「做得真好!」他的老板说「你手脚真快啊!」然后就给他一个铜板。
第二天Shlemiel只漆了150码。「这样啊,没有昨天好,不过也还是很快。150码也很了不起。」也给他一个铜板。
第三天Shlemiel只漆了30码。「只有30码而已!」老板就哇哇大叫了。「这实在是无法接受!第一天你漆了十倍的长度耶!究竟怎么回事啊?」
「我也没办法啊,」Shlemiel说「我每隔一天就离油漆罐愈来愈远啊!」

kansas-large.JPG (想挑战的可以回答真实的数字是什么?)

这个烂笑话正确地说明了使用像我刚刚写的strcat版本会发生的状况。由于strcat的第一段每次都必须扫描目的字串,重复地找寻可恶的结尾NULL字符,所以这个函数会比真正需要的时间慢上许多,而且完全不能正常处理更多的资料。你每天用的程序中很多都有这个问题。很多文件系统的实现方式都有问题,不适合把几千个文件放在同一个目录里。因为同一个目录里有几千个文件时效率就会急速下降。试著打开一个塞满垃圾的Windows资源回收桶看看有多慢 - 要花几小时才能显示出来,显然并非与内含文件数量成线性关系。里头某处一定有个油漆工Shlemiel演算法在作怪。当你发现某件事本来应该有线性的效能,实际上却是次方的效能,就可以去找隐藏的Shlemiel。他们常常躲在你的程序库里。看著一长串的strcat或是循环中的strcat并不会让你立即喊出「n次方」,不过凶手就是它了。

要怎么修正这个问题呢?有几个聪明的C程序员实现了他们自己的mystrcat,内容如下:

char* mystrcat( char* dest, char* src )
{
     while (*dest) dest++;
     while (*dest++ = *src++);
     return --dest;
}

我们在这里做了什么?我们多花了一点点成本,传回指向较长的新字串结尾的指针。如此呼叫这个函数的程序就可以选择不必重新扫描,直接把新字串接上去:

char bigString[1000];     /* 我永远不知道要配多少内存... */
char *p = bigString;
bigString[0] = '\0';
p = mystrcat(p,"John, ");
p = mystrcat(p,"Paul, ");
p = mystrcat(p,"George, ");
p = mystrcat(p,"Joel ");

这段程序的效率当然是线性而非n次方的,所以要连接很多东西时也不会变慢太多。

Pascal的设计者注意到这个问题也加以「修正」了,修正的方法是在字串的第一个字节存一个字节数。这就叫做Pascal字串。Pascal字串里可以放零也不必用NULL字符结尾。因为一个字节只能放0到255的数字,所以Pascal字串最长只能到255个字节,不过由于他们不用在结尾加NULL字符,所以占的内存和ASCIZ字串一样。Pascal字串的好处是取字串长度时不必用循环了。在Pascal里求字串长度不用循环,只要一个组合组言指令就够了。当然是快太多了。

旧的麦金塔操作系统全都是用Pascal字串。很多其他平台的C程序员也为了速度而用Pascal字串。Excel内部就是用Pascal字串,所以Excel里很多地方的字串长度最多只能到255个字节,这也是Excel飞快无比的原因之一。

有很长一段时期,如果想在C程序里写一个Pascal字串文字,必须写成:

char* str = "\006Hello!";

没错,你得自己手工算出字节的数目,然后写死在字串的第一个字节。懒惰的程序员会做下面的事,结果就拖慢了程序:

char* str = "*Hello!";
str[0] = strlen(str) - 1;

注意这个例子里用的字串既是用NULL字符结尾(编译器做的)又是Pascal字串。我习惯称之为fucked字串,因为这比NULL字符结尾的Pascal字串好叫多了。不过这里是普通级频道,所以还是用较长的名字吧。

我之前省略了一个很重要的问题。记得这一行程序吗?

char bigString[1000];     /* 我永远不知道要配多少内存... */

由于我们今天的主题是位元,所以不应该就这样忽略它。我应该要用正确的写法:找出需要的字节数然后配置正确的内存量。

我不需要这样做吗?

这是一定要的。如果不做的话,某个聪明的骇客读我的程序时会注意到,我只配置了1000个字节并期望这数字足够,他们会找出某些聪明的方法,让我把一个长1100字节的字串strcat到原本1000字节的内存中,然后就会覆盖堆迭框并改变返回地址,于是当这个函数返回时就会执行到骇客写的程序。当他们说某个程序有一个缓冲区溢出漏洞时就是指这种东西。从前这可是骇客和蠕虫侵入的头号原因,不过微软 Outlook出来后,入侵就简单到连小朋友都会做了(译注:因为漏洞很多又可以用VBScript写)。

好吧,所以说这些程序员都只是些漏洞百出的傻瓜。他们应该要算出要配置的内存数量才对。

不过实际上在C里并不是这么容易。让我们回到我们的小小范例:

char bigString[1000];     /* 我永远不知道要配多少内存... */
char *p = bigString;
bigString[0] = '\0';
p = mystrcat(p,"John, ");
p = mystrcat(p,"Paul, ");
p = mystrcat(p,"George, ");
p = mystrcat(p,"Joel ");

我们应该要配置多少内存呢?让我们试试正确的方式。

char* bigString;
int i = 0;
i = strlen("John, ")
     + strlen("Paul, ")
     + strlen("George, ")
     + strlen("Joel ");
bigString = (char*) malloc (i + 1);
/* 记得预留结尾NULL字符的空间! */
...

我已经晕了。你可能正打算放弃去别的地方。我不怪你,不过请再忍耐一下,因为快要变得很好玩了。

我们必须把每个字串扫描一次才能算出所有字串的总长度,然后在连接字串时又要再扫描一遍。既然用Pascal字串时strlen动作会变快。或许我们可以写一个能替我们配置内存的strcat

这时就出现另一种蠕虫(译注:指下面所讨论的可用链结):内存配置器。你知道malloc的运作原理吗?malloc的特性是有一长串由可用内存组成的链结串列,名为可用链结(free chain)。当你呼叫malloc时会扫描链结串列,找寻大小满足要求的内存区块。然后把区块切成两块,一块是你所要求的大小,另一块则是剩下的内存,把你要的那块传给你,再把另一块(如果有的话)放回链结串列中。当你呼叫free时会把释放的区块加回可用链结。最后可用链结会被切割成很多小块,当你要求大块内存时就会找不到大小适合的内存。于是malloc就会逾时并开始扫描可用链结,尝试把相邻的小可用区块合并成较大的区域。这个动作会花个三天半(译注:就是说很慢啦)。这一团混乱的最后结论就是说malloc的效能特性绝不会非常快(一定要扫描可用链结),而且偶而(无法预测)要清理时还会慢到晕倒。(补充一下,令人惊讶的是这和garbage collected系统的效能特性是一样的。所以人们说garbage collection的效能损失有多么巨大,不完全是事实,因为一般的malloc实现都有著相同类型的效能损失,只不过稍为好一点点。)

聪明的程序员在配置内存时会用2的次方为大小(比如4字节,8字节,16字节,18446744073709551616字节等等),让malloc的潜在不隐定性降到最低。这样可以让可用链结里小碎块的数量降到最低,而有玩乐高积木的人应该都能直觉理解其原因。虽然似乎有点浪费空间,不过很容易就会看出浪费的空间不会超过总空间的一半。所以你的程序最多只会占用所需空间的两倍,这应该不是什么大问题才对。

假设你写了一个聪明的strcat函数,可以自动重新配置目的缓冲区。这个函数是否应该每次都重新配置到刚好的大小呢?我的老师兼心灵导师Stan Eisenstat建议,当你呼叫realloc时每次都应该用原本配置大小的两倍。意思是永远不必呼用realloc超过<NOBR>lg n</NOBR>次,即使是对很大的字串来说也会有很不错的效率特性,而且也绝不会浪费超过一半的内存。

总而言之,在这往下到最底层的字节世界中,生命愈来愈混乱。现在是不是很庆幸不需要再用C写程序呢?我们现在有像Perl、Java、VB、XSLT之类的伟大程序语言,让你永远都不用考虑这种事情,因为程序语言都想办法替你处理好了。不过偶而铅管等基础设施还是会在客厅地板中央突出来,这时候我们就得考虑要用String类别还是StringBuilder类别,或是想想某些类似的差异,因为编译器还不够聪明,无法理解我们尝试完成的每件事,也无法帮助我们不会疏忽写出油漆工Shlemiel演算法。

New_Haven_Flower_Vendor.jpg

上星期我写过如果你的资料是以XML方式储存的话,就无法实现出快速的SQL叙述SELECT author FROM books。为了避免有人不懂我在说什么,我再强调一次今天整天都在讲CPU层次的事。这样子应该比较看得懂上面的意思吧。

一个关联式数据库会如何实现SELECT author FROM books呢?在关联式数据库中,资料表的每一行(举例来说是books资料表)的字节数都完全相同,而且各个栏位由列开头起算的偏移量都是固定的。所以举个例子来说,如果books资料表中每个记录的长度都是100个字节,而author栏是位于偏移量23的地方,那么各个作者就会存在第23, 123, 223, 323等字节的地址。在这个SQL查询中要移到下一个记录时要怎么做呢?基本上只要这样就好:

pointer += 100;

只要一个CPU指令。够快了吧。

现在让我们来看看用XML格式储存的books资料表。

<?xml 废话...>
<books>
     <book>
          <title>UI Design for Programmers</title>
          <author>Joel Spolsky</author>
     </book>
     <book>
          <title>The Chop Suey Club</title>
          <author>Bruce Weber</author>
     </book>
</books>

来个小问题。要移到下一个记录的程序要怎么写?

呃...

这时候一个好的程序员可能会说,好吧,让我们分析XML转成树状结构存在内存里,这样处理起来会相当快。不过这时候CPU要处理SELECT author FROM books所做的工作量绝对会无聊到让你哭出来。每个编译器作者都知道字汇和语法分析是编译过程中最慢的部份。简单的说,字汇分析和语法分析时牵涉到大量的字串处理(前面已经发现这是很慢的)和大量的内存(也是已知很慢的),然后还要在内存中建立一个抽象文法树。而且还要假设你拥有足够的内存可以一次载入所有资料。在关联式数据库中,在记录间移动的速度是固定的,而且实际上只要一个CPU指令。这是刻意设计出来的。另外利用内存映对文件,就只要把真正要用的资料由载入对应的磁碟区块即可。在用XML的时候,如果有作前处理预先分析,在记录间移动的速度也是固定的,不过需要大量的启动时间,如果不预先分析,那么在记录间移动的速度就要看之前的记录有多长了,而且不管怎么都要几百个CPU指令。

对我来说,这表示当资料量很大又要求速度时不能用XML。如果资料不多或做的事并不需要速度,XML是个不错的格式。如果你想鱼与熊掌兼得,就得想个方法在XML以外加些诠释资料(metadata),储存类似Pascal字串中字节数目的资料。这样能提示资料在文件中的地址,就不需要再去扫描分析了。不过当然不能用文字编辑器去编辑文件了,因为会把诠释资料弄乱,所以其实这也不能算上真正的XML了。

对于那些一直跟著我撑到这里的读者,我希望你已经学到或是重新思考了某些事情。我希望探讨像strcatmalloc究竟如何运作这种枯燥的电脑科学一年级课程,能在你面对XML等技术时,协助你思考最新的高层次策略与架构上的决策。至于今天的作业,可以想想为什么全美达(Transmeta)的晶片好像总是卖不好。或者想想为什么最早HTML规格的TABLE设计会烂到让拨接上网的人不能快速的显示大型表格。也可以想想COM既然这么快,为什么跨越行程边界时又快不起了。或者NT设计者为什么会把显示驱动程序放入核心空间而不留在使用者空间呢。

这所有的问题都需要你考虑字节的层次,而它们也影响到我们在各种架构和策略上所做整体而高层次的决策。我认为电脑科系一年级学生应该由基础开始学起,使用C并由CPU开始一路建立观念,这正是原因。很多电脑科学计划认为Java很「容易」,不用碰无聊的字串/内存配置就能学习最酷的面向对象技术,能让你的大程序更加模组化,所以很适合当作入门学习的电脑语言。我对这种想法其实非常的反感。这是一个蕴酿中的教学灾难。毕业生一代代下来会愈来愈堕落,到处写出油漆工Shlemiel演算法而完全不自知,因为他们完全不知道,虽然perl指令看不出来,但是字串的底层操作其实是很困难的。如果你想把某人教好,就得从最底层开始。就像电影「小子难缠」一样。不断的上蜡打蜡。三个星期后轻轻松松就把别的小鬼干掉了。

ccode.gif

这些网页的内容为表达个人意见
Copyright (c)1999-2005 Joel Spolsky. 所有权利皆予保留

 

这篇关于软件随想录(local.joelonsoftware.com/wiki)-2002年12月11日 回归原点 - Back to Basics的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/768621

相关文章

软件设计师备考——计算机系统

学习内容源自「软件设计师」 上午题 #1 计算机系统_哔哩哔哩_bilibili 目录 1.1.1 计算机系统硬件基本组成 1.1.2 中央处理单元 1.CPU 的功能 1)运算器 2)控制器 RISC && CISC 流水线控制 存储器  Cache 中断 输入输出IO控制方式 程序查询方式 中断驱动方式 直接存储器方式(DMA)  ​编辑 总线 ​编辑

【STM32】SPI通信-软件与硬件读写SPI

SPI通信-软件与硬件读写SPI 软件SPI一、SPI通信协议1、SPI通信2、硬件电路3、移位示意图4、SPI时序基本单元(1)开始通信和结束通信(2)模式0---用的最多(3)模式1(4)模式2(5)模式3 5、SPI时序(1)写使能(2)指定地址写(3)指定地址读 二、W25Q64模块介绍1、W25Q64简介2、硬件电路3、W25Q64框图4、Flash操作注意事项软件SPI读写W2

AI基础 L9 Local Search II 局部搜索

Local Beam search 对于当前的所有k个状态,生成它们的所有可能后继状态。 检查生成的后继状态中是否有任何状态是解决方案。 如果所有后继状态都不是解决方案,则从所有后继状态中选择k个最佳状态。 当达到预设的迭代次数或满足某个终止条件时,算法停止。 — Choose k successors randomly, biased towards good ones — Close

代码随想录冲冲冲 Day39 动态规划Part7

198. 打家劫舍 dp数组的意义是在第i位的时候偷的最大钱数是多少 如果nums的size为0 总价值当然就是0 如果nums的size为1 总价值是nums[0] 遍历顺序就是从小到大遍历 之后是递推公式 对于dp[i]的最大价值来说有两种可能 1.偷第i个 那么最大价值就是dp[i-2]+nums[i] 2.不偷第i个 那么价值就是dp[i-1] 之后取这两个的最大值就是d

免费也能高质量!2024年免费录屏软件深度对比评测

我公司因为客户覆盖面广的原因经常会开远程会议,有时候说的内容比较广需要引用多份的数据,我记录起来有一定难度,所以一般都用录屏工具来记录会议内容。这次我们来一起探索有什么免费录屏工具可以提高我们的工作效率吧。 1.福晰录屏大师 链接直达:https://www.foxitsoftware.cn/REC/  录屏软件录屏功能就是本职,这款录屏工具在录屏模式上提供了多种选项,可以选择屏幕录制、窗口

HomeBank:开源免费的个人财务管理软件

在个人财务管理领域,找到一个既免费又开源的解决方案并非易事。HomeBank&nbsp;正是这样一个项目,它不仅提供了强大的功能,还拥有一个活跃的社区,不断推动其发展和完善。 开源免费:HomeBank 是一个完全开源的项目,用户可以自由地使用、修改和分发。用户友好的界面:提供直观的图形用户界面,使得非技术用户也能轻松上手。数据导入支持:支持从 Quicken、Microsoft Money

消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法

消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法   消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法 [转载]原地址:http://blog.csdn.net/x605940745/article/details/17911115 消除SDK更新时的“

PDF 软件如何帮助您编辑、转换和保护文件。

如何找到最好的 PDF 编辑器。 无论您是在为您的企业寻找更高效的 PDF 解决方案,还是尝试组织和编辑主文档,PDF 编辑器都可以在一个地方提供您需要的所有工具。市面上有很多 PDF 编辑器 — 在决定哪个最适合您时,请考虑这些因素。 1. 确定您的 PDF 文档软件需求。 不同的 PDF 文档软件程序可以具有不同的功能,因此在决定哪个是最适合您的 PDF 软件之前,请花点时间评估您的

梳理2024年,螺丝钉们爱用的3款剪辑软件

这年头,视频到处都是,就跟天上的星星一样数不清。不管你是公司里的新面孔,还是职场上的老狐狸,学会怎么剪视频,就好比找到了赢的秘诀。不管是给上司汇报工作,展示你的产品,还是自己搞点小视频记录生活,只要是剪辑得漂亮,肯定能一下子吸引大家的目光,让人记得你。咱们今天就来侃侃现在超火的三款视频剪辑工具,尤其是PR剪辑,你肯定听说过,这货在剪辑界可是大名鼎鼎,用它剪视频,既专业又麻利。 NO1. 福昕轻松

秒变高手:玩转CentOS 7软件更换的方法大全

在 CentOS 7 中更换软件源可以通过以下步骤完成。更换源可以加快软件包的下载速度,特别是当默认源速度较慢时。以下是详细步骤: 前言 为了帮助您解决在使用CentOS 7安装不了软件速度慢的问题,我们推出了这份由浪浪云赞助的教程——“CentOS7如何更换软件源加快下载速度”。 浪浪云,以他们卓越的弹性计算、云存储和网络服务受到广泛好评,他们的支持和帮助使得我们可以将最前沿的技术知识分