关于goto和switch的一些思考

昨晚在实验室回寝室前,和队友们谈到关于goto的问题,于是不禁想到了《CODE COMPLETE Ⅱ》中举的一个巧妙例子:

if(statusOK)
{
	if(dataAvailable)
	{
		importantVariable = x;
		goto MID_LOOP;
	}
}
else
{
	importantVariable = GetValue();
MID_LOOP:
	//	lots of code
	//	...
}

这个例子的逻辑比较曲折,想不用goto重写它并不是非常简单的事情。在多数语言中,重写后的代码都会比原来的代码略多,效率也会相对低一点,但除非此代码段位于一个对效率要求极其严格的位置,否则在重写时无需考虑这么多。

书中给出了一个最佳重写方法:把lots of code部分移植到一个单独的子程序里去,然后就可以在goto出现的地方调用该子程序,同时保留原有的代码结构。代码如下:

if(statusOK)
{
	if(dataAvailable)
	{
		importantVariable = x;
		DoLotsOfCode(importantVariable);
	}
}
else
{
	importantVariable = GetValue();
	DoLotsOfCode(importantVariable);
}

但是,有的时候,把重复的代码提取成为单独的子程序是不现实的。此时我们可以考虑改变代码的条件判断结构以消除goto,代码如下:

if((statusOK && dataAvailable) || !statusOK)
{
	if(statusOK && dataAvailable)
	{
		importantVariable = x;
	}
	else
	{
		importantVariable = GetValue();
	}
	//	lots of code
	//	...
}

这种方式无疑是可靠的,但是也很机械,而且检测条件比原代码要多。个人不喜欢这种方式,因为使用决策的结构要更为直观。

以上只是为了说明一个情况:goto并非在所有情况下都可以被取代。

接下来引用《CODE COMPLETE Ⅱ》中的几点关于goto的使用原则,笔者甚是认同:

  • 在那些不直接支持结构化控制语句的语言里,用goto去模拟那些控制结构。在做这些的时候,应该准确地模拟。不要滥用goto所带来的灵活性。
  • 如果语言内置了等价的控制结构,那么就不要用goto。
  • 如果是为了提高代码效率而使用goto,请衡量此举实际带来的性能提升。在大多数情况下,你都可以不用goto而重新编写代码,这样既可以改善可读性,同时也不会损失效率。如果你的情况比较特殊,那么就对效率的提升做出说明,这样,goto的反对者们在看见goto以后就不会删除它。
  • 除非你要模拟结构化语句,否则尽量在每个子程序里只使用一个goto标号
  • 除非你要模拟结构化语句,否则尽量让goto向前跳转而不要向后跳转。
  • 确认所有的goto标号都被用到了。没用到的goto标号表明缺少了代码,即缺少了跳向该标号的代码。如果某些标号没有用,那么就删掉它们。
  • 确认goto不会产生某些执行不到的代码。
  • 如果你是一位经理,那么就应该持这样的观点:对某一个goto用法所展开的争论并不是事关全局的。如果程序员知道替代方案,并且也愿意为使用goto辩解,那么使用goto也无妨。

接下来是今天看到的一个对于switch的巧妙用法:

void send( int * to, int * from, int count)
          //     Duff设施,有帮助的注释被有意删去了 
 {
          int 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 );
          } 
 }   

