为什么开关比if语句更快

134

许多Java书籍都描述switch语句比if else语句更快。但我没有在任何地方找到为什么switch比if快的说明。

示例

我有一个情况,我必须从两个项目中选择任意一个。我可以使用以下任一选项:

switch (item) {
    case BREAD:
        //eat Bread
        break;
    default:
        //leave the restaurant
}
或者
if (item == BREAD) {
    //eat Bread
} else {
    //leave the restaurant
}

假设item和BREAD是一个常量int值。 在上面的示例中,哪个更快并且为什么?


也许这也是Java的一个答案:https://dev59.com/9nRA5IYBdhLWcg3w9ivq#767849 - Tobias
22
通常情况下,根据Wikipedia的说明:如果输入值的范围可以确定为“小”的并且仅有少量间隙,一些内置优化器的编译器实际上会将switch语句实现为分支表或者索引函数指针的数组,而不是一系列冗长的条件指令。这样就可以让switch语句立即确定应该执行哪个分支,而不必遍历比较列表。 - Felix Kling
这个问题的最佳答案已经解释得很好了。这篇文章也讲得非常清楚。 - bezmax
2
你应该对那些没有解释、证明或理由就直接做出这种陈述的书籍保持警惕。 - matt b
可能是在C#中使用if/else和switch-case是否有显着差异?的重复问题。 - Dharvik shah
显示剩余10条评论
5个回答

133
因为有特殊的字节码可以在有很多情况的时候进行高效的switch语句评估。
如果用IF语句实现,你需要一个检查、跳转到下一个子句,一个检查、跳转到下一个子句等等。但是使用switch,JVM会加载要比较的值,并遍历值表以找到匹配项,在大多数情况下更快。

8
"Doesn't iterate translate to 'check, jump'?" 翻译成中文为:"迭代不是指“检查、跳转”吗?" - fivetwentysix
21
@fivetwentysix: 不是的,请参考这里的信息:http://www.artima.com/underthehood/flowP.html 。引用自文章:当JVM遇到tableswitch指令时,它可以简单地检查键是否在low和high定义的范围内。如果不是,则采用默认的分支偏移量。如果是,则只需将key从low中减去,以获得跳转偏移列表中的偏移量。通过这种方式,它可以确定适当的分支偏移量,而无需检查每个情况值。 - bezmax
1
(i)switch语句可能不会被翻译成tableswitch字节码指令 - 它可能会变成一个lookupswitch指令,其执行方式类似于if/else。(ii)即使是tableswitch字节码指令也可能会被JIT编译成一系列的if/else,这取决于case的数量等因素。 - assylias
1
tableswitchlookupswitch的区别:https://dev59.com/kWkv5IYBdhLWcg3w4kp2 - Ciro Santilli OurBigBook.com

44

switch语句并不总是比if语句更快。当有较长的if-else序列时,使用switch可以基于所有值进行查找,因此它比较容易扩展。然而,对于短条件情况来说,switch并不能比if语句更快,反而可能会更慢。


5
请限制“long”的范围。是大于5?大于10?还是更像20-30? - egerardus
13
我猜这要看情况。对我来说,3个或更多的时候建议使用“switch”,因为这样更清晰,但不一定更快。 - Peter Lawrey
在哪些情况下它可能会变慢? - Eric
1
@Eric 对于少量的值,特别是稀疏的字符串或整数,速度会比较慢。 - Peter Lawrey

20

当前的JVM有两种类型的开关字节码:LookupSwitch和TableSwitch。

在switch语句中,每个case都有一个整数偏移量,如果这些偏移量连续(或几乎连续且没有大的间隔)(如case 0:case 1:case 2等),则使用TableSwitch。

如果偏移量分散并存在大间隔(如case 0: case 400: case 93748等),则使用LookupSwitch。

简而言之,不同之处在于TableSwitch在常数时间内完成,因为可能值范围内的每个值都被赋予了特定的字节码偏移量。 因此,当您给出偏移量3时,它知道要向前跳3以找到正确的分支。

LookupSwitch使用二进制搜索查找正确的代码分支。 这需要O(log n)时间,这仍然很快,但不是最好的。

有关更多信息,请参见此处:Difference between JVM's LookupSwitch and TableSwitch?

因此,就速度而言,使用以下方法:

如果有3个或更多的情况,其值是连续的或几乎连续的,请始终使用switch。

如果有2个情况,请使用if语句。

对于任何其他情况,switch最有可能更快,但这并不保证,因为LookupSwitch中的二进制搜索可能会遇到糟糕的情况。

同时请注意,JVM将在if语句上运行JIT优化,尝试在代码中首先放置最热门的分支。 这称为“分支预测”。 有关此信息的更多信息,请参见此处:https://dzone.com/articles/branch-prediction-in-java

