再论Case和If语句的优劣(50分)

  • 主题发起人 主题发起人 DarwinZhang
  • 开始时间 开始时间
case不能代替If,if功能比Case更强大。
因为If后面是逻辑表达式,而Case只能判断大小
 
要选项到多少才不会出现 DarwinZhang 的汇编代码?
地址表又如何分配?
 
beta中主要讨论的是字符串的情况, 和DarwinZhang中的有所不同。
 
我想了一下,其实beta的“是因为你的选项太少!”是不对的,
其实简单的试验一下就知道了,即使选项个数加到100个也没用。

但“变量相差很大时”的情况实际上大部分情况是可以解决的。
就是使用一个 散列表函数(或者叫Hash函数),可以在大部分情况下大大改善case的情况
设法根据实际情况构造一个hash函数 function H(i:Integetr):Integer;
使得各个值基本连续就可以了,不过选项大概要多于10个才合算,
case H(i) of
1:
2:
3:
...
end;
因为实际情况不同,构造hash函数的情况不同,
所以Delphi不可能对“变量相差很大时”的情况进行优化!
 
to DarwinZhang:
case逻辑要清楚些,你说的效率问题,if中也存在,
if i=13 then... else...
我用下面也同样能解决
case i of
[red] 13:...[/red]
0: ...
1: ...
2: ...
3: ...
11:...
12:...
14:...
............
end
 
GZ,我对Case在Delphi中的原理上还不是很清楚,想了解。。。:)
 
以现今cpu的神速, if 和 case的执行速度不应有甚麽分别.
但是在除错分别, case始终较为简洁.
 
zjczxd 老兄[:)],你的
//我想了一下,其实beta的“是因为你的选项太少!”是不对的,
你说你“想了一下”,这就很有问题了。我所说的都是实际调试
出来的。你之所以认为不对,是因为你没有实际看过!
建议你再回去仔细看我那篇帖子。当然,也不是逼你看:)

我发现一个很奇怪的问题,就有不少人断章取义,扛着半截就跑。
然后就拿着那些残缺的、本来就不能独立考虑的东西来进行辩论。
我能怎么辩?
其实我没有必要去辩解什么,我自己对这个问题理解的很清楚,
不会受到什么影响。但是,也正是因为这样,怕不少新人看到这
些经过错误理解后得出的结论而误入歧途,我一直竭力的去争辩。
不过现在我觉得这真的是没有什么作用,大家那么多张嘴。我今
天跟你争完了,明天有来一个,我能应付多少?
是的,我说过,我喜欢抬杠,但是我是指的“单挑”。像这样的
一对多的争论,发展下去没什么意义。

要是有谁真的想就这个问题和我好好争论一翻,可以和我单独约
个时间,QQ上见。否则在这里、像这样讨论下去,恐怕有误人子
弟之嫌。
 
to beta: 您看了我的:“其实简单的试验一下就知道了,即使选项个数加到100个也没用。”吗?
拜托您看清楚,我看了您的文章,可以说您的文章是面对新手的,
可您为什么不看清我写的东西?
我已经用程序生成了100个选项再看汇编代码才得出的结论。您做了吗?

很显然DarwinZhang的文章是和中等水平以上的人讨论的,您可以看一下
http://www.delphibbs.com/delphibbs/dispq.asp?lid=1277180
就会明白DarwinZhang的水平不会低。我就是因为在那里被他强白了,才会在这里和他争一争。
谁想到会惹恼了老兄。

什么“断章取意”?请您说一下 加到多少个不连续的选项会成表?
只有连续的选项才能用地址表来解决!不连续的情况 我已经提出散列表函数的办法。
* * * * * * * * * * * * * * * *

您却一个实际的东西也不拿出来,
建议您把DarwinZhang的例子中的选项增加一些,看一下就不会吵了。
 
假如您想和我深究的话,大可以在这里谈,让大家看一看。
什么“误人子弟之嫌”?难到这里的都是平庸之辈?呵呵。
 
