¿Cuál es la diferencia de rendimiento relativa de if / else frente a la instrucción switch en Java?

123

Preocupado por el rendimiento de mi aplicación web, me pregunto cuál de las declaraciones "si / si no" o cambiar es mejor en cuanto al rendimiento.

Anth0
fuente
6
¿Tiene alguna razón para pensar que no se genera el mismo código de bytes para las dos construcciones?
Pascal Cuoq
2
@Pascal: Puede haber optimización realiza mediante el uso de la tabla de consulta de empresas en lugar de una lista de ifetc.
jldupont
18
"La optimización prematura es la raíz de todos los males" - Donald Knuth
missingfaktor
104
Si bien esto es definitivamente una optimización prematura, "la adherencia sin sentido a una cita sacada mal de contexto es la razón por la que necesitamos una computadora multinúcleo de alta gama solo para mostrar una GUI razonablemente receptiva hoy" - Yo.
Lawrence Dol
2
Knuth tiene una mente precisa. Tenga en cuenta el calificativo "prematuro". La optimización es una preocupación perfectamente válida. Dicho esto, un servidor está vinculado a E / S y los cuellos de botella de E / S de red y disco son órdenes de magnitud más importantes que cualquier otra cosa que tenga en su servidor.
alphazero

Respuestas:

108

Eso es micro optimización y optimización prematura, que son malas. Preocúpese más bien por la legibilidad y el mantenimiento del código en cuestión. Si hay más de dos if/elsebloques pegados entre sí o su tamaño es impredecible, entonces puede considerar una switchdeclaración.

Alternativamente, también puede tomar Polimorfismo . Primero crea una interfaz:

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

Y hazte con todas las implementaciones en algunas Map. Puede hacer esto de forma estática o dinámica:

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

Finalmente reemplace el if/elseo switchpor algo como esto (dejando a un lado los controles triviales como los punteros nulos):

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

Se podría ser microslower de if/elseo switch, pero el código es al menos mucho mejor mantener.

Mientras habla de aplicaciones web, puede utilizarla HttpServletRequest#getPathInfo()como tecla de acción (eventualmente, escriba más código para dividir la última parte de pathinfo en un bucle hasta que se encuentre una acción). Aquí puede encontrar respuestas similares:

Si le preocupa el rendimiento de la aplicación web Java EE en general, este artículo también puede resultarle útil. Hay otras áreas que brindan una ganancia de rendimiento mucho mayor que solo (micro) optimizar el código Java sin procesar.

BalusC
fuente
1
o considere el polimorfismo en su lugar
jk.
De hecho, eso es más recomendable en caso de una cantidad "impredecible" de bloques if / else.
BalusC
73
No soy tan rápido para descartar todas las optimizaciones iniciales como "malas". Ser demasiado agresivo es una tontería, pero cuando se enfrenta a construcciones de legibilidad comparable, elegir una que se sepa que funciona mejor es una decisión adecuada.
Brian Knoblauch
8
La versión de búsqueda de HashMap puede ser fácilmente 10 veces más lenta en comparación con una instrucción de tableswitsch. ¡Yo no llamaría a esto "microslower"!
x4u
7
Estoy interesado en conocer el funcionamiento interno de Java en el caso general con declaraciones de cambio; no estoy interesado en si alguien piensa o no que esto está relacionado con la priorización excesiva de la optimización temprana. Dicho esto, no tengo absolutamente ninguna idea de por qué esta respuesta se vota tanto y por qué es la respuesta aceptada ... esto no responde a la pregunta inicial.
motor
125

Estoy totalmente de acuerdo con la opinión de que la optimización prematura es algo que se debe evitar.

Pero es cierto que Java VM tiene códigos de bytes especiales que podrían usarse para switch ().

Ver especificaciones de WM ( conmutador de búsqueda y conmutador de tabla )

Por lo tanto, podría haber algunas mejoras en el rendimiento, si el código es parte del gráfico de rendimiento de la CPU.

Waverick
fuente
60
Me pregunto por qué este comentario no tiene una calificación más alta: es el más informativo de todos. Quiero decir: todos ya sabemos que la optimización prematura es mala y tal, no hay necesidad de explicar eso por milésima vez.
Folkert van Heusden
5
+1 A partir de stackoverflow.com/a/15621602/89818 , parece que las ganancias de rendimiento realmente están ahí, y debería ver una ventaja si usa más de 18 casos.
caw
52

Es muy poco probable que un if / else o un switch sea la fuente de sus problemas de rendimiento. Si tiene problemas de rendimiento, primero debe realizar un análisis de perfil de rendimiento para determinar dónde están los puntos lentos. ¡La optimización temprana es la raíz de todo mal!

Sin embargo, es posible hablar sobre el rendimiento relativo de switch frente a if / else con las optimizaciones del compilador de Java. En primer lugar, tenga en cuenta que en Java, las sentencias switch operan en un dominio muy limitado: enteros. En general, puede ver una declaración de cambio de la siguiente manera:

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

donde c_0,, c_1..., y c_Nson números <condition>enteros que son destinos de la instrucción de cambio y deben resolverse en una expresión entera.

  • Si este conjunto es "denso", es decir, (max (c i ) + 1 - min (c i )) / n> α, donde 0 <k <α <1, donde kes mayor que algún valor empírico, a Se puede generar una tabla de salto, que es muy eficiente.

  • Si este conjunto no es muy denso, pero n> = β, un árbol de búsqueda binario puede encontrar el objetivo en O (2 * log (n)) que también es eficiente.

Para todos los demás casos, una instrucción switch es exactamente tan eficiente como la serie equivalente de declaraciones if / else. Los valores precisos de α y β dependen de varios factores y están determinados por el módulo de optimización de código del compilador.

