Switch over if-else 语句的优点

对于30个 unsigned枚举,使用 switch语句和使用 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();
}
130876 次浏览

编译器将优化它无论如何-去开关,因为它是最可读的。

开关,如果只是为了可读性。巨大的如果语句更难维护和难以阅读在我看来。

错误 _ 01 ://故意失败

或者

(ERROR _ 01 = = numError) | |

后者更容易出错,需要比前者更多的类型和格式。

我不知道什么是最佳实践,但我会使用开关,然后通过“默认”陷入故意失败的陷阱

国际海事组织,这是一个完美的例子,什么交换机秋季通过是为了。

为了清晰和约定俗成,我会选择 if 语句,尽管我相信有些人会不同意。毕竟,你想做的事情 if一些条件是真实的!用一个动作换一个动作似乎有点... 没必要。

如果您的案例在将来可能保持分组状态——如果不止一个案例对应于一个结果——则切换可能被证明更易于阅读和维护。

我不是那种会告诉你速度和内存使用情况的人,但是查看 switch 语句要比查看一个大的 if 语句(特别是2-3个月后)容易理解得多

用开关。

在最坏的情况下,编译器将生成与 if-else 链相同的代码,因此您不会丢失任何东西。如果有疑问,首先将最常见的情况放入 switch 语句中。

在最好的情况下,优化器可以找到更好的方法来生成代码。编译器常做的事情是构建一个二进制决策树(在一般情况下保存比较和跳转)或者简单地构建一个跳转表(根本不需要比较)。

它们工作得一样好,性能和现代编译器差不多。

我更喜欢 if 语句而不是 case 语句,因为它们更具可读性,也更灵活——您可以添加其他不基于数值相等的条件,比如“ | | max < min”。但是对于你在这里发布的这个简单的案例,这并不重要,只要做你最能读懂的就行了。

开关绝对是首选。比起阅读冗长的 if 条件,查看开关列表并确切知道它在做什么要容易得多。

if条件下的复制对眼睛来说是困难的。假设其中一个 ==写成了 !=,您会注意到吗?或者如果‘ numError’的一个实例被写成‘ nmuError’,而这个实例恰好被编译了?

我通常更喜欢使用多态性而不是开关,但是如果没有上下文的更多细节,就很难说了。

至于性能,最好的办法是使用一个分析器来度量应用程序在与您在野外期望的情况相似的条件下的性能。否则,你可能在错误的地方以错误的方式进行优化。

我会说用开关。这样你只需要执行不同的结果。您的十个相同的情况下可以使用默认。如果只需要显式实现一个更改,则不需要编辑默认值。从 SWITCH 中添加或删除案例也比编辑 IF 和 ELSEIF 容易得多。

switch(numerror){
ERROR_20 : { fire_special_event(); } break;
default : { null; } break;
}

也许甚至可以根据可能性列表测试您的条件(在这种情况下是 numerror) ,也许是一个数组,这样您的 SWITCH 甚至不会被使用,除非确定会有一个结果。

我同意开关解决方案的兼容性,但我在这里你是 劫持开关
开关的作用是根据值进行 与众不同处理。
如果必须用伪代码解释算法,那么应该使用 If,因为从语义上讲,它就是: 如果无论什么错误都这样做..。
因此,除非您打算有一天改变您的代码,以具体代码为每个错误,我会使用 如果

看到你只有30个错误代码,编写你自己的跳转表,然后你自己做所有的优化选择(跳转总是最快的) ,而不是希望编译器会做正确的事情。它还使代码非常小(除了跳转表的静态声明之外)。它还有一个好处,就是如果需要的话,可以使用调试器在运行时修改行为,只需直接插入表数据即可。

开关更快。

只要尝试在一个循环中使用 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-09-21:

Bk1e 添加了以下注释: “在头文件中将常量定义为枚举是另一种处理方法”。

当然了。

外部类型的要点是将值与源解耦。将此值定义为宏、简单的 const int 声明或甚至枚举都会产生内联该值的副作用。因此,如果定义、枚举值或常量 int 值发生变化,则需要重新编译。外部声明意味着在值改变的情况下不需要重新编译,但是另一方面,使得不可能使用 switch。结论是 使用开关将增加开关代码和作为案例使用的变量之间的耦合。当它是确定的,然后使用开关。如果不是,那就不奇怪了。

.

编辑2013-01-15:

Vlad Lazarenko 评论了我的回答,并给出了一个链接,链接到他对开关生成的汇编代码的深入研究。非常激动人心的 http://lazarenko.me/switch/ :

可读性代码。如果您想知道什么性能更好,请使用分析器,因为优化和编译器各不相同,而且性能问题很少出现在人们所认为的地方。