[h3][red]我认为各有好处,当要用到判断是否为true 时,我觉得用if 好,如果是列举的我认为用case好点![/red][/h3]
 
不可否认Case的可读性更好一些,
但是它并不能替代if then

大家所说的效率不知道是什么,
有没有谁对编译后的代码进行测试呢
包括代码大小和执行效率,
如果测试过,请列出测试结果

个人主观认为,case的执行效率更高一些
 
个人认为效率应该差别不大。如果需要很多区别的话,使用Case会使程序可读性增强,并且便于维护
 
to zjczxd:

// 我已经用程序生成了100个选项再看汇编代码才得出的结论。您做了吗?
我当然做了,要不然我敢这样说?众目睽睽之下我总不至于张着口瞎说吧?
至于我怎么做的,在我的文章里面写的很清楚。

// 只有连续的选项才能用地址表来解决
// 您却一个实际的东西也不拿出来
我觉得您才是没有根据吧,我什么东西都没有拿出来?我在我的文章里面一
开始的 例二 里面就已经列出来了,汇编代码也列出来了,这叫没有东西?
再说那个测试的数据是:0,1,5,10,7,9 这是连续的选项吗?当然不是!
但是它们生成的汇编代码确的确是用的跳转表!这些都是明摆在我的文章里
面的东西。我又一遍把它们搬出来,以证明我不是空口说白话。

// 当变量相差很大时就会发生效率非常低下的情况
对于这个问题我在本贴的第一个回复有误,我承认。回复的时候比较仓促。
但是我在我的文章里面说过,选项相差的极限是15。可说到效率非常低下,
是不准确的,这种情况下,无非是生成一个 sub 一个 jz,而相应的,对于
用 if 语句来说,生成的代码是一个 cmp 一个 jnz。可以看到,两者的效
率是差不多的。要说也就是一个要保存结果,一个不保存结果,对于寄存器
而言,这个时间可以忽略。因此,如果一定要说这种情况下 case 的效率低
那么这种情况下,if 的效率也低。换句话说,这种情况下,几乎无法判别
出两者的优劣


// 明白DarwinZhang的水平不会低
// 难到这里的都是平庸之辈?
我觉得您认为我把您们低估了,但是我要说的是,我只讨论问题,不管您是
不是高手。我也从来没有认为您或者谁水平不行,术业有专攻而已。
可是您列举出这个,我认为和我们讨论的具体问题没什么必然连系。[:)]

// 很显然DarwinZhang的文章是和中等水平以上的人讨论的
// 什么“断章取意”?
// 什么“误人子弟之嫌”?
我不是说您,如果您这样认为,我道歉。您看 LiChaoHui 朋友的回答:
“不可否认Case的可读性更好一些,但是它并不能替代if then”
这不就体现出结果了吗?包括我,有N个人不只M次提出 case 不能完全代替
if then,可是呢?还是有人认为我们在给他们灌输那种思想。为什么?
同样的,谁也不敢保证,什么时候就来个谁,过来把某个片断作为“精华”
搜藏回去了,这不就是误人子弟吗?您担保没有这方面的新手看到本贴?


好了,该说的都说得差不多了,介于说过了的原因,如果没有意外,我不会再
在这个帖子回复了。
真诚希望各位能够讨论出一个结果。

 
to beta: 哈哈,我就知道您很有可能把基本连续当成不连续的情况.
您看到我前面讲的:
“ 设法根据实际情况构造一个hash函数 function H(i:Integetr):Integer;
使得各个值基本连续就可以了”
* * * *
了吗?
其实,在编译Case语句时,Delphi会对选项进行一个排序,然后对选型的连续情况作出判断,
判断是否有必要用空间换取效率,这样看来您的“选项相差的极限是15”也是不对的,
因为Delphi要对若干种情况进行判断。
我举若干个例子:
1. 1,3,5,7,9,11,13,19,21,23,35 //基本连续,会生成地址表
2. 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,36,37,38,39,40,41,42,43,
* *
44,45,46,47,48,49,50,51,52,53,54,55 //基本连续,会生成地址表
3. 1,6,11,16,21,26,31,36,41,46,51 //不是基本连续,不会生成地址表
4. 1, 101, 233, 532, 1018, 2129, 2972, 3072, 4083, 5009, 5389 //不是基本连续,不会生成地址表

