切换if-else语句的优点
使用switch
语句的最佳做法是使用30个unsigned
枚举的if
语句,其中大约10个具有预期的操作(目前是相同的操作)。 性能和空间需要考虑,但并不重要。 我已经抽象了这个片段,所以不要因为命名惯例而恨我。
switch
语句:
// numError is an error enumeration type, with 0 being the non-error case // fire_special_event() is a stub method for the shared processing switch (numError) { case ERROR_01 : // intentional fall-through case ERROR_07 : // intentional fall-through case ERROR_0A : // intentional fall-through case ERROR_10 : // intentional fall-through case ERROR_15 : // intentional fall-through case ERROR_16 : // intentional fall-through case ERROR_20 : { fire_special_event(); } break; default: { // error codes that require no additional action } break; }
if
声明:
if ((ERROR_01 == numError) || (ERROR_07 == numError) || (ERROR_0A == numError) || (ERROR_10 == numError) || (ERROR_15 == numError) || (ERROR_16 == numError) || (ERROR_20 == numError)) { fire_special_event(); }
使用开关。
在最糟糕的情况下,编译器会生成与if-else链相同的代码,所以不会丢失任何东西。 如果有疑问的话,最常见的情况是首先进入switch语句。
在最好的情况下,优化器可能会find更好的方法来生成代码。 编译器所做的一些常见的事情是构build一个二叉决策树(在一般情况下保存比较和跳转),或者简单地构build一个跳转表(根本不需要比较)。
对于您在示例中提供的特例,最清晰的代码可能是:
if (RequiresSpecialEvent(numError)) fire_special_event();
显然,这只是将问题转移到代码的不同区域,但是现在您有机会重用此testing。 你也有更多的select来解决它。 你可以使用std :: set,例如:
bool RequiresSpecialEvent(int numError) { return specialSet.find(numError) != specialSet.end(); }
我并不是说这是RequiresSpecialEvent的最佳实现,只是它是一个选项。 你仍然可以使用一个开关或if-else链,或者一个查找表,或者对这个值进行一些位操作。 你的决策过程变得越模糊,在一个孤立的function中你就会得到越多的价值。
开关速度更快。
只要试着在循环内使用其他的30个不同的值,并用开关比较它的相同代码,看看开关速度有多快。
现在, 开关有一个真正的问题 :开关必须在编译时知道每个情况下的值。 这意味着下面的代码:
// WON'T COMPILE extern const int MY_VALUE ; void doSomething(const int p_iValue) { switch(p_iValue) { case MY_VALUE : /* do something */ ; break ; default : /* do something else */ ; break ; } }
不会编译。
大多数人将使用定义(Aargh!),而其他人将在同一个编译单元中声明和定义常量variables。 例如:
// WILL COMPILE const int MY_VALUE = 25 ; void doSomething(const int p_iValue) { switch(p_iValue) { case MY_VALUE : /* do something */ ; break ; default : /* do something else */ ; break ; } }
所以,最后,开发者必须在“速度+清晰度”与“代码耦合”之间进行select。
(不是说一个开关不能被写成混乱的地狱……大多数我目前看到的开关是这个“混乱”的类别“…但这是另一回事…)
编辑2008-09-21:
bk1e添加了以下注释:“ 定义常量作为头文件中的枚举是处理这种情况的另一种方法”。
当然如此。
externtypes的意义在于将来源的值解耦。 将这个值定义为一个macros,作为一个简单的const int声明,或者甚至作为一个枚举都有内联值的副作用。 因此,如果define,enum值或const int值发生变化,则需要重新编译。 外部声明意味着在价值变化的情况下不需要重新编译,但另一方面使得不能使用开关。 使用开关的结论将增加开关代码和用作情况的variables之间的耦合 。 当它是好的,然后使用开关。 如果不是这样,那就不足为奇了。
。
编辑2013-01-15:
Vlad Lazarenko对我的回答进行了评论,给出了他深入研究交换机产生的汇编代码的链接。 非常启发:http://741mhz.com/switch/
编译器无论如何都会对其进行优化 – 因为它是最具可读性的开关。
开关,如果只是为了可读性。 巨人如果说话更难以维持,更难以阅读我的意见。
ERROR_01 ://故意遗漏
要么
(ERROR_01 == numError)||
后者更容易出错,需要比第一种更多的input和格式化。
使用开关,这是什么和程序员的期望。
但是我会把多余的标签贴上来 – 为了让人感觉舒服,我试图记住什么时候/什么规则将它们排除在外。
你不希望下一个编程人员对它进行任何不必要的思考(这可能是你在几个月的时间!)
代码的可读性。 如果您想知道哪些性能更好,请使用分析器,因为优化和编译器各不相同,性能问题很less出现在人们认为它们的地方。
国际海事组织这是一个完美的例子,是什么交换落后的。
编译器非常擅长优化switch
。 最近的gcc也擅长优化if
的一堆条件。
我在Godbolt上做了一些testing用例。
当case
值紧密组合在一起时,gcc,clang和icc都足够聪明,可以使用位图来检查值是否是特殊值之一。
如海湾合作委员会5.2 -O3编译switch
( if
是非常相似的东西):
errhandler_switch(errtype): # gcc 5.2 -O3 cmpl $32, %edi ja .L5 movabsq $4301325442, %rax # highest set bit is bit 32 (the 33rd bit) btq %rdi, %rax jc .L10 .L5: rep ret .L10: jmp fire_special_event()
请注意,位图是立即数据,因此没有潜在的数据caching未命中访问或跳转表。
gcc 4.9.2 -O3将switch
编译成位图,但是用mov / shift做1U<<errNumber
。 它将if
版本编译为一系列分支。
errhandler_switch(errtype): # gcc 4.9.2 -O3 leal -1(%rdi), %ecx cmpl $31, %ecx # cmpl $32, %edi wouldn't have to wait an extra cycle for lea's output. # However, register read ports are limited on pre-SnB Intel ja .L5 movl $1, %eax salq %cl, %rax # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx testl $2150662721, %eax jne .L10 .L5: rep ret .L10: jmp fire_special_event()
注意它是如何从errNumber
减去1的(用lea
来结合那个操作和移动)。 这让它适合位图到一个32位立即,避免64位立即movabsq
需要更多的指令字节。
较短的(在机器码中)序列将是:
cmpl $32, %edi ja .L5 mov $2150662721, %eax dec %edi # movabsq and btq is fewer instructions / fewer Intel uops, but this saves several bytes bt %edi, %eax jc fire_special_event .L5: ret
(无法使用jc fire_special_event
是无所不在的,而且是一个编译器错误 。)
rep ret
用于分支目标,并且在有条件的分支之后,为了AMD K8和K10(预推土机)的利益: rep ret是什么意思? 。 没有它,分支预测在那些过时的CPU上不能工作。
bt
(位testing)与一个寄存器arg是快速的。 它结合了将errNumber
位左移1位并进行test
,但仍然是1个周期的延迟,只有一个Intel uop。 由于它的方式太慢,CISC语义:内存操作数为“位串”,被testing字节的地址是基于另一个参数arg(除以8)计算的,isn不限于内存操作数所指向的1,2,4或8字节块。
从Agner Fog的指令表中 ,可变计数移位指令比最近英特尔的bt
要慢(2个uops而不是1个,移位并不需要其他所有)。
如果您的案例可能会在未来分组 – 如果不止一个案例对应于一个结果 – 交换机可能会certificate更容易阅读和维护。
他们工作同样好。 性能与现代编译器大致相同。
我比较喜欢if语句,因为它们更具可读性,更灵活 – 您可以添加其他不基于数字相等的条件,如“|| max <min”。 但是,对于你在这里发布的简单案例,这并不重要,只要做你最可读的。
开关肯定是首选。 查看交换机的案例列表比较容易,并且知道它在做什么,而不是阅读长条件的情况。
if
条件下的重复在眼睛上很难。 假设其中的一个==
写成了!=
; 你会注意到吗? 或者,如果有一个'numError'实例被写入'nmuError',刚刚编译?
我通常更喜欢使用多态而不是开关,但没有更多的上下文细节,这很难说。
至于性能,最好的办法是使用一个分析器来测量你的应用程序的性能,这个条件与你所期望的相似。 否则,你可能在错误的地方和错误的方式进行优化。
我同意交换机解决scheme的紧凑性,但IMO却在这里劫持交换机 。
交换机的目的是根据不同的值进行不同的处理。
如果你必须用伪代码解释你的algorithm,你会使用一个if,因为在语义上,就是这样: 如果whatever_error这样做 …
所以除非你打算有一天改变你的代码,为每个错误有特定的代码,我会使用if 。
我不确定最佳做法,但是我会使用开关 – 然后通过“默认”陷阱来有目的的破译,
美学上我倾向于赞成这种方法。
unsigned int special_events[] = { ERROR_01, ERROR_07, ERROR_0A, ERROR_10, ERROR_15, ERROR_16, ERROR_20 }; int special_events_length = sizeof (special_events) / sizeof (unsigned int); void process_event(unsigned int numError) { for (int i = 0; i < special_events_length; i++) { if (numError == special_events[i]) { fire_special_event(); break; } } }
让数据变得更聪明一些,这样我们可以使逻辑变得有点麻烦。
我意识到这看起来很奇怪。 这里的灵感(从我如何在Python中):
special_events = [ ERROR_01, ERROR_07, ERROR_0A, ERROR_10, ERROR_15, ERROR_16, ERROR_20, ] def process_event(numError): if numError in special_events: fire_special_event()
while (true) != while (loop)
可能是第一个被编译器优化的,这可以解释为什么第二个循环在增加循环计数时会变慢。
为了明确和惯例,我会selectif语句,尽pipe我确信有些人会不同意。 毕竟, if
条件成立,你就想做点什么! 有一个行动切换似乎有点…不必要的。
请使用开关。 if语句将花费与条件数量成比例的时间。
我不是告诉你关于速度和内存使用情况的人,但是查看一个开关语句是很容易理解的,然后是一个大的if语句(特别是2-3个月)
我会说使用开关。 这样你只需要实现不同的结果。 你的十个相同的情况下可以使用默认值。 如果你需要的一个变化是明确地实现变化,不需要编辑默认值。 添加或删除SWITCH的案例要比编辑IF和ELSEIF要容易得多。
switch(numerror){ ERROR_20 : { fire_special_event(); } break; default : { null; } break; }
也许甚至在一系列的可能性上testing你的条件(在这种情况下是数值),或许你的SWITCH没有被使用,除非肯定会有结果。
看你只有30个错误代码,编写自己的跳转表,然后你自己做所有的优化select(跳转总是最快),而不是希望编译器能够做正确的事情。 它也使代码非常小(除了跳转表的静态声明)。 它也有一个好处,就是在debugging器的时候,你可以在运行时修改行为,只要你直接戳表数据。
我知道它的老,但
public class SwitchTest { static final int max = 100000; public static void main(String[] args) { int counter1 = 0; long start1 = 0l; long total1 = 0l; int counter2 = 0; long start2 = 0l; long total2 = 0l; boolean loop = true; start1 = System.currentTimeMillis(); while (true) { if (counter1 == max) { break; } else { counter1++; } } total1 = System.currentTimeMillis() - start1; start2 = System.currentTimeMillis(); while (loop) { switch (counter2) { case max: loop = false; break; default: counter2++; } } total2 = System.currentTimeMillis() - start2; System.out.println("While if/else: " + total1 + "ms"); System.out.println("Switch: " + total2 + "ms"); System.out.println("Max Loops: " + max); System.exit(0); } }
改变循环次数有很大的变化:
while if / else:5ms Switch:1ms Max Loops:100000
而if / else:5ms开关:3ms最大循环:1000000
while if / else:5ms Switch:14ms Max Loops:10000000
while if / else:5ms Switch:149ms Max Loops:100000000
(如果你想添加更多的语句)
编制程序时,我不知道有没有什么区别。 但是对于程序本身和尽可能简单的代码,我个人认为这取决于你想要做什么。 如果其他语句有其优点,我认为是:
允许您针对特定范围testingvariables,您可以使用函数(标准库或个人)作为条件。
(例:
`int a; cout<<"enter value:\n"; cin>>a; if( a > 0 && a < 5) { cout<<"a is between 0, 5\n"; }else if(a > 5 && a < 10) cout<<"a is between 5,10\n"; }else{ "a is not an integer, or is not in range 0,10\n";
但是,如果其他语句能够匆忙地变得复杂和混乱(尽pipe您尝试了最好的尝试)。 开关语句往往更清晰,更清晰,更易于阅读; 但只能用于testing特定的值(例如:
`int a; cout<<"enter value:\n"; cin>>a; switch(a) { case 0: case 1: case 2: case 3: case 4: case 5: cout<<"a is between 0,5 and equals: "<<a<<"\n"; break; //other case statements default: cout<<"a is not between the range or is not a good value\n" break;
我更喜欢if – else if – else语句,但是这真的取决于你。 如果你想使用函数作为条件,或者你想testing一些对象的范围,数组或vector和/或你不介意处理复杂的嵌套,我会build议使用if else if else块。 如果你想testing单个值,或者你想要一个简单易读的块,我build议你使用switch()大小写块。