在Java中,if/else语句和switch语句的相对性能差异是什么?

147

担心我的网络应用程序的性能,我想知道在性能方面,"if/else"语句和switch语句哪个更好?


6
你有什么理由认为这两个结构体生成的字节码不同? - Pascal Cuoq
2
@Pascal:可以尝试使用表格查找来进行优化,而不是使用一长串的if语句。 - jldupont
21
"过早优化是万恶之源" - 唐纳德·克努斯 - missingfaktor
117
虽然这明显是过早的优化,但“毫无头绪地坚持一个被严重断章取义的引用就是我们今天需要高端多核计算机才能显示一个相当响应的图形用户界面的原因” -我。 - Lawrence Dol
3
Knuth思维精准。请注意限定词“过早”。优化是一个完全合理的问题。尽管如此,服务器受到IO限制,网络和磁盘I/O的瓶颈比服务器中其他因素对性能的影响要显著得多。 - alphazero
显示剩余6条评论
11个回答

147

我完全同意这个观点,即避免过早地进行优化。

但事实上,Java虚拟机有一些特殊的字节码可用于switch语句。

请参见lookupswitchtableswitch,详见Java虚拟机规范

因此,如果代码是性能CPU图的一部分,就可能会有一些性能提升。


66
我想知道为什么这条评论没有更高的评分:它是所有评论中最有信息量的。我的意思是:我们都已经知道过早优化是不好的,没有必要再解释一遍了。 - Folkert van Heusden
7
根据https://dev59.com/I2Uo5IYBdhLWcg3w-zii#15621602所述,似乎确实存在性能提升,如果您使用18个以上的案例,则应该会看到优势。 - caw

119

这是微观优化和过早优化,它们是有害的。相反,更应该关注所讨论的代码的可读性和可维护性。如果有两个以上的if/else块粘在一起,或者其大小是不可预测的,那么你可以考虑使用switch语句。

或者,您还可以使用多态性。首先创建一些接口:

public interface Action { 
    void execute(String input);
}

可以通过静态或动态的方式获取所有实现,并将它们存储在 Map 中:

Map<String, Action> actions = new HashMap<String, Action>();

最后,将if/elseswitch替换为以下代码(忽略空指针等细节):

actions.get(name).execute(input);

虽然可能比使用if/elseswitch微慢一点,但这种代码至少更易于维护。

由于您在谈论Web应用程序,因此可以使用HttpServletRequest#getPathInfo()作为操作关键字(最终编写一些代码以循环地分离pathinfo的最后一部分,直到找到一个操作)。您可以在这里找到类似的答案:

如果您担心Java EE Web应用程序的性能问题,则这篇文章也可能对您有所帮助。除了(微)优化原始Java代码之外,还有其他领域可以提供更大的性能提升。


1
或者考虑使用多态性。 - jk.
在“不可预测”的if/else块数量的情况下,这确实更加推荐。 - BalusC
81
我不急于将所有早期优化视为“邪恶”。过于激进是愚蠢的,但当面临可读性相当的结构时,选择已知性能更好的是一个合适的决定。 - Brian Knoblauch
11
与tableswitch指令相比,HashMap查找版本的速度可能会慢10倍。我不会称其为“微慢”! - x4u
我的意思是,慢一些以“微秒”为单位。 - BalusC
12
我对Java中使用switch语句的内部运作机制感兴趣,不关心别人是否认为这与过度优化有关。话虽如此,我完全不明白为什么这个答案得到了这么多赞和被接受的原因......它没有回答最初的问题。 - searchengine27

52

如果你遇到性能问题,if/else或switch语句很少会是问题的来源。你应该首先进行性能分析以确定存在哪些缓慢的地方。过早优化是万恶之源!

尽管如此,通过Java编译器优化,我们可以讨论switch与if/else的相对性能。首先请注意,在Java中,switch语句仅适用于非常有限的域 - 整数。一般来说,你可以将switch语句视为以下方式:

switch (<condition>) {
   case c_0: ...
   case c_1: ...
   ...
   case c_n: ...
   default: ...
}

其中c_0, c_1, ..., 和 c_N 是 switch 语句的目标整数,<condition> 必须解析为整数表达式。

  • 如果这个集合是"密集的"--也就是说,(max(ci) + 1 - min(ci)) / n > α, 其中 0 < k < α < 1, 其中 k 大于某个经验值,则可以生成跳转表,这是非常高效的。

  • 如果这个集合不是很密集,但是 n >= β,那么二叉搜索树可以在 O(2 * log(n)) 的时间内找到目标,这仍然是有效的。