您的体验可能会有所不同。 我不知道JVM是否在LookupSwitch上运行类似的优化,但我已经学会了相信JIT优化,并且不尝试聪明地超越编译器。


2
自从发布这篇文章以来,我注意到“switch表达式”和“模式匹配”即将到来Java中,可能会在Java 12中实现。http://openjdk.java.net/jeps/325 http://openjdk.java.net/jeps/305目前还没有具体的计划,但似乎这些功能将使switch成为更强大的语言特性。例如,模式匹配将允许更平滑和高效的instanceof查找。然而,我认为可以安全地假设对于基本的switch/if场景,仍将适用我提到的规则。 - HesNotTheStig
在我的性能测试中,if-else 在大多数情况下与 switch 相当甚至更快。谁知道,如果 Java 使用 LookupSwitch 并且程序流只匹配了一个 case,然后继续执行到底部(没有 break),它是一种具有顺序链接的二叉树吗? - the_kaba

1

所以,如果你计划有大量的数据包,内存成本现在并不高,而且数组也非常快。你也不能依赖于switch语句自动生成跳转表,因此更容易自己生成跳转表场景。如下面的例子所示,我们假设最多有255个数据包。

要获得以下结果,你需要抽象化...我不会解释它是如何工作的,希望你已经理解了。

我更新了这个,将数据包大小设置为255,如果你需要更多的话,你需要进行边界检查(id < 0) || (id > length)。

Packets[] packets = new Packets[255];

static {
     packets[0] = new Login(6);
     packets[2] = new Logout(8);
     packets[4] = new GetMessage(1);
     packets[8] = new AddFriend(0);
     packets[11] = new JoinGroupChat(7); // etc... not going to finish.
}

public void handlePacket(IncomingData data)
{
    int id = data.readByte() & 0xFF; //Secure value to 0-255.

    if (packet[id] == null)
        return; //Leave if packet is unhandled.

    packets[id].execute(data);
}

由于我现在经常在C++中使用跳转表,所以我将展示一个函数指针跳转表的示例。这是一个非常通用的示例,但我已经运行它并且它可以正确地工作。请记住,您必须将指针设置为NULL,C++不会像Java一样自动执行此操作。

#include <iostream>

struct Packet
{
    void(*execute)() = NULL;
};

Packet incoming_packet[255];
uint8_t test_value = 0;

void A() 
{ 
    std::cout << "I'm the 1st test.\n";
}

void B() 
{ 
    std::cout << "I'm the 2nd test.\n";
}

void Empty() 
{ 

}

void Update()
{
    if (incoming_packet[test_value].execute == NULL)
        return;

    incoming_packet[test_value].execute();
}

void InitializePackets()
{
    incoming_packet[0].execute = A;
    incoming_packet[2].execute = B;
    incoming_packet[6].execute = A;
    incoming_packet[9].execute = Empty;
}

int main()
{
    InitializePackets();

    for (int i = 0; i < 512; ++i)
    {
        Update();
        ++test_value;
    }
    system("pause");
    return 0;
}

另外一个我想提出的观点是著名的分而治之。因此,我的上述255个数组的想法在最坏情况下可以减少到不超过8个if语句。

也就是说,但请记住,这会变得混乱且很快难以管理,我的其他方法通常更好,但在数组无法胜任的情况下使用。您必须确定您的用例以及每种情况何时最有效。就像如果您只有几个检查,您也不希望使用这些方法之一。

If (Value >= 128)
{
   if (Value >= 192)
   {
        if (Value >= 224)
        {
             if (Value >= 240)
             {
                  if (Value >= 248)
                  {
                      if (Value >= 252)
                      {
                          if (Value >= 254)
                          {
                              if (value == 255)
                              {

                              } else {

                              }
                          }
                      }
                  }
             }      
        }
   }
}

2
为什么要双重间接引用?既然ID必须受到限制,为什么不只是检查传入的ID是否符合“0 <= id < packets.length”,并确保“packets[id]!= null”,然后执行“packets[id].execute(data)”呢? - Lawrence Dol
抱歉回复晚了,我再次查看了这个问题。我真的不知道当时在想什么,我更新了帖子并将数据包限制为无符号字节的大小,因此不需要进行长度检查。 - Jeremy Trifilo

0

在字节码级别上,主题变量仅从运行时加载的结构化.class文件中的内存地址一次性加载到处理器寄存器中,并且这是在switch语句中;而在if语句中,您的代码编译DE会产生不同的jvm指令,并且这需要将每个变量加载到寄存器中,尽管相同的变量在下一个if语句中被使用。如果您了解汇编语言编程,则这将是常见的情况;尽管Java编译的代码不是字节码或直接机器代码,但其条件概念仍然保持一致。 好的,我试图避免更深层次的技术性解释。我希望我已经清楚地阐述了这个概念并揭开了神秘面纱。谢谢。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接