switch 相对于 if-else 语句的优点
-
01-07-2019 - |
题
使用的最佳实践是什么 switch
声明与使用 if
30人的声明 unsigned
枚举,其中大约 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 语句中。
在最好的情况下,优化器可能会找到更好的方法来生成代码。编译器所做的常见事情是构建一个二元决策树(在平均情况下保存比较和跳转)或简单地构建一个跳转表(根本不需要比较即可工作)。
其他提示
对于您在示例中提供的特殊情况,最清晰的代码可能是:
if (RequiresSpecialEvent(numError))
fire_special_event();
显然,这只是将问题转移到代码的不同区域,但现在您有机会重用此测试。您还可以选择更多解决方法。您可以使用 std::set,例如:
bool RequiresSpecialEvent(int numError)
{
return specialSet.find(numError) != specialSet.end();
}
我并不是说这是 RequiresSpecialEvent 的最佳实现,只是说它是一个选项。您仍然可以使用开关或 if-else 链、查找表或对值进行一些位操作,等等。您的决策过程变得越模糊,您从将其置于独立函数中获得的价值就越大。
开关 是 快点。
只需在循环内尝试 if/else-ing 30 个不同的值,然后使用 switch 将其与相同的代码进行比较,看看切换的速度有多快。
现在 交换机有一个真正的问题 :开关必须在编译时知道每种情况下的值。这意味着以下代码:
// 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 ;
}
}
不会编译。
大多数人会使用定义(啊!),而其他人会在同一个编译单元中声明和定义常量变量。例如:
// 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 ;
}
}
因此,最终,开发人员必须在“速度+清晰度”与“速度+清晰度”之间做出选择。“代码耦合”。
(并不是说开关不能写得令人困惑......我目前看到的大多数开关都属于这种“令人困惑”的类别”......但这是另一个故事了……)
2008年9月21日编辑:
巴克1e 添加了以下评论:”在头文件中将常量定义为枚举是处理此问题的另一种方法”。
当然如此。
外部类型的要点是将值与源解耦。将此值定义为宏、简单的 const int 声明,甚至枚举,都会产生内联该值的副作用。因此,如果定义、枚举值或 const int 值发生更改,则需要重新编译。extern声明意味着在值改变的情况下不需要重新编译,但另一方面,使得无法使用switch。结论是 使用 switch 会增加 switch 代码和用作 case 的变量之间的耦合. 。确定后,再使用开关。如果不是,那就不足为奇了。
.
2013年1月15日编辑:
弗拉德·拉扎连科 评论了我的答案,给出了他对开关生成的汇编代码的深入研究的链接。非常有启发性: http://741mhz.com/switch/
无论如何,编译器都会对其进行优化 - 选择开关,因为它是最具可读性的。
Switch,如果只是为了可读性。在我看来,巨大的 if 语句更难维护,也更难阅读。
错误_01 :// 故意失败
或者
(ERROR_01 == numError) ||
后者比第一个更容易出错,并且需要更多的打字和格式化。
代码的可读性。如果您想知道什么性能更好,请使用分析器,因为优化和编译器各不相同,而且性能问题很少出现在人们认为的地方。
使用switch,这就是它的用途,也是程序员所期望的。
不过,我会把多余的案例标签放进去——只是为了让人们感到舒服,我试图记住何时/什么规则将它们排除在外。
您不希望下一个从事该工作的程序员必须对语言细节进行任何不必要的思考(几个月后可能就是您了!)
编译器确实擅长优化 switch
. 。最近的 gcc 也擅长优化一系列条件 if
.
我做了一些测试用例 神箭.
当。。。的时候 case
值紧密地分组在一起,gcc、clang 和 icc 都足够聪明,可以使用位图来检查值是否是特殊值之一。
例如gcc 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()
请注意,位图是立即数据,因此访问它或跳转表时不会出现潜在的数据缓存未命中情况。
gcc 4.9.2 -O3 编译 switch
到位图,但 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()
注意它是如何减去 1 的 errNumber
(和 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(Bulldozer 之前): “rep ret”是什么意思?. 。如果没有它,分支预测在那些过时的 CPU 上就无法正常工作。
bt
(位测试)使用寄存器 arg 速度很快。它结合了左移 1 的工作 errNumber
位并做一个 test
, ,但仍然有 1 个周期延迟并且只有一个英特尔微指令。由于其过于 CISC 语义,内存参数的速度很慢:对于“位串”的内存操作数,要测试的字节的地址是根据另一个参数(除以8)计算的,并且不限于指向的1、2、4或8字节块通过内存操作数。
从 Agner Fog 的说明书, ,可变计数移位指令比 bt
在最近的英特尔上(2 uops 而不是 1,并且移位不能完成所有其他需要的事情)。
在我看来,这是一个完美的例子,说明了开关掉落的目的。
如果您的案例将来可能仍保持分组状态(如果多个案例对应一个结果),那么这种切换可能会更易于阅读和维护。
它们工作得同样好。对于现代编译器来说,性能大致相同。
我更喜欢 if 语句而不是 case 语句,因为它们更具可读性,也更灵活——您可以添加其他不基于数字相等的条件,例如“ || max < min ”。但对于您在这里发布的简单案例,这并不重要,只需做对您来说最易读的事情即可。
开关绝对是首选。查看开关的情况列表并确定它正在做什么比读取长 if 条件更容易。
中的重复 if
病情对眼睛来说很困难。假设其中之一 ==
被写 !=
;你会注意到吗?或者,如果“numError”的一个实例被写为“nmuError”,而它恰好编译了?
我通常更喜欢使用多态性而不是开关,但如果没有上下文的更多细节,很难说。
至于性能,最好的选择是使用分析器来测量应用程序在与实际情况类似的条件下的性能。否则,您可能会在错误的地方以错误的方式进行优化。
我同意开关解决方案的兼容性,但在我看来,你是 劫持开关 这里。
开关的目的是 不同的 根据值进行处理。
如果你必须用伪代码解释你的算法,你会使用 if,因为从语义上来说,它就是这样的: 如果有任何错误,请执行此操作...
因此,除非您打算有一天更改代码以便为每个错误提供特定的代码,否则我会使用 如果.
我不确定最佳实践,但我会使用 switch - 然后通过“默认”捕获故意失败
从美学上来说,我倾向于这种方法。
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)
第一个循环可能是由编译器优化的,这可以解释为什么在增加循环计数时第二个循环会变慢。
为了清晰和约定,我会选择 if 语句,尽管我确信有些人会不同意。毕竟,你想做某事 if
某些条件为真!只需一个动作就能实现开关似乎有点……不必要的。
我不是告诉你速度和内存使用情况的人,但是查看 switch 语句比大型 if 语句更容易理解(尤其是 2-3 个月后)
我会说使用 SWITCH。这样你只需要实现不同的结果。您的十个相同的案例可以使用默认值。如果发生任何更改,您只需显式实施更改即可,无需编辑默认值。从 SWITCH 添加或删除案例也比编辑 IF 和 ELSEIF 容易得多。
switch(numerror){
ERROR_20 : { fire_special_event(); } break;
default : { null; } break;
}
也许甚至可以根据一系列可能性(可能是一个数组)测试您的条件(在本例中为 numerror),这样您的 SWITCH 甚至不会被使用,除非肯定会有结果。
鉴于您只有 30 个错误代码,请编写自己的跳转表,然后您自己做出所有优化选择(跳转总是最快的),而不是希望编译器会做正确的事情。它还使得代码非常小(除了跳转表的静态声明)。它还具有一个附带的好处,即使用调试器,您可以在需要时修改运行时的行为,只需直接查看表数据即可。
我知道它很旧但是
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);
}
}
改变循环计数会带来很大变化:
而如果/否则:5ms开关:1MS最大循环:100000
而如果/否则:5ms开关:3ms最大循环:1000000
而如果/否则:5ms开关:14ms最大循环:10000000
而如果/否则:5ms开关:149ms最大循环:100000000
(如果需要,可以添加更多语句)
到了编译程序的时候,不知道有没有什么区别。但至于程序本身以及保持代码尽可能简单,我个人认为这取决于你想要做什么。if else if else 语句有其优点,我认为是:
允许您针对特定范围测试一个变量,可以将功能(标准库或个人)用作条件。
(例子:
`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";
然而,If else if else 语句可能会很快变得复杂和混乱(尽管您尽了最大努力)。Switch 语句往往更清晰、更干净、更容易阅读;但只能用于测试特定值(例如:
`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 语句,但这实际上取决于你。如果您想使用函数作为条件,或者您想针对范围、数组或向量测试某些内容和/或您不介意处理复杂的嵌套,我建议使用 If else if else 块。如果您想针对单个值进行测试或者您想要一个干净且易于阅读的块,我建议您使用 switch() case 块。