【问题标题】:Why switch is faster than if为什么 switch 比 if 快
【发布时间】:2011-10-06 01:32:58
【问题描述】:

许多 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的答案:stackoverflow.com/questions/767821/…
  • 一般来说,来自Wikipedia如果输入值的范围是可识别的“小”并且只有几个间隙,一些包含优化器的编译器实际上可能将 switch 语句实现为分支表或索引函数指针数组,而不是冗长的一系列条件指令。这允许 switch 语句立即确定要执行的分支,而无需通过比较列表。
  • 这个 question 的最佳答案很好地解释了它。这个 article 也很好地解释了一切。
  • 我希望在大多数情况下,优化编译器能够生成具有相似性能特征的代码。在任何情况下,您都必须调用数百万次才能注意到任何差异。
  • 你应该警惕那些在没有解释/证明/推理的情况下做出这样的陈述的书籍。

标签: java switch-statement


【解决方案1】:

因为有特殊的字节码可以在很多情况下进行高效的 switch 语句评估。

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

【讨论】:

  • 不迭代翻译成“检查,跳转”?
  • @fivetwentysix:不,请参阅此信息:artima.com/underthehood/flowP.html。引用文章:当JVM遇到tableswitch指令时,可以简单的检查一下key是否在low和high定义的范围内。如果不是,则采用默认的分支偏移量。如果是这样,它只是从 key 中减去 low 以获得分支偏移列表中的偏移。通过这种方式,它可以确定适当的分支偏移量,而无需检查每个案例值。
  • (i) switch 可能不会被翻译成 tableswitch 字节码指令 - 它可能会变成 lookupswitch 指令,其执行类似于 if/else (ii) 甚至是 @987654326 @字节码指令可能会被JIT编译成一系列if/else,具体取决于cases的个数等因素。
  • tableswitch vs loopuswitch: stackoverflow.com/questions/10287700/…
【解决方案2】:

switch 语句并不总是比if 语句快。它比一长串if-else 语句的扩展性更好,因为switch 可以根据所有值执行查找。但是,对于短期情况,它不会更快,可能会更慢。

【讨论】:

  • 请限制“长”。大于 5?大于 10?或者更多像 20 - 30?
  • 我怀疑这取决于。对我来说,它的 3 个或更多建议 switch 会更清晰,如果不是更快。
  • 什么情况下会慢?
  • @Eric 对于少量的值,尤其是稀疏的 String 或 int,它的速度较慢。
【解决方案3】:

目前的JVM有两种切换字节码:LookupSwitch和TableSwitch。

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

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

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

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

有关这方面的更多信息,请参阅此处:Difference between JVM's LookupSwitch and TableSwitch?

至于哪个最快,请使用以下方法: 如果您有 3 个或更多案例的值连续或几乎连续,请始终使用开关。

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

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

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

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

【讨论】:

  • 自从发布这篇文章以来,我注意到“切换表达式”和“模式匹配”即将出现在 Java 中,可能在 Java 12 之后。openjdk.java.net/jeps/325openjdk.java.net/jeps/305 尚无具体内容,但似乎这些将使switch 成为更强大的语言功能。例如,模式匹配将允许更流畅和高效的instanceof 查找。但是,我认为可以肯定的是,对于基本的 switch/if 场景,我提到的规则仍然适用。
【解决方案4】:

因此,如果您打算拥有大量数据包,那么这些天来内存并不是一个很大的成本,而且数组非常快。您也不能依赖 switch 语句来自动生成跳转表,因此更容易自己生成跳转表场景。正如您在下面的示例中看到的,我们假设最多 255 数据包。

要获得以下结果,您需要抽象.. 我不打算解释它是如何工作的,所以希望您对此有所了解。

如果您需要更多,我将其更新为将数据包大小设置为 255,然后您必须对 (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 语句作为最坏的情况。

即但请记住,它会变得混乱且难以快速管理,而我的其他方法通常更好,但这适用于阵列无法削减它的情况。您必须弄清楚您的用例以及每种情况何时最有效。就像如果您只有几项检查,您就不想使用这两种方法中的任何一种。

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 {

                              }
                          }
                      }
                  }
             }      
        }
   }
}

【讨论】:

  • 为什么是双重间接?既然 ID 无论如何都必须受到约束,为什么不将传入的 id 检查为 0 &lt;= id &lt; packets.length 并确保 packets[id]!=null 然后执行 packets[id].execute(data)
  • 是的,很抱歉迟到的回复又看了一遍..我不知道我到底在想什么我更新了帖子大声笑并将数据包的大小限制为无符号字节的大小,所以没有长度检查需要。
【解决方案5】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-11
    • 2010-10-20
    • 1970-01-01
    相关资源
    最近更新 更多