beta 的第三篇心得:深入研究 case 语句 (50分)

B

beta

Unregistered / Unconfirmed
GUEST, unregistred user!
声明:本文乃 熊恒(beta) 原创,如要转载请保持文章完整。
深入研究 case 语句 --- by 熊恒(beta)
不小心在论坛上引发舌战,当中涉及到了 if then
else
if 的连续使用和直接使
用 case 的情况下,效率孰高孰低的问题。今天的话题就从解决这个争论开始吧,
在最前面,我不想多说什么,实践是检验真理的唯一标准,咱们用例子说话:
// 例一
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
begin
i := 7;
if i = 0 then
ShowMessage('0')
else
if i = 1 then
ShowMessage('1')
else
if i = 5 then
ShowMessage('5')
else
if i = 10 then
ShowMessage('10')
else
if i = 7 then
ShowMessage('7')
else
if i = 9 then
ShowMessage('9')
else
ShowMessage('X');
end;

// 例二
procedure TForm1.Button2Click(Sender: TObject);
var
i: Integer;
begin
i := 7;
case i of
0: ShowMessage('0');
1: ShowMessage('1');
5: ShowMessage('5');
10: ShowMessage('10');
7: ShowMessage('7');
9: ShowMessage('9');
else
ShowMessage('X');
end;
end;

咱们先从例一开始(把精彩的留在后面:p)。这是典型的 if then
else
if 使
用,看一下它对应的汇编代码:
i := 7;
mov eax, $00000007
if i = 0 then
test eax, eax // 可以看到编译器很聪明的,和 0 的比较被优化了
jnz +$0b // 若不为 0 则跳转到下一个 if
ShowMessage('0')
... // 省略不必要的部分
else
if i = 1 then
cmp eax, $01
jnz +$0c // 若不为 1 则跳转到下一个 if
ShowMessage('1')
...
else
if i = 5 then
cmp eax, $05
jnz +$0c // 若不为 5 则跳转到下一个 if
... // 后面部分不必再看了
可以看到它是一个 if 一个 if 地“规规矩矩”地进行顺序比较的,要是你一共
有 1M 个可能性,并且刚好所有的 if 都不成立(没有找到对应选项),那就要
轮到最后一个 else
,可想而知,要进行多少次痛苦的比较。
再看一看例二,采用的是 case 语句,还是先看汇编代码:
i := 7;
mov eax, $00000007
case i of
cmp eax, $0a
jnbe +$75
jmp dword ptr [eax * 4 + $0045239d]
... // 后面虽然还有但是由于上面这个 jmp,就不用看了
这最后三句汇编代码非常值得我们仔细推敲!其实 case i of 从决策到跳转一共
只用了三条汇编代码!优美!
先看第一句:cmp eax, $0a
从前面一句可以看到,eax 里面就是我们的 i 的值,那么把它和 $0a 相比
又是什么意思呢?$0a 的十进制值就是 10,看看前面的代码,的确有 10 这个选
项。why 10? 这肯定是首先浮现在你脑子里的问题,如果你仔细看,会发现,10
是几个选项里面最大的一个。(这正是我为什么把 10 这个最大的选项排在中间,
而不是最后,就是为了说明这个 $0a 是指的所有选项的最大值,而不是最后一个
选项的值!)
第二句:jnbe +$75
很好理解,如果 i 大于所有选项中最大的(即找肯定不到对应选项),则直
接跳转到 else
部分。回想前面的例一,要是出现找不到的情况,将是人生(不,
应该说是 CPU 的)一大悲哀 :)
好了,前面已经排除“异己”了,现在来看它是如何找到“党组织”的。
第三句:jmp dword ptr [eax * 4 + $0045239d]
跳转了。跳到哪里了?以从 eax * 4 + $0045239d 单元里面取出的值作为跳
转的目的。有汇编基础的朋友应该可以看出,$004523f 是一个基地址,eax,我们
的 i,是可变的,也就是说这是一个查表的过程!类似于数组的使用 arr。那
它查出来那么这个神秘的跳转目的地到底在哪里呢?一个单步(Step Over)就看
见光标停留在了 ShowMessage('7') 的位置。啊?就,找到组织了?
看一下它是怎么找到目的的。首先,把 i 作为一个索引,用查表的方式找出一
个数,然后它就说这个数就是我们梦寐以求的入口地址,直接跳过去了(晕~,
欲知详情,往下看)。因此,不管 i 在所有选项中处于什么位置,总是可以通过
这样的一个简单的查表,得到目的地址,只用一条跳转语句就过去了。
因此得出今天的结论:case 语句的效率高!依据是,if then
else
if 的平均时
间复杂度是 O(n/2),而 case 的时间复杂度是 O(1)。
当然了,如果本文就这样完了,那我肯定被读者的鸡蛋爆头——谜团尚未解开。
别急嘛,下面接着来:
首先当然是为什么能够通过一个简单的查表找到目的地址。我是这样理解的:
Delphi 在编译的时候就建立了一个地址表。把所有选项的入口地址存入这个表中
(既然存的是地址,DWord 类型的,所以前面的第三句汇编代码里面有一个乘以
4 的动作)。这个表结构大致是这样的,第 0 项中放的是值为 0 的选项的入口
地址;第 1 项中放的是值为 1 的选项的入口地址。因此,只要有了这个表的基
地址(显然,这可以在编译时确定),事情就变得容易了。
假设 i 等于 7,于是我只要到那个表里面找出第 7 项的值,那自然就是值为
7 的选项的入口地址。呵呵,原来就这么简单。
现在你明白了为什么 case 语句只能用于顺序类型了吧(要不然怎么根据值建
这个表),它不是不能实现对其他类型的使用,而是不愿意,因为这样我们可以
获得极高的效率。我可不想看见 case 语句变成简单的 if then
else
if 的封装。
其实问题并没有解决完,回头看看刚才的那三行汇编代码的前两行,那仅仅是
排除了 i 大于最大选项值的情况。但是要是 i 为 6 呢?这是它虽然小于最大选
项值 10,但是它仍然不存在于选项表中。那,程序就顺序执行咯,照样根据 i
取出偏移索引值。那这个索引值会指引它跳向何处呢?当然是 else
部分的入口
地址咯(如果有 else
的话,如果没有,则跳出 case 到后面紧接着的代码行呗)。
那但是前面建表的时候,没有 6 这个选项,在表中第 6 项应该没有相应的值啊。
呵呵,这好办,把表里面那些没有对应选项值的“空位”全部填上 else
部分的入
口地址偏移值(若没有 else
部分则填 case 后面紧接着的代码行的地址)就可以
啦。好办法!
还有问题,要是选项里面有负数怎么办?怎么建表?容易,直接以最小的选项
值为基础,所有选项值减去这个负数,于是,选项值又从 0 开始了(当然,查表
的时候 i 也做相应变化)。那要是 i 是负数呢?仔细看那三句汇编的第二句:
jnbe +$7c,jnbe 是判断无符号的,i 要是负数,把它看成无符号数,最高位就
是 1,自然大于所有的有符号正数(最高位为 0),而被当成异己,直接排除了。
最后一个问题,这个神秘的表到底建在什么地方呢?如果我的眼神没有退化的
话,它应该紧接着 case i of 即那三行汇编的后面。表后面就是所有选项的入口。
不过,在最大项和最小项之间的差值大于等于 15 的时候,查找地址的动作略有
不同,这应该和表所占空间有关系吧(这种情况没有深入研究,头有点晕了:p)。
悄悄告诉你,当选项数目很少(小于 5)的时候,不会建立这个表,而是以另
外一种比较有趣的方法实现,你若有兴趣可以自己去看一下。还有,当最小选项
值大于 3 的时候,Delphi 会自动把所有选项的值减去该最小项的值,使最小项
从 0 开始,这样可以节约入口地址表的空间(当然,在用 i 来查表的时候,也
要先减去该值,否则就不对了)。
不管是谁,想到这个办法来实现多分支结构,同时能够极大地提高效率,的确是
个了不起的人。这也进一步让我认识到,编程的确是一项艺术。
差不多应该讲完了(我所知道的),就是不知道讲清楚没有,有问题大家一起探
讨吧。吵架也没有关系,只要别人身攻击就行了:p

 
Y