对于所有其他情况,switch 语句与等价的 if/else 语句序列一样有效率。α 和 β 的精确值取决于多个因素,并由编译器的代码优化模块确定。

最后,当<condition>的定义域不是整数时,switch 语句完全无用。


+1. 网络I/O所花费的时间很有可能轻易地超过了这个特定问题。 - Adam Paynter
4
应该注意到,开关不仅适用于整数。来自Java教程的说明:“开关与字节、短整型、字符和整型原始数据类型一起使用。它还可以与枚举类型(在枚举类型中讨论)、String类以及包装某些原始类型的几个特殊类一起使用:Character、Byte、Short和Integer(在数字和字符串中讨论)。对String的支持是最近添加的;在Java 7中添加。http://docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html - atraudes
1
@jhonFeminella,您能否比较一下Java7中使用Switch语句的BIG O表示法对String类型和if/else if语句中String类型的影响? - Kanagavelu Sugumar
更准确地说,javac 8在时间复杂度和空间复杂度之间给予时间复杂度3的权重:https://dev59.com/kWkv5IYBdhLWcg3w4kp2#31032054 - Ciro Santilli OurBigBook.com

11

使用 switch!

我讨厌维护 if-else 块!这里有一个测试:

public class SpeedTestSwitch
{
    private static void do1(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            switch (r)
            {
                case 0:
                    temp = 9;
                    break;
                case 1:
                    temp = 8;
                    break;
                case 2:
                    temp = 7;
                    break;
                case 3:
                    temp = 6;
                    break;
                case 4:
                    temp = 5;
                    break;
                case 5:
                    temp = 4;
                    break;
                case 6:
                    temp = 3;
                    break;
                case 7:
                    temp = 2;
                    break;
                case 8:
                    temp = 1;
                    break;
                case 9:
                    temp = 0;
                    break;
            }
        }
        System.out.println("ignore: " + temp);
    }

    private static void do2(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            if (r == 0)
                temp = 9;
            else
                if (r == 1)
                    temp = 8;
                else
                    if (r == 2)
                        temp = 7;
                    else
                        if (r == 3)
                            temp = 6;
                        else
                            if (r == 4)
                                temp = 5;
                            else
                                if (r == 5)
                                    temp = 4;
                                else
                                    if (r == 6)
                                        temp = 3;
                                    else
                                        if (r == 7)
                                            temp = 2;
                                        else
                                            if (r == 8)
                                                temp = 1;
                                            else
                                                if (r == 9)
                                                    temp = 0;
        }
        System.out.println("ignore: " + temp);
    }

    public static void main(String[] args)
    {
        long time;
        int loop = 1 * 100 * 1000 * 1000;
        System.out.println("warming up...");
        do1(loop / 100);
        do2(loop / 100);

        System.out.println("start");

        // run 1
        System.out.println("switch:");
        time = System.currentTimeMillis();
        do1(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));

        // run 2
        System.out.println("if/else:");
        time = System.currentTimeMillis();
        do2(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));
    }
}

我的用于进行基准测试的C#标准代码


1
当我在我的笔记本电脑上运行它时; 切换所需时间:3585, if/else 所需时间:3458,因此 if/else 更好 :) 或者不更差。 - halil
@halil 被接受的答案的第二句话是绝对正确的。在你的情况下,我仍然会采用switch。时间差异并不大。 - Bitterblue
2
测试中的主要成本是随机数生成。我修改了测试以在循环之前生成随机数,并使用临时值反馈到r中。然后,开关比if-else链快近两倍。 - boneill
不准确,随机数应该在之前生成,并且相同的随机数应该被提供给两种方法。 - AmeyaB
正如其他人所指出的那样,随机数是昂贵的,并且隐藏了性能差异。我用以下代码替换了 int r = (int) (Math.random() * 10); int r = (loop & 0b11111) != 0b0 ? 0 : loop % 10; 结果if/else比switch快三倍,这表明它也取决于数据的分布。 - D. L.
显示剩余4条评论

9

在Windows7中,我进行的测试结果是:ENUM > MAP > SWITCH > IF/ELSE IF 的性能最佳。