对于您在示例中提供的特殊情况,最清晰的代码可能是:

if (RequiresSpecialEvent(numError))
fire_special_event();

显然,这只是将问题转移到代码的不同区域,但是现在您有机会重用这个测试。对于如何解决这个问题,您还有更多的选择。例如,您可以使用 std: : set:

bool RequiresSpecialEvent(int numError)
{
return specialSet.find(numError) != specialSet.end();
}

我并不是说这是 RequresSpecialEvent 的最佳实现,只是说它是一个选项。您仍然可以使用开关或 if-else 链,或查找表,或对值进行一些位操作,等等。您的决策过程变得越模糊,将它放在一个孤立的函数中获得的价值就越大。

从美学上讲,我倾向于这种方法。

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()

使用开关,这是它的目的和程序员的期望。

我会把多余的案例标签放进去——只是为了让人们感到舒服,我试图记住什么时候/什么规则是为了把他们排除在外。
您不希望下一个编程人员必须对语言细节进行任何不必要的思考(几个月后可能就是您了!)

我知道它很古老,但是

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);
}
}

改变循环计数会改变很多:

如果/否则: 5毫秒 开关: 1毫秒 Max Loops: 100000

如果/否则: 5毫秒 切换: 3毫秒 麦克斯循环: 1000000

如果/否则: 5毫秒 转换: 14ms 麦克斯循环: 1000000

如果/否则: 5毫秒 开关: 149毫秒 麦克斯循环: 10000000

(如果需要,可以添加更多语句)

while (true) != while (loop)

第一个循环可能是由编译器优化的,这就解释了为什么第二个循环在增加循环计数时会变慢。

编译器非常擅长优化 switch。最近的 gcc 也擅长优化 if中的一系列条件。

我做了一些 Godbolt的测试案例。

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编译成一个位图,但使用 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(前推土机)的利益: “代表”是什么意思?。没有它,分支预测在那些过时的 CPU 上不能很好地工作。

带寄存器参数的 bt(位测试)是快速的。它结合了左移一个1的 errNumber位和做一个 test的工作,但仍然是1周期延迟和只有一个英特尔上行。它使用内存参数的速度很慢,因为它的语义太过于 CISC: 对于“位字符串”的内存操作数,要测试的字节的地址是根据另一个参数(除以8)计算的,并且不限于内存操作数指向的1、2、4或8字节块。

Agner Fog 的指令表开始,可变计数的 shift 指令比最近 Intel 上的 bt慢(2个 uops 而不是1个,shift 不能完成所需的其他所有事情)。

说到编译程序,我不知道是否有什么不同。但是至于程序本身和尽可能保持代码简单,我个人认为这取决于您想要做什么。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 语句可能会变得复杂和混乱(尽管您尽了最大努力)。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 块。

对不起,我不同意目前公认的答案。现在是2021年。现代编译器及其优化器不应该再区分 switch和等效的 if链。如果他们仍然这样做,并且为任何一种变体创建了优化不佳的代码,那么就写信给编译器供应商(或者在这里公开,这样做更容易得到尊重) ,但是不要让微优化影响你的编码风格。

因此,如果你使用:

switch (numError) { case ERROR_A: case ERROR_B: ... }

或:

if(numError == ERROR_A || numError == ERROR_B || ...) { ... }

或:

template<typename C, typename EL>
bool has(const C& cont, const EL& el) {
return std::find(cont.begin(), cont.end(), el) != cont.end();
}


constexpr std::array errList = { ERROR_A, ERROR_B, ... };
if(has(errList, rnd)) { ... }

不会对执行速度产生影响。但是取决于您正在从事的项目,它们可能会在代码清晰度和代码可维护性方面产生很大的差异。例如,如果您必须在代码的许多位置检查某个错误列表,那么模板化的 has()可能更容易维护,因为 errList 只需要在一个位置进行更新。

谈到当前的编译器,我已经用 clang++ -O3 -std=c++1z(版本10和11)和 g++ -O3 -std=c++1z编译了下面引用的测试代码。两个 clang 版本提供了相似的编译代码和执行时间。所以从现在开始我只讨论第11版。最值得注意的是,functionA()(使用 if)和 functionB()(使用 switch)与 clang产生完全相同的汇编输出!而且 functionC()使用跳台,尽管许多其他海报认为跳台是 switch的独家功能。然而,尽管许多人认为跳转表是最优的,但这实际上是 clang上最慢的解决方案: 与 functionA()functionB()相比,functionC()需要多出约20% 的执行时间。

手工优化的版本 functionH()是迄今为止 clang上最快的。它甚至部分展开了循环,对每个循环进行两次迭代。

