Какова относительная разница в производительности оператора if / else и оператора switch в Java?

124

Беспокоясь о производительности моего веб-приложения, мне интересно, какой из операторов if / else или switch лучше с точки зрения производительности?

Anth0
источник
6
Есть ли у вас основания полагать, что один и тот же байт-код не создается для двух конструкций?
Паскаль Куок
2
@Pascal: можно было бы оптимизировать поиск по таблице вместо списка ifи т. Д.
jldupont
18
«Преждевременная оптимизация - корень всех зол» - Дональд Кнут
missingfaktor
104
Хотя это определенно преждевременная оптимизация, «бездумное следование цитате, вырванной из контекста, является причиной того, что сегодня нам нужен высокопроизводительный многоядерный компьютер только для того, чтобы отображать достаточно отзывчивый графический интерфейс» - Я.
Лоуренс Дол
2
Кнут обладает точным умом. Обратите внимание на квалификатор «преждевременный». Оптимизация - вполне обоснованная проблема. Тем не менее, сервер привязан к вводу-выводу, а узкие места сетевого и дискового ввода-вывода на порядок значительнее, чем все остальное, что происходит на вашем сервере.
alphazero

Ответы:

108

Это микрооптимизация и преждевременная оптимизация - зло. Скорее беспокойтесь о читабельности и ремонтопригодности рассматриваемого кода. Если if/elseсклеено более двух блоков или их размер непредсказуем, то вам стоит подумать о switchзаявлении.

В качестве альтернативы вы также можете использовать Polymorphism . Сначала создайте интерфейс:

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

И получить все реализации в некоторых Map. Вы можете сделать это статически или динамически:

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

Наконец, замените if/elseили switchна что-то вроде этого (оставив в стороне тривиальные проверки, такие как нулевые указатели):

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

Это может быть немного меньше if/elseили switch, но код, по крайней мере, намного лучше обслуживается.

Когда вы говорите о веб-приложениях, вы можете использовать ее в HttpServletRequest#getPathInfo()качестве клавиши действия (в конечном итоге напишите еще код, чтобы отделить последнюю часть pathinfo в цикле, пока действие не будет найдено). Вы можете найти здесь похожие ответы:

Если вы беспокоитесь о производительности веб-приложений Java EE в целом, эта статья также может оказаться полезной для вас. Есть и другие области, которые дают гораздо больший прирост производительности, чем простая (микро) оптимизация исходного кода Java.

BalusC
источник
1
или вместо этого рассмотрим полиморфизм
jk.
Это действительно более рекомендуется в случае «непредсказуемого» количества блоков if / else.
BalusC
73
Я не спешу отбрасывать всю раннюю оптимизацию как «зло». Быть слишком агрессивным глупо, но когда вы сталкиваетесь с конструкциями сравнимой читабельности, правильным решением будет выбрать тот, который, как известно, работает лучше.
Брайан Кноблаух
8
Версия поиска HashMap может быть в 10 раз медленнее, чем инструкция по переключению таблиц. Я бы не назвал это «микромедленником»!
x4u
7
Мне интересно знать внутреннюю работу Java в общем случае с операторами switch - меня не интересует, думает ли кто-нибудь, что это связано с приоритетом ранней оптимизации. При этом я абсолютно не понимаю, почему за этот ответ так много голосов и почему это принятый ответ ... это ничего не делает для ответа на первоначальный вопрос.
searchchengine27
125

Я полностью согласен с мнением, что преждевременной оптимизации следует избегать.

Но верно, что виртуальная машина Java имеет специальные байт-коды, которые можно использовать для switch ().

См. Спецификацию WM ( переключатель поиска и переключатель таблиц )

Так что может быть некоторый прирост производительности, если код является частью графика производительности ЦП.

Waverick
источник
60
Интересно, почему этот комментарий не оценивается выше: он самый информативный из всех. Я имею в виду: мы все уже знаем, что преждевременная оптимизация - это плохо и так далее, не нужно объяснять это в тысячный раз.
Folkert van Heusden
5
+1 Что касается stackoverflow.com/a/15621602/89818, похоже, что прирост производительности действительно есть, и вы должны увидеть преимущество, если используете 18+ случаев.
caw
52

Крайне маловероятно, что if / else или переключатель станут источником проблем с производительностью. Если у вас проблемы с производительностью, вам следует сначала провести анализ профилирования производительности, чтобы определить, где находятся медленные места. Преждевременная оптимизация - это корень всех зол!

Тем не менее, можно говорить об относительной производительности switch по сравнению с if / else с оптимизацией компилятора Java. Прежде всего отметим, что в Java операторы switch работают с очень ограниченным доменом - целыми числами. В общем, вы можете просмотреть оператор switch следующим образом:

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

где c_0, c_1, ..., и c_Nявляются целыми числами , которые являются мишенями заявления переключателя, и <condition>должны разрешаться в выражение целого.

  • Если это множество «плотное», то есть (max (c i ) + 1 - min (c i )) / n> α, где 0 <k <α <1, где kбольше некоторого эмпирического значения, a таблица переходов может быть сгенерирована, что очень эффективно.

  • Если этот набор не очень плотный, но n> = β, двоичное дерево поиска может найти цель в O (2 * log (n)), что тоже по-прежнему эффективно.

Во всех остальных случаях оператор switch столь же эффективен, как и эквивалентная серия операторов if / else. Точные значения α и β зависят от ряда факторов и определяются модулем оптимизации кода компилятора.

Наконец, конечно, если доменом <condition>не являются целые числа, оператор switch совершенно бесполезен.