当然,如果Delphi的版本不同,判断连续的标准是不同的,但基本的趋势是这样的。
另外,我要说明,Case实际上是使用近似折半搜索法来进行搜索的,
因此无法根据各种情况出现的效率,象If一样进行调整。
“该说的都说得差不多了,介于说过了的原因,如果没有意外,我不会再在这个帖子回复了。”
因为您只认为“我们没有去试验而只有您去试验的”,并没有静下心来看我写的东西,
所以才会造成你没有深究的原因。没想到您居然是这样的不事实求是。唉!
另外,只要新手仔细看了本帖,他也因该能够分清情况,希望您不要低估了其他的人。(只是建议而已)
 
DarwinZhang兄的分析很根本!
 
不好意思,这个问题很久没来看.下午我做一个答复.
 
不知道各位有没有用过这个函数
AnsiIndexStr
 
感谢大家的不同看法,但是从谈论情况看,好像大家对Case语句的原理说得并不够深入,
我将原来的一个测试帖上来吧。

实验条件:
软件: Win2k + Delphi 6.0 (Build 6.163)
硬件: PentiumMMX 200 + 64M SDRAM


对几条汇编语句的解释:
cmp 将两个数相减,但只影响标志位,不影响数值;
sub,dec,add,inc 运算语句,既影响标志位也影响数值;
jnbe 是无符号数的比较,如果大于就跳转;
jnle 是有符号数的比较,如果大于就跳转;
jz 等于零(两数相等)跳转;
jmp 无条件跳转。

==========================================================
这是少量代码的情况:
case i of
1: Caption:='1';
2: Caption:='2';
3: Caption:='3';
end;
//下面是注释过的Exe汇编代码
dec eax //减去1
jz +$08 //是1跳转
dec eax
jz +$13 //是2跳转
dec eax
jz +$13 //是3跳转
jmp +$28 //其他情况跳转
...

case i of
3: Caption:='3';
1: Caption:='1';
2: Caption:='2';
end;
//下面是注释过的Exe汇编代码
dec eax //减去1
jnz +$14 //是1跳转
dec eax
jnz +$1f //是2跳转
dec eax
jnz +$18 //其他情况跳转
...
可见,改变选项顺序,并不能真正改变执行的顺序。
另外,Delphi会将Cmp改成 sub语句,这是因为既可以减少目标程序代码又可因此而加快执行速度.

下面我们来分析以下 "基本连续变化之中间间断"的几种情况,相隔非常小的情况和连续
的情况Beta兄的文章好象说得比较详细,就不再赘述。
--------------------------------------------------------------------------------
二次表情况:

case i of //中间间隔中等的情况
1: ShowMessage('13');
2 : ShowMessage('1');
3 : ShowMessage('4');
4 : ShowMessage('7');
5: ShowMessage('11');
6: ShowMessage('12');

22: ShowMessage('12')
//可以适当加大间隔,但为了让大家看得清楚,所以选间隔16
23: ShowMessage('12');
24: ShowMessage('12');
25: ShowMessage('12');
26: ShowMessage('12');
27: ShowMessage('12');
end