实际上,clang计算了位字段,这在 functionH()中明确提供,在 functionA()functionB()中也是如此。然而,它在 functionA()functionB()中使用了条件分支,这使得它们变慢,因为分支预测经常失败,而它在 functionH()中使用了效率更高的 adc(“附加进位”)。虽然它没有应用这个明显的优化也在其他变种,是未知的我。

g++生成的代码看起来比 clang复杂得多——但实际上对于 functionA()运行速度要快一些,对于 functionC()则要快得多。在非手动优化的功能中,functionC()g++上最快的,比 clang上的任何功能都要快。相反,使用 g++而不是使用 clang编译时,functionH()需要两倍的执行时间,这主要是因为 g++不执行循环展开。

以下是详细的结果:

clang:
functionA: 109877 3627
functionB: 109877 3626
functionC: 109877 4192
functionH: 109877 524


g++:
functionA: 109877 3337
functionB: 109877 4668
functionC: 109877 2890
functionH: 109877 982

如果将整个代码中的常量 32改为 63,那么性能将发生巨大变化:

clang:
functionA: 106943 1435
functionB: 106943 1436
functionC: 106943 4191
functionH: 106943 524


g++:
functionA: 106943 1265
functionB: 106943 4481
functionC: 106943 2804
functionH: 106943 1038

加速的原因是,在测试的最高值为63的情况下,编译器会删除一些不必要的绑定检查,因为无论如何,rnd的值都绑定到63。注意,去掉了绑定检查之后,在 g++上使用简单 if()的未优化的 functionA()执行速度几乎和手工优化的 functionH()一样快,而且它还产生相当类似的汇编输出。

结论是什么?如果您经常手工优化和测试编译器,您将获得最快的解决方案。任何假设是否 switchif更好,是无效的-他们是相同的 clang。对 array的值进行检查的容易编码的解决方案实际上是 g++上最快的情况(如果省略手动优化和事件匹配列表的最后一个值)。

未来的编译器版本将越来越好地优化您的代码,并更接近您的手工优化。所以不要在这上面浪费你的时间,除非周期对你来说真的很重要。

下面是测试代码:

#include <iostream>
#include <chrono>
#include <limits>
#include <array>
#include <algorithm>


unsigned long long functionA() {
unsigned long long cnt = 0;


for(unsigned long long i = 0; i < 1000000; i++) {
unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
if(rnd == 1 || rnd == 7 || rnd == 10 || rnd == 16 ||
rnd == 21 || rnd == 22 || rnd == 63)
{
cnt += 1;
}
}


return cnt;
}


unsigned long long functionB() {
unsigned long long cnt = 0;


for(unsigned long long i = 0; i < 1000000; i++) {
unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
switch(rnd) {
case 1:
case 7:
case 10:
case 16:
case 21:
case 22:
case 63:
cnt++;
break;
}
}


return cnt;
}


template<typename C, typename EL>
bool has(const C& cont, const EL& el) {
return std::find(cont.begin(), cont.end(), el) != cont.end();
}


unsigned long long functionC() {
unsigned long long cnt = 0;
constexpr std::array errList { 1, 7, 10, 16, 21, 22, 63 };


for(unsigned long long i = 0; i < 1000000; i++) {
unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
cnt += has(errList, rnd);
}


return cnt;
}


// Hand optimized version (manually created bitfield):
unsigned long long functionH() {
unsigned long long cnt = 0;


const unsigned long long bitfield =
(1ULL << 1) +
(1ULL << 7) +
(1ULL << 10) +
(1ULL << 16) +
(1ULL << 21) +
(1ULL << 22) +
(1ULL << 63);


for(unsigned long long i = 0; i < 1000000; i++) {
unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
if(bitfield & (1ULL << rnd)) {
cnt += 1;
}
}


return cnt;
}


void timeit(unsigned long long (*function)(), const char* message)
{
unsigned long long mintime = std::numeric_limits<unsigned long long>::max();
unsigned long long fres = 0;


for(int i = 0; i < 100; i++) {
auto t1 = std::chrono::high_resolution_clock::now();
fres = function();
auto t2 = std::chrono::high_resolution_clock::now();


auto duration = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
if(duration < mintime) {
mintime = duration;
}
}


std::cout << message << fres << " " << mintime << std::endl;
}




int main(int argc, char* argv[]) {
timeit(functionA, "functionA: ");
timeit(functionB, "functionB: ");
timeit(functionC, "functionC: ");
timeit(functionH, "functionH: ");
timeit(functionA, "functionA: ");
timeit(functionB, "functionB: ");
timeit(functionC, "functionC: ");
timeit(functionH, "functionH: ");
timeit(functionA, "functionA: ");
timeit(functionB, "functionB: ");
timeit(functionC, "functionC: ");
timeit(functionH, "functionH: ");


return 0;
}