import java.util.HashMap;
import java.util.Map;

public class StringsInSwitch {
public static void main(String[] args) {
    String doSomething = null;


    //METHOD_1 : SWITCH
    long start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        switch (input) {
        case "Hello World0":
            doSomething = "Hello World0";
            break;
        case "Hello World1":
            doSomething = "Hello World0";
            break;
        case "Hello World2":
            doSomething = "Hello World0";
            break;
        case "Hello World3":
            doSomething = "Hello World0";
            break;
        case "Hello World4":
            doSomething = "Hello World0";
            break;
        case "Hello World5":
            doSomething = "Hello World0";
            break;
        case "Hello World6":
            doSomething = "Hello World0";
            break;
        case "Hello World7":
            doSomething = "Hello World0";
            break;
        case "Hello World8":
            doSomething = "Hello World0";
            break;
        case "Hello World9":
            doSomething = "Hello World0";
            break;
        case "Hello World10":
            doSomething = "Hello World0";
            break;
        case "Hello World11":
            doSomething = "Hello World0";
            break;
        case "Hello World12":
            doSomething = "Hello World0";
            break;
        case "Hello World13":
            doSomething = "Hello World0";
            break;
        case "Hello World14":
            doSomething = "Hello World0";
            break;
        case "Hello World15":
            doSomething = "Hello World0";
            break;
        }
    }

    System.out.println("Time taken for String in Switch :"+ (System.currentTimeMillis() - start));




    //METHOD_2 : IF/ELSE IF
    start = System.currentTimeMillis();

    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        if(input.equals("Hello World0")){
            doSomething = "Hello World0";
        } else if(input.equals("Hello World1")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World2")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World3")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World4")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World5")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World6")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World7")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World8")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World9")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World10")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World11")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World12")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World13")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World14")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World15")){
            doSomething = "Hello World0";

        }
    }
    System.out.println("Time taken for String in if/else if :"+ (System.currentTimeMillis() - start));









    //METHOD_3 : MAP
    //Create and build Map
    Map<String, ExecutableClass> map = new HashMap<String, ExecutableClass>();
    for (int i = 0; i <= 15; i++) {
        String input = "Hello World" + (i & 0xF);
        map.put(input, new ExecutableClass(){
                            public void execute(String doSomething){
                                doSomething = "Hello World0";
                            }
                        });
    }


    //Start test map
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);
        map.get(input).execute(doSomething);
    }
    System.out.println("Time taken for String in Map :"+ (System.currentTimeMillis() - start));






    //METHOD_4 : ENUM (This doesn't use muliple string with space.)
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "HW" + (i & 0xF);
        HelloWorld.valueOf(input).execute(doSomething);
    }
    System.out.println("Time taken for String in ENUM :"+ (System.currentTimeMillis() - start));


    }

}

interface ExecutableClass
{
    public void execute(String doSomething);
}



// Enum version
enum HelloWorld {
    HW0("Hello World0"), HW1("Hello World1"), HW2("Hello World2"), HW3(
            "Hello World3"), HW4("Hello World4"), HW5("Hello World5"), HW6(
            "Hello World6"), HW7("Hello World7"), HW8("Hello World8"), HW9(
            "Hello World9"), HW10("Hello World10"), HW11("Hello World11"), HW12(
            "Hello World12"), HW13("Hello World13"), HW14("Hello World4"), HW15(
            "Hello World15");

    private String name = null;

    private HelloWorld(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
        doSomething = "Hello World0";
    }

    public static HelloWorld fromString(String input) {
        for (HelloWorld hw : HelloWorld.values()) {
            if (input.equals(hw.getName())) {
                return hw;
            }
        }
        return null;
    }

}





//Enum version for betterment on coding format compare to interface ExecutableClass
enum HelloWorld1 {
    HW0("Hello World0") {   
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    }, 
    HW1("Hello World1"){    
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    };
    private String name = null;

    private HelloWorld1(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
    //  super call, nothing here
    }
}


/*
 * https://dev59.com/8XRC5IYBdhLWcg3wUfN2
 * https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.10
 * http://forums.xkcd.com/viewtopic.php?f=11&t=33524
 */ 

`Time taken for String in Switch :3235Time taken for String in if/else if :3143Time taken for String in Map :4194Time taken for String in ENUM :2866` - halil
1
@halil 我不确定这段代码在不同的环境中如何工作,但你提到if/elseif比Switch和Map更好,我无法说服,因为if/elseif必须执行更多次的相等比较。 - Kanagavelu Sugumar