ysai

Unregistered / Unconfirmed
GUEST, unregistred user!
好,UUUUUUUUUP
 
D

dhycq

Unregistered / Unconfirmed
GUEST, unregistred user!
G

gmc001

Unregistered / Unconfirmed
GUEST, unregistred user!

孔明.net

Unregistered / Unconfirmed
GUEST, unregistred user!
我想叫你一声老大。
 
N

nansha

Unregistered / Unconfirmed
GUEST, unregistred user!
有意思且精彩的解释!
 
C

Cheasy

Unregistered / Unconfirmed
GUEST, unregistred user!
在学习中...
Thank you!!!
 
I

iwalk

Unregistered / Unconfirmed
GUEST, unregistred user!
大侠,你已经不是beta了,
应该是beta3
 

蛐蛐

Unregistered / Unconfirmed
GUEST, unregistred user!
并不是每个IF then
 else
 。。都可以用CASE来替换的,
尽管有这样的或那样的好方法。
 

雪中漫步

Unregistered / Unconfirmed
GUEST, unregistred user!
你什么时候出书啊,呵呵!
 
B

beta

Unregistered / Unconfirmed
GUEST, unregistred user!
// 并不是每个IF then
 else
 。。都可以用CASE来替换的
这是当然,要不然 if then
else
就没有存在的必要了
我只是想说明,对于顺序类型的多分支结构,用 case 比较好
 
S

SS2000

Unregistered / Unconfirmed
GUEST, unregistred user!
分支跳转表的技术在编译器中很常用,和中断向量表类似
难得beta写出这么好的文章,让我们大家更了解一些底层的东西
当初我一句if..then
..else
..效率最高的错误论断(不过,如果
只case 2,3中情况,还是if..then
..else
..效率高,呵呵),
让beta写出这么好的文章,我也是有功劳的,也得给我点分吧,
抛砖引玉,呵呵
 
B

beta

Unregistered / Unconfirmed
GUEST, unregistred user!
放心,只要系统不反对,都有分:)
 
A

antic_ant

Unregistered / Unconfirmed
GUEST, unregistred user!
高手,高手,高高手
 
W

wingerk

Unregistered / Unconfirmed
GUEST, unregistred user!
我在以前写单片机程序时就经常用这种跳转表的方式,没什么特别值得惊奇的。
反汇编一些程序,会发现大量这种结构,不仅仅是DELPHI而已,这个是一个基本技术,
不知道的只是因为没有深究或者知识面窄。
 
B

beta

Unregistered / Unconfirmed
GUEST, unregistred user!
// 反汇编一些程序
并不是每个人都有那么多功夫或者说机会,接触这些东西的。
// 不知道的只是因为没有深究或者知识面窄。
知识面再宽,也不可能什么都知道。我不想在这里讨论全才与专才的问题,我只想说
术业有专攻。我写这篇文章出来只是想给用惯了高级语言的,在这方面没什么经验的
朋友看的,并没有拉着您等学通天下的高手进来签到。不过倒是您写的那些话,除了
体现出您知识面宽,什么都懂以外,没有别的意义。
是的,正是因为我知识面窄,所以每学到一点东西我都如获至宝,这是我前进的方式
没有必要非得获得其他人的认可。
 

Similar threads

顶部