//下面是注释过的Exe汇编代码
Unit1.pas.106: case i of //基本连续变化
cmp eax,$1b //$1b=27,因为选项最大是27
jnbe +$000000ee //假如比27大则跳转到从当前地址加$ee即是退出按钮代码地址
mov al,[eax+$004537c7] //从修正地址(表二)取修正值,第二修正表是以一个字节为单位,可以节约空间;该指令涉及到取地址,执行该指令时间要长一点
jmp dword ptr [eax*4+$004537e3] //按照地址表(表一)中内容进行跳转,地址表以四个字节为单位;该指令是7个字节且涉及到取地址,,执行该指令时间是比较长的
//-----下面是修正地址表(表二)--------------------------------------
+0 +1 +2 +3 +4 +5 +6 +7
004537C0 00
004537C8 01 02 03 04 05 06 00 00 //从1~6都有对应的表地址
004537D0 00 00 00 00 00 00 00 00 //如果不是选项那么就使用结束程序地址
004537D8 00 00 00 00 00 07 08 09
004537E0 0A 0B 0C //从22~27都有对应的表地址
//------下面是地址表(表一)------------------------------------------
+0 +1 +2 +3 +4 +5 +6 +7
004537E0 A8 38 45 00 17 //对应地址表,低字节在前,高字节在后
每个地址都占用4个字节
004537E8 38 45 00 26 38 45 00 32 //比如 26 38 45 00 代表地址 $453826 恰好是 ShowMessage('1')
语句的地址
004537F0 38 45 00 3E 38 45 00 4A
004537F8 38 45 00 56 38 45 00 62
00453800 38 45 00 6E 38 45 00 7A
00453808 38 45 00 86 38 45 00 92
00453810 38 45 00 9E 38 45 00 //假如是用一个地址表的话需要28*4=112
字节,而使用双表只要28+13*4=80个字节
//---------对应选项的指令---------
Unit1.pas.107: 1: ShowMessage('13');
00453817 mov eax,$004538d4
0045381C call ShowMessage
00453821 jmp +$00000082 //跳到$4538A8
Unit1.pas.108: 2: ShowMessage('1');
00453826 mov eax,$004538e0
0045382B call showMessage
00453830 jmp +$76
Unit1.pas.109: 3: ShowMessage('4');
00453832 mov eax,$004538ec
...
...
004538A8 xor eax,eax //做退出的准备工作
004538AA pop edx
004538AB pop ecx
004538AC pop ecx
004538AD mov fs:[eax],edx
004538B0 push $004538c5
004538B5 lea eax,[ebp-$04]
004538B8 call @LStrClr //将使用过的资源释放
004538BD ret //按钮事件结束返回

这时,Delphi是先从修正表中选取相应的修正值,然后再到地址表中去找地址,实现跳转。
------------------------------------------------------------------------------
如果间隔加到如下模式,Delphi还是使用两个地址表的方法:
case i of //中间间隔中等的情况
1: ShowMessage('13');
2 : ShowMessage('1');
3 : ShowMessage('4');
4 : ShowMessage('7');
5: ShowMessage('11');
6: ShowMessage('12');

52: ShowMessage('12');
53: ShowMessage('12');
54: ShowMessage('12');
55: ShowMessage('12');
56: ShowMessage('12');
57: ShowMessage('12');
end;
这时,假如是用一个地址表的话需要58*4=232个字节,而使用双表只要58+13*4=110个字节,
节约了(232-110)/232=52.3%的空间,而速度却相差无几。
可能有人会说“现在内存是以M为单位,节约这么几百个字节有必要吗?”
是的,现在的内存是比较大,但是积少成多。何况,要关心速度的情况一般是要反复执行的
代码,
这时代码是放在高速缓存中执行的,高速缓存是以K为单位。当然谈节约空间就有必要了。
--------------------------------------------------------------------------------
但到如下情况则情况为之一变:
case i of //中间间隔很大的情况
1: ShowMessage('13');
2 : ShowMessage('1');
3 : ShowMessage('4');
4 : ShowMessage('7');
5: ShowMessage('11');
6: ShowMessage('12');