8
根据Cliff Click在他2009年Java One演讲中的说法(现代硬件速成课)
今天,性能受到内存访问模式的支配。缓存未命中占主导地位——内存是新的磁盘。[幻灯片65]
您可以在此处获取他的完整幻灯片。
Cliff给出了一个例子(在第30张幻灯片上完成),即使CPU进行寄存器重命名、分支预测和推测执行,它在启动7个操作后只能持续4个时钟周期,然后由于两个缓存未命中而被阻塞,这需要300个时钟周期才能返回。
所以他说要加速程序,您不应该关注这种小问题,而应该关注更大的问题,比如是否进行不必要的数据格式转换,例如将“SOAP→XML→DOM→SQL→…”转换,这会“通过缓存传递所有数据”。

8
我记得读过一篇关于Java字节码中有两种Switch语句的文章。其中一种是非常快的实现方式,它使用Switch语句的整数值来确定要执行的代码的偏移量。这需要所有整数都是连续的并且处于一个明确定义的范围内。我猜枚举类型的所有值也属于这种情况。但是,我同意其他帖子上的评论...除非这是非常非常热门的代码,否则担心这个可能为时过早。

4
赞成这个热门代码的注释。如果它在你的主循环中,那么它就不是过早的了。 - KingAndrew
是的,javac 以几种不同的方式实现 switch,其中一些比其他方式更有效。通常情况下,效率不会比直接使用“if”梯形结构差,但由于有足够多的变化(特别是在JITC中),因此很难比这更精确。 - Hot Licks

2
对于大多数switchif-then-else块,我想不到有任何值得关注或显著的与性能相关的问题。
但是这里有一件事:如果你正在使用switch块,则其使用本身表明您正在从编译时已知的常量集合中获取值进行切换。在这种情况下,如果您可以使用具有特定于常量的方法的enum,则根本不应该使用switch语句。
switch语句相比,枚举提供更好的类型安全性和更易于维护的代码。枚举可以设计成如果将常量添加到常量集合中,则必须为新值提供特定于常量的方法才能编译代码。另一方面,忘记向switch块添加新的case有时只能在运行时捕获,如果您足够幸运地设置了块以抛出异常。 switchenum特定于常量的方法之间的性能不应有显著差异,但后者更易读、更安全、更易于维护。

0

0
速度:如果有很多情况,使用switch语句可能比if语句更快。如果只有几种情况,则不会影响速度。如果情况的数量超过5个,则最好使用switch,否则也可以使用if-else。如果switch包含超过五个项目,则使用查找表或哈希列表来实现。这意味着所有项目都具有相同的访问时间,与if列表相比,其中最后一个项目需要更长的时间才能到达,因为它必须首先评估每个先前的条件。 可读性清晰:当您必须组合情况时,switch看起来更干净。if语句也容易出错。如果缺少else语句,可能会导致混乱。使用switch添加/删除标签也更容易,使您的代码更易于更改和维护。

示例:-> String environment="QA";

    switch(environment.toLowerCase().trim()) {
    case "qa":
        System.out.println(environment+" :"+"Environment running the TestCases");
        break;
    case "Stage":
        System.out.println(environment+" :"+"Environment running the TestCases");
        break;
    case "Dev":
        System.out.println(environment+" :"+"Environment running the TestCases");
        break;
    case "UAT":
        System.out.println(environment+" :"+"Environment running the TestCases");
        break;
    case "Prod":
        System.out.println(environment+" :"+"Environment running the TestCases");
        break;
    default:
        System.out.println(environment+" :"+"Please pass the right Environment");
        break;
    }
    
    String browser="Chrome";
    
    if (browser.equals("chrome")) {
        System.out.println(browser + ": " + " Launch the Browser");
    } else if (browser.equals("safari")) {
        System.out.println(browser + ": " + " Launch the Browser");
    } else if (browser.equals("IE")) {
        System.out.println(browser + ": " + " Launch the Browser");
    } else if (browser.equals("opera")) {
        System.out.println(browser + ": " + " Launch the Browser");

    } else if (browser.equals("Edge")) {
        System.out.println(browser + ": " + " Launch the Browser");

    } else {
        System.out.println("Please pass the right browser");
    }
        

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