这是著名的达夫设备(Duff's Device),可用于快速拷贝连续的内存空间。乍看之下,代码结构显得有些诡异:在各个case之后间断插入了一个完整的do-while循环,以前从来没有看到过这种用法。既然如此,我们不妨看看汇编代码:

--- C:\Users\Cration\Desktop\VC_Projects\switchCaseTest\main.c  ----------------------------------------------------------------------------------------------
9:
10:   void send( int * to, int * from, int count)
11:    {
0040DE40   push        ebp
0040DE41   mov         ebp,esp
0040DE43   sub         esp,48h
0040DE46   push        ebx
0040DE47   push        esi
0040DE48   push        edi
0040DE49   lea         edi,[ebp-48h]
0040DE4C   mov         ecx,12h
0040DE51   mov         eax,0CCCCCCCCh
0040DE56   rep stos    dword ptr [edi]
12:       int n = (count + 7 ) / 8 ;
0040DE58   mov         eax,dword ptr [ebp+10h]
0040DE5B   add         eax,7
0040DE5E   cdq
0040DE5F   and         edx,7
0040DE62   add         eax,edx
0040DE64   sar         eax,3
0040DE67   mov         dword ptr [ebp-4],eax
13:       switch (count % 8 ) {
0040DE6A   mov         eax,dword ptr [ebp+10h]
0040DE6D   and         eax,80000007h
0040DE72   jns         send+39h (0040de79)
0040DE74   dec         eax
0040DE75   or          eax,0FFFFFFF8h
0040DE78   inc         eax
0040DE79   mov         dword ptr [ebp-48h],eax
0040DE7C   cmp         dword ptr [ebp-48h],7
0040DE80   ja          $L802+2Fh (0040df83)
0040DE86   mov         ecx,dword ptr [ebp-48h]
0040DE89   jmp         dword ptr [ecx*4+40DF8Ah]
14:       case 0 :    do {  * to ++ = * from ++ ;
0040DE90   mov         eax,dword ptr [ebp+8]
0040DE93   mov         ecx,dword ptr [ebp+0Ch]
0040DE96   mov         edx,dword ptr [ecx]
0040DE98   mov         dword ptr [eax],edx
0040DE9A   mov         eax,dword ptr [ebp+8]
0040DE9D   add         eax,4
0040DEA0   mov         dword ptr [ebp+8],eax
0040DEA3   mov         ecx,dword ptr [ebp+0Ch]
0040DEA6   add         ecx,4
0040DEA9   mov         dword ptr [ebp+0Ch],ecx
15:       case 7 :          * to ++ = * from ++ ;
0040DEAC   mov         eax,dword ptr [ebp+8]
0040DEAF   mov         ecx,dword ptr [ebp+0Ch]
0040DEB2   mov         edx,dword ptr [ecx]
0040DEB4   mov         dword ptr [eax],edx
0040DEB6   mov         eax,dword ptr [ebp+8]
0040DEB9   add         eax,4
0040DEBC   mov         dword ptr [ebp+8],eax
0040DEBF   mov         ecx,dword ptr [ebp+0Ch]
0040DEC2   add         ecx,4
0040DEC5   mov         dword ptr [ebp+0Ch],ecx
16:       case 6 :          * to ++ = * from ++ ;
0040DEC8   mov         eax,dword ptr [ebp+8]
0040DECB   mov         ecx,dword ptr [ebp+0Ch]
0040DECE   mov         edx,dword ptr [ecx]
0040DED0   mov         dword ptr [eax],edx
0040DED2   mov         eax,dword ptr [ebp+8]
0040DED5   add         eax,4
0040DED8   mov         dword ptr [ebp+8],eax
0040DEDB   mov         ecx,dword ptr [ebp+0Ch]
0040DEDE   add         ecx,4
0040DEE1   mov         dword ptr [ebp+0Ch],ecx
17:       case 5 :          * to ++ = * from ++ ;
0040DEE4   mov         eax,dword ptr [ebp+8]
0040DEE7   mov         ecx,dword ptr [ebp+0Ch]
0040DEEA   mov         edx,dword ptr [ecx]
0040DEEC   mov         dword ptr [eax],edx
0040DEEE   mov         eax,dword ptr [ebp+8]
0040DEF1   add         eax,4
0040DEF4   mov         dword ptr [ebp+8],eax
0040DEF7   mov         ecx,dword ptr [ebp+0Ch]
0040DEFA   add         ecx,4
0040DEFD   mov         dword ptr [ebp+0Ch],ecx
18:       case 4 :          * to ++ = * from ++ ;
0040DF00   mov         eax,dword ptr [ebp+8]
0040DF03   mov         ecx,dword ptr [ebp+0Ch]
0040DF06   mov         edx,dword ptr [ecx]
0040DF08   mov         dword ptr [eax],edx
0040DF0A   mov         eax,dword ptr [ebp+8]
0040DF0D   add         eax,4
0040DF10   mov         dword ptr [ebp+8],eax
0040DF13   mov         ecx,dword ptr [ebp+0Ch]
0040DF16   add         ecx,4
0040DF19   mov         dword ptr [ebp+0Ch],ecx
19:       case 3 :          * to ++ = * from ++ ;
0040DF1C   mov         eax,dword ptr [ebp+8]
0040DF1F   mov         ecx,dword ptr [ebp+0Ch]
0040DF22   mov         edx,dword ptr [ecx]
0040DF24   mov         dword ptr [eax],edx
0040DF26   mov         eax,dword ptr [ebp+8]
0040DF29   add         eax,4
0040DF2C   mov         dword ptr [ebp+8],eax
0040DF2F   mov         ecx,dword ptr [ebp+0Ch]
0040DF32   add         ecx,4
0040DF35   mov         dword ptr [ebp+0Ch],ecx
20:       case 2 :          * to ++ = * from ++ ;
0040DF38   mov         eax,dword ptr [ebp+8]
0040DF3B   mov         ecx,dword ptr [ebp+0Ch]
0040DF3E   mov         edx,dword ptr [ecx]
0040DF40   mov         dword ptr [eax],edx
0040DF42   mov         eax,dword ptr [ebp+8]
0040DF45   add         eax,4
0040DF48   mov         dword ptr [ebp+8],eax
0040DF4B   mov         ecx,dword ptr [ebp+0Ch]
0040DF4E   add         ecx,4
0040DF51   mov         dword ptr [ebp+0Ch],ecx
21:       case 1 :          * to ++ = * from ++ ;
0040DF54   mov         eax,dword ptr [ebp+8]
0040DF57   mov         ecx,dword ptr [ebp+0Ch]
0040DF5A   mov         edx,dword ptr [ecx]
0040DF5C   mov         dword ptr [eax],edx
0040DF5E   mov         eax,dword ptr [ebp+8]
0040DF61   add         eax,4
0040DF64   mov         dword ptr [ebp+8],eax
0040DF67   mov         ecx,dword ptr [ebp+0Ch]
0040DF6A   add         ecx,4
0040DF6D   mov         dword ptr [ebp+0Ch],ecx
22:               } while ( -- n >    0 );
0040DF70   mov         eax,dword ptr [ebp-4]
0040DF73   sub         eax,1
0040DF76   mov         dword ptr [ebp-4],eax
0040DF79   cmp         dword ptr [ebp-4],0
0040DF7D   jg          $L792 (0040de90)
23:             }
24:    }
0040DF83   pop         edi
0040DF84   pop         esi
0040DF85   pop         ebx
0040DF86   mov         esp,ebp
0040DF88   pop         ebp
0040DF89   ret

         由此我们可以看出:各个case之后的代码是完全一致的,而case之后的while(--n> 0);与常规的do-while循环也是完全一致的。再看看switch(count%8),汇编代码对其进行了一系列的数值运算,最后以一句jmp dword ptr [ecx*4+40DF8Ah]决定了跳转到哪一个case的初始地址。那么,switch仅仅决定了第一次进入循环时的入口,之后程序便只是按照循环判断条件执行了。

        于是作者的意图就逐渐明了了:典型的循环展开,每次执行8个拷贝操作,以减少循环判断条件的执行次数,在count很大的时候,判断次数约为原来的1/8。此处switch只执行了一次,用于判断那些不足8个的余数拷贝操作(在开始阶段)。不得不说,这个思路十分巧妙,而且必须对编译器有一定的了解程度才能有这样的想法。(如果是我的话,估计会在循环内部加上八个label,然后在开始用八个if判断并goto到循环内部,虽然效果一致,但损失了编译器对switch的性能优化)

 

         结语:无论是巧妙的goto结构还是Duff's Device,从算法的角度上看,的确是值得学习。但考虑到代码的可读性和可移植性,笔者并不建议在日常开发中采用。理由①:代码的可读性降低,不易于维护。  理由②:即使能保证代码逻辑完全正确,且把这些代码写成接口完整的模块,还是不能保证在所有的编译器下通过编译,特别是对于嵌入式开发的种种编译器。


阅读更多
个人分类: C/C++
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