152: ShowMessage('12');
153: ShowMessage('12');
154: ShowMessage('12');
155: ShowMessage('12');
156: ShowMessage('12');
157: ShowMessage('12');
end;
//下面是注释过的Exe汇编代码
Unit1.pas.106: case i of
cmp eax,$98 //$98=152
jnle +$32 //跳转到大于152的情况
jz +$ao //等于152跳转到对应的情况
cmp eax $06
jnbe +$dd //比6大,跳转到退出代码地址
jmp dword ptr [eax*4+$4537ce] //跳转到地址表
...
add eax,$ffffff67 //这里是大于152的情况,相当于减去153, $ffffff67=-153
cmp eax,$04 //153+4=157
jnbe +$ac //大于157则跳转
jmp dword ptr [eax*4+$4537ff] //跳转到地址表
...
...
因此,这时Delphi是将选项分成两种不同的情况来进行地址跳转的。
-------------------------------------------------------------------------------------

下面我们来分析以下 "非小间隔连续变化"的几种情况。
case i of //非连续变化
1: ShowMessage('13');
11 : ShowMessage('1');
16 : ShowMessage('4');
26 : ShowMessage('7');
23: ShowMessage('11');
28: ShowMessage('12');
37: ShowMessage('12');
43: ShowMessage('12');
49: ShowMessage('12');
57: ShowMessage('12');
59: ShowMessage('12');
68: ShowMessage('12');
end;
对应汇编代码:
unit1.pas.case i of
cmp eax,$25 //$25=37 即首先判断第7个选项的值
jnle +$32
jz +$a8
cmp eax,$17 //$17=23 即再判断第5个选项的值
jnle +$18
jz +$85
dec eax //减去1看是不是0
jz +$4f //是0,说明原来的值是1,跳转到1对应的选项
sub eax,$0a //减去$0a=10,
jz +59 //是0,说明原来的值是11,跳转到1对应的选项
......
这样我们看到,这时Delphi是先将整个跳转区域分成若干个比较小的区域,再用递减的办法来实现跳转的。
可以尝试将顺序打乱,比如:
case i of //非连续变化
16 : ShowMessage('4');
57: ShowMessage('12');
1: ShowMessage('13');
23: ShowMessage('11');
28: ShowMessage('12');
26 : ShowMessage('7');
37: ShowMessage('12');
43: ShowMessage('12');
11 : ShowMessage('1');
49: ShowMessage('12');
59: ShowMessage('12');
68: ShowMessage('12');
end;
再看得到的汇编代码(就不贴出来了),其中的cmp后的指令和Dec,Sub指令和上面的代码是一样的,
所以Delphi进行了排序,然后再进行编译。
--------------------------------------------------------------------------------------
case i of //非连续变化
1 : ShowMessage('4');
5: ShowMessage('12');
10: ShowMessage('13');
15: ShowMessage('11');
20: ShowMessage('12');
25: ShowMessage('7');
30: ShowMessage('12');
40: ShowMessage('12');
50: ShowMessage('1');
60: ShowMessage('12');
70: ShowMessage('12');
80: ShowMessage('12');
end;
对应汇编代码:
cmp eax,$1e //$1e=30
jnle +$7e //大于30
jz +f4 //等于30
cmp eax,$19 //$19=25
jnbe +$131 //大于25跳转,实际是大于30跳转
jmp dword ptr [eax*4+$004537cc] //地址表法
...
cmp eax,$3c //$3c=60
jnle +$15 //大于60跳转
jz +$95 //等于60跳转
sub eax,$28 //$28=40
jz +$78 //等于40跳转
....
因此,Delphi会根据情况来分段使用不同的处理办法。
-----------------------------------------------------------------------
综合上面的内容,我们可以发现:Case语句的编译比较复杂,Borland确实很有办法,
可是得到的结论并不令人满意,因此:
结论是case语句比较"狡猾",难以干预它的行为,以至我们难以干涉它的实现方式。
就这一点来说,If语句就好得多。If语句总是”忠实“的生成我们想要的代码,
我们原来想要的结果一致,可以很容易根据概率调整程序。
 
后退
顶部