为什么切换比不快


116

许多Java书籍都将该switch语句描述为比该if else语句快。但我没有找到任何地方为什么切换比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的答案:stackoverflow.com/questions/767821/…–
Tobias

19
通常,来自Wikipedia如果输入值的范围可以确定地是“很小的”并且只有几个空白,那么一些合并了优化程序的编译器实际上可能将switch语句实现为分支表或索引函数指针数组,而不是冗长的条件指令系列。这使switch语句可以立即确定要执行哪个分支,而不必遍历比较列表。
Felix Kling

这个问题的最高答案很好地解释了它。此文章说明了一切相当不错了。
bezmax

我希望在大多数情况下,优化的编译器将能够生成具有类似性能特征的代码。无论如何,您将不得不致电数百万次才能注意到任何差异。
米奇·麦特

2
您应该对发表这样的陈述而没有解释/证明/理由的书籍保持警惕。
马特b

Answers:


110

因为在很多情况下,有一些特殊的字节码可以有效地评估switch语句。

如果使用IF语句实现,则将进行检查,跳转到下一个子句,进行检查,跳转到下一个子句等。通过切换,JVM加载要比较的值,并遍历值表以查找匹配项,这在大多数情况下会更快。


6
不会迭代翻译为“检查,跳转”吗?
2011年

17
@fivetwentysix:否,请参考以下信息:artima.com/underthehood/flowP.html。引用:当JVM遇到tableswitch指令时,它可以简单地检查键是否在由low和high定义的范围内。如果不是,它将采用默认的分支偏移量。如果是这样,它只是从键中减去low来获得分支偏移列表中的偏移。以这种方式,它可以确定适当的分支偏移,而不必检查每个案例值。
bezmax

1
(i)a switch可能不会翻译成tableswitch字节码指令-它可能会变成lookupswitch与if / else相似的指令(ii)即使tableswitchJIT可能将字节码指令编译成一系列if / else,具体取决于因素例如cases 的数量。
assylias 2014年


34

一个switch说法是并不总是比一个快if言。它的伸缩性比一长串的if-else语句更好,因为它switch可以基于所有值执行查找。但是,对于短期情况,它不会更快,并且可能会更慢。


5
请限制“长”。大于5?大于10?或更像20-30?
vanderwyst 2012年

11
我怀疑这取决于。对我来说,它的3个或更多建议switch可能会更快或更清晰。
彼得·劳瑞

在什么情况下会变慢?
埃里克

1
@Eric对于少数几个稀疏的esp String或int值,速度较慢。
彼得·劳瑞

8

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

switch语句中的每个case都有一个整数偏移量,如果这些偏移量是连续的(或几乎是连续的,没有大的间隙)(情况0:情况1:情况2,依此类推),则使用TableSwitch。

如果偏移量以较大的间隙分布(情况0:情况400:情况93748等),则使用LookupSwitch。

简而言之,不同之处在于TableSwitch是在恒定时间内完成的,因为可能值范围内的每个值都被赋予了特定的字节码偏移量。因此,当您将语句的偏移量设置为3时,它知道跳到3才能找到正确的分支。

查找开关使用二进制搜索来找到正确的代码分支。它以O(log n)的时间运行,这仍然不错,但不是最好的。

有关此的更多信息,请参见此处:JVM的LookupSwitch和TableSwitch之间的区别?

因此,就最快的情况而言,请使用以下方法:如果您有3个或更多的案例,其值是连续的或几乎连续的,请始终使用开关。

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

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

另外,请记住,JVM将对if语句运行JIT优化,这些语句将尝试将最热的分支放在代码的最前面。这称为“分支预测”。有关此的更多信息,请参见此处:https : //dzone.com/articles/branch-prediction-in-java

您的经历可能会有所不同。我不知道JVM不会在LookupSwitch上运行类似的优化,但是我已经学会了信任JIT优化,而不是试图超越编译器。


1
自发布以来,引起我注意的是,“开关表达式”和“模式匹配”正在Java中出现,可能最早 在Java 12中出现。openjdk.java.net/ jeps/ 325 openjdk.java.net/jeps/305 还没有什么具体的,但是看来这些将使switch语言功能更加强大。例如,模式匹配将允许更加平滑和instanceof高效的查找。但是,我认为可以肯定地说,对于基本的切换/如果场景,我提到的规则仍然适用。
HesNotTheStig

1

因此,如果您打算承载大量数据包,那么这些天内存并不是真正的大笔费用,而且阵列的速度非常快。您也不能依靠switch语句自动生成跳转表,因此,自己生成跳转表方案会更容易。在下面的示例中可以看到,我们假设最多255个数据包。

为了得到以下结果,您需要抽象一下。.我不会解释它是如何工作的,因此希望您对此有所了解。

我将其更新为将数据包大小设置为255(如果您需要更多,则必须对(id <0)||进行边界检查)。(id>长度)。

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 (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]!=nullpackets[id].execute(data)
劳伦斯·多尔

是的,很抱歉,迟来的答复再次看了看..我也不知道我到底在想什么,我更新了大声笑,并将数据包封顶为一个无符号字节的大小,因此不需要进行长度检查。
杰里米·特里菲洛

0

在字节码级别上,主题变量仅从Runtime加载的结构化.class文件中的内存地址加载一次到处理器寄存器中,并且位于switch语句中;在if语句中,代码编译DE会生成不同的jvm指令,这要求将每个变量都加载到寄存器中,尽管与下一个if语句中使用的变量相同。如果您知道使用汇编语言进行编码,那么这将很常见。尽管java编译的cox不是字节码或直接的机器代码,但其条件概念仍然一致。好吧,我试图避免在解释时加深技术性。我希望我已经使这个概念变得清晰和神秘。谢谢。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.