Джон Феминелла
источник
+1. Есть большая вероятность, что время, потраченное на сетевой ввод-вывод, легко затмевает эту конкретную проблему.
Адам Пейнтер,
3
Следует отметить, что переключатели работают не только с целыми числами. Из учебных пособий по Java: «Переключатель работает с примитивными типами данных byte, short, char и int. Он также работает с перечисляемыми типами (обсуждаемыми в разделе« Типы перечисления »), классом String и несколькими специальными классами, которые охватывают определенные примитивные типы. : Символьные, байтовые, короткие и целые числа (обсуждаются в разделе «Числа и строки»). Поддержка String - более позднее дополнение; добавлено в Java 7. docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
atraudes
1
@jhonFeminella Не могли бы вы сравнить эффекты BIG O для Java7 String в Swtich и String в if / else if ..?
Kanagavelu Sugumar
Точнее, javac 8 дает вес 3 временной сложности над пространственной сложностью: stackoverflow.com/a/31032054/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
11

Используйте переключатель!

Ненавижу поддерживать блоки 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 # для тестирования

Bitterblue
источник
Не могли бы вы (когда-нибудь) рассказать немного о том, как вы это тестировали?
DerMike
Большое спасибо за ваше обновление. То есть они различаются на порядок, что, конечно, возможно. Вы уверены, что компилятор не просто оптимизировал switches?
DerMike
@DerMike Я не помню, как я получил старые результаты. У меня сегодня совсем другое дело. Но попробуйте сами и дайте мне знать, как это получается.
Bitterblue
1
когда я запускаю его на своем ноутбуке; необходимое время переключения: 3585, если / еще требуется время: 3458, так что если / иначе лучше :) или не хуже
халил
1
Преобладающая стоимость в тесте - генерация случайных чисел. Я изменил тест, чтобы сгенерировать случайное число перед циклом, и использовал значение temp для обратной связи в r. Таким образом, переключение почти в два раза быстрее, чем цепочка if-else.
Boneill
8

Я помню, как читал, что в байт-коде Java есть 2 вида операторов Switch. (Я думаю, что это было в «Настройка производительности Java». Одна из них - очень быстрая реализация, которая использует целочисленные значения оператора switch, чтобы знать смещение кода, который будет выполняться. Для этого потребуется, чтобы все целые числа были последовательными и в четко определенном диапазоне Я предполагаю, что использование всех значений Enum тоже попадет в эту категорию.

Я согласен со многими другими плакатами ... может быть преждевременно беспокоиться об этом, если только это не очень-очень горячий код.

malaverdiere
источник
4
+1 за комментарий к горячему коду. Если это в вашем основном цикле, это не преждевременно.
KingAndrew
Да, javac реализует switchнесколько различных способов, некоторые из которых более эффективны, чем другие. В целом эффективность будет не хуже, чем у простой « ifлестницы», но есть достаточно вариаций (особенно с JITC), поэтому трудно быть намного более точным.
Hot Licks
8

Согласно Клиффу Клик в его выступлении на Java One 2009 Ускоренный курс современного оборудования :

Сегодня в производительности преобладают шаблоны доступа к памяти. Преобладают пропуски кеша - новый диск - это память. [Слайд 65]

Вы можете получить его полные слайды здесь .

Клифф приводит пример (завершение слайда 30), показывающий, что даже когда ЦП выполняет переименование регистров, предсказание ветвлений и спекулятивное выполнение, он может запустить только 7 операций за 4 тактовых цикла, прежде чем потребуется блокировка из-за двух промахов в кэше, которые требуют 300 тактов для возврата.

Поэтому он говорит, что для ускорения вашей программы вы должны обращать внимание не на такие второстепенные проблемы, а на более серьезные, например, выполняете ли вы ненужные преобразования формата данных, такие как преобразование «SOAP → XML → DOM → SQL →… "который" передает все данные через кеш ".

Джим Ферранс
источник
4

В моем тесте лучшая производительность - ENUM> MAP> SWITCH> IF / ELSE IF в Windows7.

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


/*
 * http://stackoverflow.com/questions/338206/why-cant-i-switch-on-a-string
 * 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 :3235 Time taken for String in if/else if :3143 Time taken for String in Map :4194 Time taken for String in ENUM :2866
Халил
@halil Я не уверен, как этот код работает в разных средах, но вы упомянули, что if / elseif лучше, чем Switch и Map, в чем я не могу убедить, поскольку if / elseif должен выполнять больше, чем сравнение, равное количеству раз.
Канагавелу Сугумар
2

Я не могу себе представить, что для большинства switchи большинства if-then-elseблоков есть какие-либо заметные или существенные проблемы, связанные с производительностью.

Но вот в чем дело: если вы используете switchблок, само его использование предполагает, что вы включаете значение, взятое из набора констант, известных во время компиляции. В этом случае вам действительно не следует использовать switchоператоры вообще, если вы можете использовать их enumс методами, зависящими от константы.

По сравнению с switchоператором перечисление обеспечивает лучшую безопасность типов и код, который легче поддерживать. Перечисления могут быть спроектированы таким образом, что если к набору констант добавляется константа, ваш код не будет компилироваться без предоставления специфичного для константы метода для нового значения. С другой стороны, если вы забыли добавить новый блок caseв switchблок, это может быть обнаружено только во время выполнения, если вам посчастливилось настроить свой блок на создание исключения.

Производительность между switchи enumспецифичным для константы методом не должна существенно отличаться, но последний более читабелен, безопаснее и проще в обслуживании.

scottb
источник