我看了一下中间语言,他是把所有的case的变量集中起来,然后判断是否跟某个变量相等,如果相等,就转到相应的处理
但是这个判断也是从上往下比较是不是和这些变量相等的把,那和if else按顺序判断不是一样的么。vs里面case加不了断点,f11就直接进入到分支了,但是实际上,是不是从上往下case都判断过呢
但是这个判断也是从上往下比较是不是和这些变量相等的把,那和if else按顺序判断不是一样的么。vs里面case加不了断点,f11就直接进入到分支了,但是实际上,是不是从上往下case都判断过呢
再跳转之前,有没有判断其他的case,这是我要问的关键
我也感觉是这样的,因为中间语言看了好像是这样,只不过是switch形成了一个中间表的样子,但是用那个变量值和表中的值比较的时候 好像也是从上往下比较的吧
vs 里面 f11就是逐语句,试过,case打不了断点
using System.Collections.Generic;
using System.Linq;
using System.Text;namespace ConsoleApplication1
{
class Program
{
static int x = 1;
static void Main(string[] args)
{
switch (foo())
{
case 2:
Console.WriteLine("\t" + 2);
break;
case 3:
Console.WriteLine("\t" + 3);
break;
case 4:
Console.WriteLine("\t" + 4);
break;
}
}
static int foo()
{
x++;
Console.WriteLine("foo()");
return x;
}
}
}
2.swich做为一个基本流程控制语法,甚至在世界上第一个女程序员时代就已经被建立,如果这玩意到现在还有啥问题,那么这100年程序员们都在干啥呢?3.你做个简单实验,你在swich里面做两个的分支。 swich(a)
case 1:
break;
case 1+0:
break;编译器会告诉你你错了,出现了两个一样的分支,这说明什么?这说明编译器直接优化你的代码,他预先就判定了值,why??答案很简单,他需要保证每个分支值不一样,他需要构建一个hash表,他需要用查表法直接查到分支位置,也就如此而已
=========================================你要明白什么是hash表,什么是内存寻址 就不会这么认为了。 基址+偏移量。也就是说就算他是生成了一个中间表他一不会去全体扫描,他只会直接算出偏移量,然后直接跳转过去就ok
另外,switch只会一次性对变量求值。嗯,switch是把里面的东西计算一次,然后和其他的进行比较,不会计算多次,这个我以前差过,我想请教大神的是,他拿出switch里面的那个变量后,是不是在底层和每个case里面的值进行了比较? 这种比较又是怎么样的呢,顺序变量从上往下判断么,或者是其他?如果不是和每个case比较,那他是怎么知道对应那个,咋就直接跳转到那个case了呢,底层的东西我真的想了解一下,真心不是像孔乙己一样要知道多个茴香豆的写法,请指教,谢谢
2.我没有质疑这个,我只是想大神指导一下底层的东西
3.嗯,他保证switch的值是唯一的,这个我同意,“他需要构建一个hash表,他需要用查表法直接查到分支位置:”,嗯,我想问的就是这个查表法,是怎么个查找,怎么就查到这个分支的,没有一个个判断,他早呢么知道是哪个?查找的方式是什么?顺序查找还是其他的,请指教,谢谢。
4.我只是想多知道点东西,之前我也查过很多相关的帖子,自己也用IL反编译过switch代码,但是还是有点迷糊,所以想请教一下,如果能解惑的话,当然很感谢,如果不能我也不能强求不是
楼主的意思不是质疑
“case加不了断点,f11就直接进入到分支了”这个不是很奇怪吗
加不了断点,在哪儿判断的
这个不是很好用文字表达,hash表的存储方式并不是使用连续内存存储,他是使用基地址+偏移量存储比 1,3,5 相当于下面的表述swich表 基地址为100;
则 case 1: 在内存地址100+1的位置存储一个代码段指针
case 3: 在内存100+3 的位置存储一个代码段指针
case 5: 在内存100+5 的位置存储一个代码段指针那么如果是参数5进入swich逻辑的话,他并不需要去扫描1,3的地址,他会直接采用100+5去获取内存地址[105]处一个代码段指针,然后直接jmp过去
嗯,对,他是转到1,3,5对应的内存地址的,我看中间语言就是这样的
我的问题就是
“如果是参数5进入swich逻辑的话,他并不需要去扫描1,3的地址”
那么哪怎么知道是转到1对应的地址呢,还是3对应的地址呢,还是5对应的地址呢?他是不是要先跟前面的1,3,5判断下呢,然后再转到对应的内存地址呢?这个顺序是怎么样的呢?或者说是其他的?
static void Main(string[] args)
{
string x="g";
switch (x)
{
case "k":
Console.WriteLine("cccc");
break;
case "a":
Console.WriteLine("cccc");
break;
case "g":
Console.WriteLine("cccc");
break; }
}
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 93 (0x5d)
.maxstack 2
.locals init (string V_0,
string V_1)
IL_0000: nop
IL_0001: ldstr "g"
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: brfalse.s IL_005c
IL_000c: ldloc.1
IL_000d: ldstr "k"
IL_0012: call bool [mscorlib]System.String::op_Equality(string,
string)
IL_0017: brtrue.s IL_0035
IL_0019: ldloc.1
IL_001a: ldstr "a"
IL_001f: call bool [mscorlib]System.String::op_Equality(string,
string)
IL_0024: brtrue.s IL_0042
IL_0026: ldloc.1
IL_0027: ldstr "g"
IL_002c: call bool [mscorlib]System.String::op_Equality(string,
string)
IL_0031: brtrue.s IL_004f
IL_0033: br.s IL_005c
IL_0035: ldstr "cccc"
IL_003a: call void [mscorlib]System.Console::WriteLine(string)
IL_003f: nop
IL_0040: br.s IL_005c
IL_0042: ldstr "cccc"
IL_0047: call void [mscorlib]System.Console::WriteLine(string)
IL_004c: nop
IL_004d: br.s IL_005c
IL_004f: ldstr "cccc"
IL_0054: call void [mscorlib]System.Console::WriteLine(string)
IL_0059: nop
IL_005a: br.s IL_005c
IL_005c: ret
} // end of method Program::Main
http://stackoverflow.com/questions/3366376/are-net-switch-statements-hashed-or-indexed我说一下我的理解。首先不像C/C++一样,C#的switch除了支持数字,还支持字符串类型。
字符串比较简单,先说字符串,如果case个数较少的话,C#会用if else这样的方式来实现,就像41楼列出来的一样,从前往后一个个比较;如果个数较多的话(我也不知道具体多少算多),C#会new一个Dictionary来实现快速查找,Dictionary基本上就是一个hashtable,把字符串映射到一些连续的数字上(注意,是连续的,这很重要,并不是简单的GetHashCode返回的乱七八糟的数字),然后再switch这个数字。再说数字的,数字的也要分情况,如果那些case里的数字是离散的,那C#也是会用if else来实现;如果是一串连续的数字,像1,2,3,这时候就会用il里定义的switch指令 ,像这样 switch( IL_0229, IL_0239, IL_0249),这三个行号放在一块叫跳转表(jump table), 这个switch指令的执行我猜是这样的(我没看过il的教程,纯粹是猜测),判断当前寄存器里的值,如果值为0,那么跳转到IL_0229这行,如果值为1,则跳转到IL_0239,2则跳IL_0249。这个动作是O(1)的,不需要一个一个判断。注意这里寄存器里的值是连续的,只能从0往上加,如果你case的第一个不是0,编译器会先做一些四则运算把它变成0,就像1,2,3的话减去1就可以了。 如果你给的case里又有连续又有离散的呢,比方说我有4个值,1,2,3,100,这时候我发现编译器会先if else一下是不是100,如果不是的话剩下那1,2,3又可以switch了,反正这个是考验编译器智商的了,尽可能的优化。如果你还想问il里的switch到底是怎么实现o(1)的跳转的,可以看一下编译成x86后的代码,类似于这行,
jmp dword ptr [eax*4+002B2878h] 至于vs里f11单步不到中间的判断过程(即使是开启显示汇编),我觉得是vs考虑到大多数人没有想要看那些中间过程,而是想直接知道跳到哪个case了,所以把这个过程有意隐藏了
为什么高手总是这么低调呢,非常感谢您,我以前猜想的就是你这样的,但是不知道原因,看了你说的解释后,我感觉就是这样的,就是官方的不知道是不是有这样的说明,因为不敢肯定就一定是这样的,有些内容你也说了是猜测的,不过你猜测的好像很是那么回事,嗯,暂时先这么理解吧,十分感谢。”jmp dword ptr [eax*4+002B2878h]
“ 这个就真心看不懂了
还有一个疑问,希望能指导一下,是不是switch 的是数字的话,编译成机器码的时候,还进行了2分查找之类的,所以比if什么的速度更快了,或者说就只是像你说的跳转表,执行swtich指令。
如果是连续的,就直接switch了,这样最快。如果是离散的,从我的观察,小数据量时是直接if else的,没用什么数据结构,但没试过大量的,不排除用List排序实现的可能,就像大量字符串用Dictionary一样,但据我实验50个case还是用if判断,有可能编译器真没那么智能,也有可能没到编译器认为的阈值。
{
case 0:fun(0);
case 1:fun(1)
case 2:fun(2)
}如果你会汇编,一看反汇编后的代码就知道是怎么回事了
编译器为所有的case生成了一个表,里面存有所有case后面的代码的地址。
根据switch里的值直接转到表里的地址就可以了。
//下面的代码并不能执行,只是举例说明一下
address[3]={case_0,case_1,case_2};
jmp address[a];//跳转到a对应的位置。
case_0:fun(0);
case_1:fun(1);
case_2:fun(2);
那他去表里找对应的记录的时候是按顺序找的么,如果是的话,那么和if else也差不多,从上往下找
如果swtich的是字符串或者字符类型的话
用if和switch其实效率差不多,if每次都要把字符串取出来和要比较的值比较一下,而switch只是取出来一次,然后和case里面的分别比较就行了,所以在这点上效率上要高那么一点点。
两者的机制不一样,swtich是把所有case的值统一放到一起形成一个表,然后用要判断的值与他们进行一个个比较,在我看来,这个比较应该是按顺序进行的,如果比较相等的话,则转到对应的处理方法,swtich是case值放一起,处理的方法放一起,而if是每个判断条件后面跟一个处理方法,根据判断的结果来决定是否进入到该处理方法。所以这么来说的话,在这一点上,他们的处理效率应该是差不多的,只不过一个是放到一起一个个判断,一个是判断一个处理一个,但是都是顺序来的。(可以看一下41楼的中间语言,或者自己写个小程序看看中间语言)
如果swtich的是数字的话
用if和swtich效率差距就比较大,特别是如果是连续的数字,swtich的中间语言会形成switch( IL_0229, IL_0239, IL_0249),这么样的东西,会将case里面的数字按照顺序排列,这样的话,就可以支持二分查找啦,那么这样的速度绝对是快很多,机制还是和上面说到的机制一样,但是查找方法不一样,所以会比if快很多,而if的处理数字机制和处理字符串的机制差不多,不过不用像字符串一样频繁的去取了。
如果不是连续的数字的话,也会形成switch( IL_0229, IL_0239, IL_0249)这样的东西,我估计像46楼猜想的那样 ”这个动作是O(1)的,不需要一个一个判断。“ ,所以数字不管是连续的还是不连续的,他就算是按顺序来,也比if那种一个个判断要快上许多。
以上纯属目前个人推测以及猜想,说的也比较乱,暂时这么想的,如果有错误的地方,还往不吝赐教,谢谢
C中有段著名的代码,叫Duff's device(达夫设备),代码如下:
send(to, from, count)
register short *to, *from;
register count;
{
register n = (count + 7) / 8;
switch(count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while(--n > 0);
}
}
这段代码很多第一次看到的人都会晕吧?
对于语法,建议你可以看看《C语言参考手册(原书第5版)》,这本书有中文版的,虽然译得很勉强,但是至少对于switch语句我是看明白了。
以前,我以为像:
/*代码1*/
switch(a1+a2*a3)
{
case 3:bbb;break;
case 4:ccc;break;
case 5:ddd;break;
default:eee;break;
}
将会被编译成这样的伪代码:
tmp1=a1+a2*a3;
jmp case3
case3:
cmp tmp1,3
jnz case4
bbb;
jmp endofswitch
case4:
cmp tmp1,4
jnz case5
ccc;
jmp endofswitch
case5:
cmp tmp1,5
jnz default
ddd;
jmp endofswitch
default:
eee;
jmp endofswitch
endofswitch:
这就像if else if else了。不过这并不正确,因为如果将源代码改成:
/*代码2*/
switch(a1+a2*a3)
{
case 3:bbb;
case 4:ccc;
case 5:ddd;break;
default:eee;break;
}
按照C语言的说法,假如case 3被匹配,那么除了bbb;被执行外,还将“掉落”执行ccc;ddd;直到ddd;后的break;才跳出到switch语句后。但由于刚才的编译伪代码将判断放在了源代码case标签出现的地方,那么除了在break处编译成转到switch结束处之外,还要在所有没有break语句的地方,放一个跳过下一处case判断的跳转,代码实在是太冗长了。
其实代码1的编译后伪代码大约是这样的:
tmp1=a1+a2*a3;
cmp tmp1,3
jz case3
cmp tmp1,4
jz case4
cmp tmp1,5
jz case5
jmp default
case3:
bbb;
jmp endofswitch
case4:
ccc;
jmp endofswitch
case5:
ddd;
jmp endofswitch
default:
eee;
endofswitch:
而这种形式下,只要没有break,就等于去掉了相应的jmp endofswitch。
由于C/C++对case后能出现的东西限制很严,只能是编译时静态求值的一些简单的内置类型直接量(不能是字符串),且具体的实现将可能对case的最多个数有限制,以方便地做成一张跳转表。比如:
switch(MESSAGE_CODE)
case 0: ...
case 2: ...
case 3: ...
...
case 1023: ...
那么编译器完全可以做出一个0..1023的跳转数组,并对没有设置选项的case 1,设为跳到switch之外,因此编译后代码就更短了:
addr_arr[1024]={case0,endofswitch or default,case2,case 3... case 1023};
tmp1=MESSAGE_CODE;
if tmp1 not in 0..1023 then goto endofswitch(or default address)
jmp addr_arr[MESSAGE_CODE];
case 0:
...;jmp endofswitch or default
case 2:
...;jmp endofswitch or default
case 3:
...
endofswitch or default:
即使case值的集合比较离散,不适合做成数组跳转表,C仍然可以将所有选项做成二叉树,通过二分法查找,以缩短比较的总次数。这也是我以前看过一些编译或反汇编出来的结果非常难懂的原因。
C程序的switch往往除了判断int型的代码外,往往也判断char,比如对命令行程序选项的解释器代码,因此这些强制规则都是历史的产物了,与UNIX时代语言设计者想让switch发挥的作用密切相关。
如今,虽然不指望switch语句会像Ada的case、SQL的CASE或VB的Select Case那样灵活,至少也得容许case中出现字符串吧?C++觉得字符串的比较还是个“大活儿”,对库函数是有复杂的依赖(比如,是char还是wchar_t的字符串?),因此与C一致,它仍然不允许。Java和C#呢?算是在效率与灵活性方面取了个折中,这也是这几种语言各自定位造成的。
回到文章开始的Duff's device,《C语言参考手册》中介绍了case是与其外最内层的switch相匹配的。
default也可以出现在所有的case之前或它们之间(因此也可以从default往其他case上“掉落”),这并不影响编译器识别和跳转,default虽然总是所有case都没成功后的第一去处,但它的代码也可以在case的前面:
switch(a)
{
default: aaa;
case 'b':bbb;
case 'c':ccc;break;
case 'd':ddd;break;
case 'e':eee;
}
在C++中,要注意如果某case代码段中有构造发生,那么一定要将其包在{}块语句中。因为如果没有放在{}中,则其作用域在switch的一对{}之内,由于switch之后会在各case及default中跳转,对象的析构全部放在switch结尾,其构造却有可能被跳过,如:
switch(i)
{
case 1:
break;//跳过了构造却仍然要执行析构
case 2:
SomeClassType a;//a在此构造
break;
case 3:
break;//跳过了构造却仍然要执行析构
//a在此析构
}
如果是连续的,就直接switch了,这样最快。如果是离散的,从我的观察,小数据量时是直接if else的,没用什么数据结构,但没试过大量的,不排除用List排序实现的可能,就像大量字符串用Dictionary一样,但据我实验50个case还是用if判断,有可能编译器真没那么智能,也有可能没到编译器认为的阈值。
[/Quote]
刚刚又仔细看了一下,我这里搞错了,确实是2分查找的,但没用什么其他的工具类,是直接if的,先判断中间的值,再往2边走。
像这个
switch(a)
{
case 100:
case 200:
case 300:
case 400:
case 500:
case 600:
}
il里是这样的
if(a>300)
{
if(a==100){}
else if(a==200){}
else if(a==300){}
}else
{
if(a==400){}
else if(a==500){}
else if(a==600){}
}可能我这边的数据量少,只2分了一步
这里没有什么只取一下的概念,字符串比较的时候都是在调用string.Equals()函数
前面说过字符串量大的时候会用hashtable,这个是近似于o(1)的,比if的o(n)要快
switch( IL_0229, IL_0239, IL_0249)是绝对的o(1)的,过程就是一个数组的随机存取,像#36楼所说的不是连续的就不能用switch( IL_0229, IL_0239, IL_0249),只能用2分查找,O(log(n))的,但还是要比自己if强总的来说,用switch要比if好,不管是效率还是可读性。从根本上看switch的优势是因为它对要比较的表达式做了较多的限制(只能是编译时可知的,类型必须是int或string)从而编译器能够有针对性的优化,而 if因为 可变性较大没法做优化
004015BF mov eax,dword ptr [ebp-0Ch]
004015C2 mov dword ptr [ebp-10h],eax
004015C5 cmp dword ptr [ebp-10h],5
004015C9 je main+79h (004015d9)
004015CB cmp dword ptr [ebp-10h],6
004015CF je main+82h (004015e2)
004015D1 cmp dword ptr [ebp-10h],7
004015D5 je main+8Bh (004015eb)
004015D7 jmp main+94h (004015f4)可以看出,switch仍然是和每个分支的条件值进行比较,一旦je相等,则跳到相应分支的地址去执行分支代码,而并没有所谓的hash表,通过case值直接hash得到分支的地址。我唯一看到if-else和switch语句有区别的地方就是,if是直接从条件变量的地址取值出来和条件进行判断,而switch要开辟一个空间拷贝一个条件变量,然后再去新的值和case的值逐一比较,代码在这里
004015BF mov eax,dword ptr [ebp-0Ch]
004015C2 mov dword ptr [ebp-10h],eax
int main()
{ int a = 5;
int b = 6;
int c = 7; if ( a == 5 )
{
c = 5*a;
}
else if ( a == 6 )
{
c = 6*a;
}
else
{
c = 7*a;
} c = 6;
switch ( c )
{
case 5:
b = 5;
break;
case 6:
b = 6;
break; case 7:
b = 7;
break; default:
b = 8;
break;
} cout<<a<<b<<c<<endl; return 0;
}7: int a = 5;
00401578 mov dword ptr [ebp-4],5
8: int b = 6;
0040157F mov dword ptr [ebp-8],6
9: int c = 7;
00401586 mov dword ptr [ebp-0Ch],7
10:
11: if ( a == 5 )
0040158D cmp dword ptr [ebp-4],5
00401591 jne main+3Eh (0040159e)
12: {
13: c = 5*a;
00401593 mov eax,dword ptr [ebp-4]
00401596 imul eax,eax,5
00401599 mov dword ptr [ebp-0Ch],eax
14: }
15: else if ( a == 6 )
0040159C jmp main+58h (004015b8)
0040159E cmp dword ptr [ebp-4],6
004015A2 jne main+4Fh (004015af)
16: {
17: c = 6*a;
004015A4 mov ecx,dword ptr [ebp-4]
004015A7 imul ecx,ecx,6
004015AA mov dword ptr [ebp-0Ch],ecx
18: }
19: else
004015AD jmp main+58h (004015b8)
20: {
21: c = 7*a;
004015AF mov edx,dword ptr [ebp-4]
004015B2 imul edx,edx,7
004015B5 mov dword ptr [ebp-0Ch],edx
22: }
23:
24: c = 6;
004015B8 mov dword ptr [ebp-0Ch],6
25: switch ( c )
26: {
004015BF mov eax,dword ptr [ebp-0Ch]
004015C2 mov dword ptr [ebp-10h],eax
004015C5 cmp dword ptr [ebp-10h],5
004015C9 je main+79h (004015d9)
004015CB cmp dword ptr [ebp-10h],6
004015CF je main+82h (004015e2)
004015D1 cmp dword ptr [ebp-10h],7
004015D5 je main+8Bh (004015eb)
004015D7 jmp main+94h (004015f4)
27: case 5:
28: b = 5;
004015D9 mov dword ptr [ebp-8],5
29: break;
004015E0 jmp main+9Bh (004015fb)
30:
31: case 6:
32: b = 6;
004015E2 mov dword ptr [ebp-8],6
33: break;
004015E9 jmp main+9Bh (004015fb)
34:
35: case 7:
36: b = 7;
004015EB mov dword ptr [ebp-8],7
37: break;
004015F2 jmp main+9Bh (004015fb)
38:
39: default:
40: b = 8;
004015F4 mov dword ptr [ebp-8],8
41: break;
42: }
43:
44: cout<<a<<b<<c<<endl;
004015FB push offset @ILT+195(std::endl) (004010c8)