Finalmente, por supuesto, si el dominio de <condition>no son los números enteros, una declaración de cambio es completamente inútil.

John Feminella
fuente
+1. Existe una buena posibilidad de que el tiempo dedicado a la E / S de red eclipse fácilmente este problema en particular.
Adam Paynter
3
Cabe señalar que los conmutadores funcionan con más que ints. De los tutoriales de Java: "Un conmutador funciona con los tipos de datos primitivos byte, short, char e int. También funciona con tipos enumerados (que se describen en Tipos de enumeración), la clase String y algunas clases especiales que envuelven ciertos tipos primitivos : Carácter, Byte, Corto y Entero (discutido en Números y Cadenas) ". El soporte para String es una adición más reciente; agregado en Java 7. docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
atraudes
1
@jhonFeminella ¿Podría comparar los efectos de la noción BIG O para Java7 String en Swtich en comparación con String en if / else if ..?
Kanagavelu Sugumar
Más precisamente, javac 8 da un peso de 3 a la complejidad temporal sobre la complejidad espacial: stackoverflow.com/a/31032054/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
11

¡Usa el interruptor!

¡Odio mantener bloqueos if-else! Hágase una prueba:

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

Mi código estándar de C # para la evaluación comparativa

Azul amargo
fuente
¿Podría por favor (en algún momento) explicar un poco cómo lo comparó?
DerMike
Muchas gracias por tu actualización. Quiero decir, difieren en un orden de magnitud, lo que es posible, por supuesto. ¿Estás seguro de que el compilador no solo optimizó los switches?
DerMike
@DerMike No recuerdo cómo obtuve los resultados anteriores. Hoy me puse muy diferente. Pero pruébalo tú mismo y cuéntame cómo te resultó.
Bitterblue
1
cuando lo ejecuto en mi computadora portátil; tiempo de cambio necesario: 3585, si / si no tiempo necesario: 3458 así que si / si no es mejor :) o no peor
halil
1
El costo dominante en la prueba es la generación de números aleatorios. Modifiqué la prueba para generar el número aleatorio antes del ciclo, y usé el valor de temperatura para retroalimentar en r. El cambio es entonces casi dos veces más rápido que la cadena if-else.
boneill
8

Recuerdo haber leído que hay 2 tipos de declaraciones Switch en el código de bytes de Java. (Creo que fue en 'Java Performance Tuning' One es una implementación muy rápida que usa los valores enteros de la declaración de cambio para conocer el desplazamiento del código que se ejecutará. Esto requeriría que todos los enteros sean consecutivos y en un rango bien definido Supongo que el uso de todos los valores de un Enum también entraría en esa categoría.

Sin embargo, estoy de acuerdo con muchos otros carteles ... puede ser prematuro preocuparse por esto, a menos que este sea un código muy, muy caliente.

malaverdiere
fuente
4
+1 para el comentario del código activo. Si está en su bucle principal, no es prematuro.
KingAndrew
Sí, javac implementa switchvarias formas diferentes, algunas más eficientes que otras. En general, la eficiencia no será peor que una " ifescalera" sencilla , pero hay suficientes variaciones (especialmente con el JITC) que es difícil ser mucho más preciso que eso.
Hot Licks
8

Según Cliff Click en su charla Java One de 2009 Un curso intensivo en hardware moderno :

Hoy en día, el rendimiento está dominado por patrones de acceso a la memoria. Los errores de caché dominan: la memoria es el nuevo disco. [Diapositiva 65]

Puede obtener sus diapositivas completas aquí .

Cliff da un ejemplo (terminando en la diapositiva 30) que muestra que incluso con la CPU haciendo cambio de nombre de registro, predicción de rama y ejecución especulativa, solo puede iniciar 7 operaciones en 4 ciclos de reloj antes de tener que bloquear debido a dos fallas de caché que toman 300 ciclos de reloj para volver.

Entonces, él dice que para acelerar su programa, no debería estar mirando este tipo de problemas menores, sino más grandes, como si está haciendo conversiones de formato de datos innecesarias, como convertir "SOAP → XML → DOM → SQL → ... "que" pasa todos los datos a través de la caché ".

Jim Ferrans
fuente
4

En mi prueba, el mejor rendimiento es ENUM> MAP> SWITCH> IF / ELSE IF en 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
 */ 
Kanagavelu Sugumar
fuente
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
@halil No estoy seguro de cómo funciona este código en diferentes entornos, pero ha mencionado si / elseif es mejor que Switch y Map, que no puedo convencer, ya que si / elseif tiene que realizar más veces no es igual a la comparación.
Kanagavelu Sugumar
2

Para la mayoría switchy la mayoría de los if-then-elsebloques, no puedo imaginar que haya preocupaciones importantes o apreciables relacionadas con el rendimiento.

Pero aquí está la cuestión: si está utilizando un switchbloque, su uso sugiere que está activando un valor tomado de un conjunto de constantes conocidas en tiempo de compilación. En este caso, realmente no debería usar switchdeclaraciones si puede usar unenum con métodos específicos de constante.

En comparación con una switchdeclaración, una enumeración proporciona una mejor seguridad de tipos y un código que es más fácil de mantener. Las enumeraciones se pueden diseñar de modo que si se agrega una constante al conjunto de constantes, su código no se compilará sin proporcionar un método específico de constante para el nuevo valor. Por otro lado, olvidar agregar un nuevo casea un switchbloque a veces solo se puede detectar en el tiempo de ejecución si tiene la suerte de haber configurado su bloque para lanzar una excepción.

Rendimiento entre switch un enummétodo específico de constante y no debería ser significativamente diferente, pero este último es más legible, más seguro y más fácil de mantener.

Scottb
fuente