¿Por qué utilizar un enfoque OO en lugar de una declaración gigante de "cambio"?

59

Estoy trabajando en una tienda .Net, C # y tengo un compañero de trabajo que sigue insistiendo en que debemos usar declaraciones gigantes de Switch en nuestro código con muchos "Casos" en lugar de enfoques más orientados a objetos. Su argumento se remonta constantemente al hecho de que una declaración Switch se compila en una "tabla de salto de la CPU" y, por lo tanto, es la opción más rápida (aunque en otras cosas se le dice a nuestro equipo que no nos importa la velocidad).

Sinceramente, no tengo un argumento en contra de esto ... porque no sé de qué diablos está hablando.
¿Tiene razón?
¿Solo está hablando por el culo?
Solo trato de aprender aquí.

James P. Wright
fuente
77
Puede verificar si tiene razón usando algo como .NET Reflector para mirar el código de ensamblaje y buscar la "tabla de salto de la CPU".
FrustratedWithFormsDesigner
55
"Sentencia switch se compila en una 'tabla de saltos de la CPU' Lo mismo ocurre con el método del peor caso con el envío de todas las funciones virtuales puras-Ninguno funciones virtuales son simplemente vinculados directamente ¿Ha descargado ningún código para comparar..?
S. Lott
64
El código debe estar escrito para PERSONAS, no para máquinas, de lo contrario, haríamos todo en conjunto.
maple_shaft
8
Si él es un gran chiflado, dígale a Knuth: "Deberíamos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal".
DaveE
12
Mantenibilidad ¿Alguna otra pregunta con respuestas de una palabra con la que pueda ayudarlo?
Matt Ellen

Respuestas:

48

Probablemente sea un viejo hacker de C y sí, está hablando por el culo. .Net no es C ++; el compilador .Net sigue mejorando y la mayoría de los hacks inteligentes son contraproducentes, si no hoy, en la próxima versión .Net. Las funciones pequeñas son preferibles porque .Net JIT-s cada función una vez antes de que se esté utilizando. Entonces, si algunos casos nunca se ven afectados durante un LifeCycle de un programa, entonces no se incurre en costos al compilarlos en JIT. De todos modos, si la velocidad no es un problema, no debería haber optimizaciones. Escriba para el programador primero, para el compilador segundo. Su compañero de trabajo no se convencerá fácilmente, por lo que probaría empíricamente que un código mejor organizado es realmente más rápido. Elegiría uno de sus peores ejemplos, los reescribiría de una mejor manera y luego me aseguraría de que su código sea más rápido. Recoge cerezas si es necesario. Luego ejecútelo unos millones de veces, perfílelo y muéstrelo.

EDITAR

Bill Wagner escribió:

Punto 11: Comprenda la atracción de las funciones pequeñas (segunda edición efectiva de C #) Recuerde que traducir su código C # en código ejecutable por máquina es un proceso de dos pasos. El compilador de C # genera IL que se entrega en ensamblajes. El compilador JIT genera código de máquina para cada método (o grupo de métodos, cuando se trata de la inclusión), según sea necesario. Las funciones pequeñas hacen que sea mucho más fácil para el compilador JIT amortizar ese costo. Las funciones pequeñas también son más propensas a ser candidatas para la inserción. No es solo la pequeñez: el flujo de control más simple también es importante. Menos ramas de control dentro de las funciones facilitan que el compilador JIT registre variables. No es solo una buena práctica escribir código más claro; es cómo se crea un código más eficiente en tiempo de ejecución.

EDIT2:

Entonces ... aparentemente una declaración de cambio es más rápida y mejor que un montón de declaraciones if / else, porque una comparación es logarítmica y otra es lineal. http://sequence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html

Bueno, mi enfoque favorito para reemplazar una declaración de cambio enorme es con un diccionario (o, a veces, incluso una matriz si estoy activando enumeraciones o entradas pequeñas) que asigna valores a funciones que se llaman en respuesta a ellas. Hacerlo obliga a uno a eliminar una gran cantidad de desagradable estado de espagueti compartido, pero eso es algo bueno. Una declaración de cambio grande suele ser una pesadilla de mantenimiento. Entonces ... con matrices y diccionarios, la búsqueda llevará un tiempo constante, y habrá poca memoria extra desperdiciada.

Todavía no estoy convencido de que la declaración de cambio sea mejor.

Trabajo
fuente
47
No te preocupes por probarlo más rápido. Esta es una optimización prematura. El milisegundo que puede guardar no es nada en comparación con el índice que olvidó agregar a la base de datos que le cuesta 200 ms. Estás peleando la batalla equivocada.
Rein Henrichs
27
@ Job, ¿y si tiene razón? El punto no es que esté equivocado, el punto es que tiene razón y no importa .
Rein Henrichs
2
Incluso si tuviera razón sobre el 100% de los casos, sigue perdiendo el tiempo.
Jeremy
66
Quiero arrancarme los ojos tratando de leer la página que vinculaste.
AttackingHobo
3
¿Qué pasa con el odio en C ++? Los compiladores de C ++ también están mejorando, y los grandes conmutadores son tan malos en C ++ como en C #, y por exactamente la misma razón. Si estás rodeado de antiguos programadores de C ++ que te dan pena, no es porque sean programadores de C ++, sino porque son malos programadores.
Sebastian Redl
39

A menos que su colega pueda proporcionar pruebas de que esta alteración proporciona un beneficio medible real en la escala de toda la aplicación, es inferior a su enfoque (es decir, polimorfismo), que en realidad proporciona tal beneficio: mantenibilidad.

La microoptimización solo debe hacerse, después de que los cuellos de botella se hayan inmovilizado. La optimización prematura es la raíz de todo mal .

La velocidad es cuantificable. Hay poca información útil en "el enfoque A es más rápido que el enfoque B". La pregunta es " ¿Cuánto más rápido? ".

back2dos
fuente
2
Totalmente cierto. Nunca afirmes que algo es más rápido, siempre mide. Y mida solo cuando esa parte de la aplicación sea el cuello de botella en el rendimiento.
Kilian Foth
66
-1 para "La optimización prematura es la raíz de todo mal". Muestre la cita completa , no solo una parte que sesgue la opinión de Knuth.
alternativa
2
@mathepic: intencionalmente no presenté esto como una cita. Esta oración, como es, es mi opinión personal, aunque por supuesto no es mi creación. Aunque puede notarse que los chicos de c2 parecen considerar solo esa parte la sabiduría central.
back2dos
8
@alternative La cita completa de Knuth "No hay duda de que el grial de la eficiencia conduce al abuso. Los programadores pierden enormes cantidades de tiempo pensando o preocupándose por la velocidad de las partes no críticas de sus programas, y estos intentos de eficiencia realmente tienen fuerte impacto negativo cuando se consideran la depuración y el mantenimiento. Deberíamos olvidarnos de pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal ". Describe perfectamente al compañero de trabajo del OP. En mi humilde opinión, back2dos resumió bien la cita con "la optimización prematura es la raíz de todo mal"
MarkJ
2
@MarkJ 97% del tiempo
alternativa
27

¿A quién le importa si es más rápido?

A menos que esté escribiendo software en tiempo real, es poco probable que la minúscula cantidad de aceleración que pueda obtener al hacer algo de una manera completamente loca haga mucha diferencia para su cliente. Ni siquiera trataría de luchar contra este en el frente de la velocidad, este tipo claramente no va a escuchar ninguna discusión sobre el tema.

Sin embargo, el objetivo del juego es la mantenibilidad, y una declaración de cambio gigante ni siquiera es ligeramente mantenible, ¿cómo se explican las diferentes rutas a través del código a un nuevo tipo? ¡La documentación tendrá que ser tan larga como el código mismo!

Además, tiene la incapacidad total para realizar pruebas unitarias de manera efectiva (demasiadas rutas posibles, sin mencionar la probable falta de interfaces, etc.), lo que hace que su código sea aún menos mantenible.

[En el lado del interés: el JITter funciona mejor en métodos más pequeños, por lo que las declaraciones de cambio gigantes (y sus métodos inherentemente grandes) dañarán su velocidad en ensamblajes grandes, IIRC.]

Ed James
fuente
1
+ un gran ejemplo de optimización prematura.
ShaneC
Definitivamente esto.
DeadMG
+1 para 'una declaración de cambio gigante ni siquiera es ligeramente mantenible'
Korey Hinton el
2
Una declaración de cambio gigante es mucho más fácil de comprender para el chico nuevo: todos los comportamientos posibles se recopilan allí en una buena lista ordenada. Las llamadas indirectas son extremadamente difíciles de seguir, en el peor de los casos (puntero de función) necesita buscar en toda la base de código las funciones de la firma correcta, y las llamadas virtuales son solo un poco mejores (busque funciones del nombre y firma correctos y relacionados por herencia). Pero la mantenibilidad no se trata de ser de solo lectura.
Ben Voigt
14

Aléjese de la declaración de cambio ...

Este tipo de declaración de cambio debe evitarse como una plaga porque viola el Principio Abierto Cerrado . Obliga al equipo a realizar cambios en el código existente cuando se necesita agregar una nueva funcionalidad, en lugar de simplemente agregar un nuevo código.

Dakotah North
fuente
11
Eso viene con una advertencia. Hay operaciones (funciones / métodos) y tipos. Cuando agrega una nueva operación, solo tiene que cambiar el código en un lugar para las instrucciones de cambio (agregue una nueva función con la declaración de cambio), pero tendría que agregar ese método a todas las clases en el caso OO (viola abrir / principio cerrado). Si agrega nuevos tipos, debe tocar cada instrucción de cambio, pero en el caso OO simplemente agregaría una clase más. Por lo tanto, para tomar una decisión informada, debe saber si agregará más operaciones a los tipos existentes o si agregará más tipos.
Scott Whitlock
3
Si necesita agregar más operaciones a los tipos existentes en un paradigma OO sin violar OCP, entonces creo que para eso es el patrón de visitante.
Scott Whitlock
3
@ Martin - nombre de pila si lo desea, pero esta es una compensación muy conocida. Os remito a Clean Code de RC Martin. Revisa su artículo sobre OCP, explicando lo que describí anteriormente. No puede diseñar simultáneamente para todos los requisitos futuros. Debe elegir si es más probable que agregue más operaciones o más tipos. OO favorece la adición de tipos. Puede usar OO para agregar más operaciones si modela operaciones como clases, pero eso parece entrar en el patrón de visitante, que tiene sus propios problemas (especialmente los gastos generales).
Scott Whitlock
8
@ Martin: ¿Alguna vez has escrito un analizador? Es bastante común tener grandes cajas de cambio que enciendan el siguiente token en el búfer de búsqueda anticipada. Usted podría reemplazar esos interruptores con función virtual llama al siguiente token, pero eso sería una pesadilla propio mantenimiento. Es raro, pero a veces la caja del interruptor es en realidad la mejor opción, ya que mantiene el código que debe leerse / modificarse juntos muy cerca.
nikie
1
@ Martin: Usaste palabras como "nunca", "nunca" y "Poppycock", así que asumí que estás hablando de todos los casos sin excepciones, no solo del caso más común. (Y por cierto: la gente todavía escribe analizadores a mano. Por ejemplo, el analizador CPython todavía se escribe a mano, IIRC.)
Nikkie
8

He sobrevivido a la pesadilla conocida como la máquina de estados finitos masiva manipulada por declaraciones de cambio masivas. Peor aún, en mi caso, el FSM abarcó tres DLL de C ++ y era bastante claro que el código fue escrito por alguien versado en C.

Las métricas que debe preocupar son:

  • Velocidad de hacer un cambio
  • Velocidad de encontrar el problema cuando sucede

Se me asignó la tarea de agregar una nueva característica a ese conjunto de archivos DLL, y pude convencer a la gerencia de que me tomaría tanto tiempo reescribir los 3 archivos DLL como un archivo DLL orientado a objetos como lo haría para mí parchear y el jurado manipula la solución en lo que ya estaba allí. La reescritura fue un gran éxito, ya que no solo era compatible con la nueva funcionalidad, sino que era mucho más fácil de extender. De hecho, una tarea que normalmente tomaría una semana para asegurarse de no romper nada terminaría en unas pocas horas.

Entonces, ¿qué hay de los tiempos de ejecución? No hubo aumento o disminución de la velocidad. Para ser justos, nuestro rendimiento se vio limitado por los controladores del sistema, por lo que si la solución orientada a objetos fuera más lenta, no lo sabríamos.

¿Qué hay de malo con las declaraciones de cambio masivo para un lenguaje OO?

  • El flujo de control del programa se retira del objeto al que pertenece y se coloca fuera del objeto
  • Muchos puntos de control externo se traducen en muchos lugares que necesita revisar
  • No está claro dónde se almacena el estado, particularmente si el interruptor está dentro de un bucle
  • La comparación más rápida es ninguna comparación (puede evitar la necesidad de muchas comparaciones con un buen diseño orientado a objetos)
  • Es más eficiente iterar a través de sus objetos y siempre llamar al mismo método en todos los objetos que cambiar su código en función del tipo de objeto o enumeración que codifica el tipo.
Berin Loritsch
fuente
8

No compro el argumento de rendimiento; Se trata de la mantenibilidad del código.

PERO: a veces , una declaración de interruptor gigante es más fácil de mantener (menos código) que un grupo de clases pequeñas que anulan las funciones virtuales de una clase base abstracta. Por ejemplo, si implementara un emulador de CPU, no implementaría la funcionalidad de cada instrucción en una clase separada, simplemente la introduciría en un swtich gigante en el código de operación, posiblemente llamando a funciones auxiliares para obtener instrucciones más complejas.

Regla general: si el cambio se realiza de alguna manera en el TIPO, probablemente debería usar la herencia y las funciones virtuales. Si el cambio se realiza en un VALOR de un tipo fijo (por ejemplo, el código de operación de la instrucción, como se indicó anteriormente), está bien dejarlo como está.

zvrba
fuente
5

No puedes convencerme de que:

void action1()
{}

void action2()
{}

void action3()
{}

void action4()
{}

void doAction(int action)
{
    switch(action)
    {
        case 1: action1();break;
        case 2: action2();break;
        case 3: action3();break;
        case 4: action4();break;
    }
}

Es significativamente más rápido que:

struct IAction
{
    virtual ~IAction() {}
    virtual void action() = 0;
}

struct Action1: public IAction
{
    virtual void action()    { }
}

struct Action2: public IAction
{
    virtual void action()    { }
}

struct Action3: public IAction
{
    virtual void action()    { }
}

struct Action4: public IAction
{
    virtual void action()    { }
}

void doAction(IAction& actionObject)
{
    actionObject.action();
}

Además, la versión OO es más fácil de mantener.

Martin York
fuente
8
Para algunas cosas y para pequeñas cantidades de acciones, la versión OO es mucho más tonta. Tiene que tener algún tipo de fábrica para convertir algún valor en la creación de una IAction. En muchos casos, es mucho más legible simplemente activar ese valor.
Zan Lynx
@Zan Lynx: Tu argumento es demasiado genérico. La creación del objeto IAction es tan difícil como recuperar el número entero de acción, no más difícil ni más fácil. Entonces podemos tener una conversación real sin ser genéricos. Considera una calculadora. ¿Cuál es la diferencia en complejidad aquí? La respuesta es cero. Como todas las acciones pre-creadas. Obtiene la entrada del usuario y ya es una acción.
Martin York
3
@ Martin: está asumiendo una aplicación de calculadora GUI. Tomemos una aplicación de calculadora de teclado escrita para C ++ en un sistema integrado. Ahora tiene un entero de código de escaneo de un registro de hardware. Ahora, ¿qué es menos complejo?
Zan Lynx
2
@ Martin: No ves cómo entero -> tabla de búsqueda -> creación de nuevo objeto -> llamar a función virtual es más complicado que entero -> interruptor -> función? ¿Cómo no ves eso?
Zan Lynx
2
@ Martin: Quizás lo haga. Mientras tanto, explique cómo hace que el objeto IAction llame a action () desde un entero sin una tabla de búsqueda.
Zan Lynx
4

Tiene razón en que el código de máquina resultante probablemente será más eficiente. El compilador esencial transforma una instrucción switch en un conjunto de pruebas y ramas, que serán relativamente pocas instrucciones. Existe una alta probabilidad de que el código resultante de enfoques más abstractos requiera más instrucciones.

SIN EMBARGO : es casi seguro que su aplicación particular no necesita preocuparse por este tipo de micro-optimización, o no estaría usando .net en primer lugar. Para cualquier cosa que no sea aplicaciones embebidas muy limitadas o trabajo intensivo de CPU, siempre debe dejar que el compilador se encargue de la optimización. Concéntrese en escribir código limpio y fácil de mantener. Esto es casi siempre de gran valor que unas pocas décimas de nano-segundo en tiempo de ejecución.

Luke Graham
fuente
3

Una razón importante para usar clases en lugar de declaraciones de cambio es que las declaraciones de cambio tienden a generar un archivo enorme que tiene mucha lógica. Esto es tanto una pesadilla de mantenimiento como un problema con la administración de la fuente, ya que debe revisar y editar ese archivo enorme en lugar de archivos de clase más pequeños

Homde
fuente
3

una declaración de cambio en el código OOP es una fuerte indicación de las clases faltantes

pruébelo en ambos sentidos y ejecute algunas pruebas de velocidad simples; Lo más probable es que la diferencia no sea significativa. Si lo son y el código es de tiempo crítico, entonces mantenga la declaración de cambio

Steven A. Lowe
fuente
3

Normalmente odio la palabra "optimización prematura", pero esto apesta. Vale la pena señalar que Knuth usó esta famosa cita en el contexto de presionar para usar gotodeclaraciones para acelerar el código en áreas críticas . Esa es la clave: caminos críticos .

Estaba sugiriendo usar gotopara acelerar el código, pero advirtió contra aquellos programadores que desearían hacer este tipo de cosas basadas en corazonadas y supersticiones de código que ni siquiera es crítico.

Favorecer las switchdeclaraciones lo más uniformemente posible a lo largo de una base de código (ya sea que se maneje o no una carga pesada) es el ejemplo clásico de lo que Knuth llama el programador "centavo y tonto" que pasa todo el día luchando por mantener su "optimizado" "código que se convirtió en una pesadilla de depuración como resultado de intentar ahorrar centavos por libras. Dicho código rara vez se puede mantener y mucho menos incluso eficiente en primer lugar.

¿Tiene razón?

Es correcto desde la perspectiva de eficiencia muy básica. Que yo sepa, ningún compilador puede optimizar el código polimórfico que involucra objetos y el despacho dinámico mejor que una declaración de cambio. Nunca terminará con una LUT o una tabla de salto al código en línea del código polimórfico, ya que dicho código tiende a servir como una barrera optimizadora para el compilador (no sabrá a qué función llamar hasta el momento en que el envío dinámico ocurre).

Es más útil no pensar en este costo en términos de tablas de salto, sino más en términos de la barrera de optimización. Para el polimorfismo, la llamada Base.method()no permite que el compilador sepa qué función realmente se llamará si methodes virtual, no está sellada y puede anularse. Dado que no sabe qué función se va a llamar de antemano, no puede optimizar la llamada a la función y utilizar más información para tomar decisiones de optimización, ya que no sabe qué función se va a llamar en la hora en que se compila el código.

Los optimizadores están en su mejor momento cuando pueden mirar en una llamada de función y hacer optimizaciones que aplanan por completo a la persona que llama y a la persona que llama, o al menos optimizan a la persona que llama para trabajar de manera más eficiente con la persona que llama. No pueden hacer eso si no saben qué función se llamará de antemano.

¿Solo está hablando por el culo?

Usar este costo, que a menudo equivale a centavos, para justificar convertirlo en un estándar de codificación aplicado de manera uniforme es generalmente muy tonto, especialmente para lugares que tienen una necesidad de extensibilidad. Eso es lo principal que debe tener en cuenta con los optimizadores prematuros genuinos: quieren convertir las preocupaciones menores de rendimiento en estándares de codificación aplicados de manera uniforme en una base de código sin tener en cuenta el mantenimiento en absoluto.

Sin embargo, me ofende un poco la cita del "viejo pirata informático C" utilizada en la respuesta aceptada, ya que soy uno de esos. No todos los que han estado codificando durante décadas a partir de un hardware muy limitado se han convertido en un optimizador prematuro. Sin embargo, también me he encontrado y trabajado con ellos. Pero esos tipos nunca miden cosas como predicciones erróneas de sucursales o errores de caché, piensan que saben mejor y basan sus nociones de ineficiencia en una base de código de producción compleja basada en supersticiones que hoy en día no son ciertas y a veces nunca lo fueron. Las personas que han trabajado genuinamente en campos críticos para el rendimiento a menudo entienden que la optimización efectiva es una priorización efectiva, y tratar de generalizar un estándar de codificación que degrada el mantenimiento para ahorrar centavos es una priorización muy ineficaz.

Los centavos son importantes cuando tienes una función barata que no hace tanto trabajo, que se llama mil millones de veces en un ciclo muy apretado y crítico para el rendimiento. En ese caso, terminamos ahorrando 10 millones de dólares. No vale la pena afeitarse centavos cuando tiene una función llamada dos veces para la cual el cuerpo solo cuesta miles de dólares. No es aconsejable pasar el tiempo regateando centavos durante la compra de un automóvil. Vale la pena regatear centavos si está comprando un millón de latas de refresco a un fabricante. La clave para una optimización efectiva es comprender estos costos en su contexto adecuado. Alguien que intenta ahorrar centavos en cada compra y sugiere que todos los demás intenten regatear centavos sin importar lo que compren, no es un optimizador experto.


fuente
2

Parece que su compañero de trabajo está muy preocupado por el rendimiento. Es posible que en algunos casos una estructura de caja / interruptor grande funcione más rápido, pero espero que ustedes hagan un experimento haciendo pruebas de tiempo en la versión OO y la versión de caja / interruptor. Supongo que la versión OO tiene menos código y es más fácil de seguir, comprender y mantener. Primero abogaría por la versión OO (ya que el mantenimiento / legibilidad debería ser inicialmente más importante), y solo consideraría la versión del switch / case solo si la versión OO tiene serios problemas de rendimiento y se puede demostrar que un switch / case hará un mejora significativa.

FrustratedWithFormsDesigner
fuente
1
Junto con las pruebas de tiempo, un volcado de código puede ayudar a mostrar cómo funciona el envío de métodos C ++ (y C #).
S.Lott
2

Una ventaja de mantenibilidad del polimorfismo que nadie ha mencionado es que podrá estructurar su código de manera mucho más agradable utilizando la herencia si siempre cambia a la misma lista de casos, pero en algún momento varios casos se manejan de la misma manera y otras veces no son

P.ej. si está cambiando entre Dog, Caty Elephant, y algunas veces Dogy Cattiene el mismo caso, puede hacer que ambos hereden de una clase abstracta DomesticAnimaly poner esas funciones en la clase abstracta.

Además, me sorprendió que varias personas usaran un analizador sintáctico como ejemplo de dónde no usarías el polimorfismo. Para un analizador similar a un árbol, este es definitivamente el enfoque incorrecto, pero si tiene algo como el ensamblaje, donde cada línea es algo independiente, y comienza con un código de operación que indica cómo debe interpretarse el resto de la línea, usaría totalmente el polimorfismo y una fabrica. Cada clase puede implementar funciones como ExtractConstantso ExtractSymbols. He usado este enfoque para un intérprete BASIC de juguete.

jwg
fuente
Un conmutador también puede heredar comportamientos, a través de su caso predeterminado. "... extiende BaseOperationVisitor" se convierte en "predeterminado: BaseOperation (nodo)"
Samuel Danielson
0

"Deberíamos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal"

Donald Knuth

Thorsten Müller
fuente
0

Incluso si esto no fuera malo para la mantenibilidad, no creo que sea mejor para el rendimiento. Una llamada de función virtual es simplemente una indirección adicional (lo mismo que el mejor caso para una declaración de cambio), por lo que incluso en C ++ el rendimiento debería ser aproximadamente igual. En C #, donde todas las llamadas a funciones son virtuales, la instrucción switch debería ser peor, ya que tiene la misma sobrecarga de llamadas a funciones virtuales en ambas versiones.

Dirk Holsopple
fuente
1
¿Falta "no"? En C #, no todas las llamadas a funciones son virtuales. C # no es Java.
Ben Voigt
0

Su colega no está hablando por fuera, en lo que respecta al comentario sobre las tablas de salto. Sin embargo, usar eso para justificar escribir código incorrecto es donde sale mal.

El compilador de C # convierte las declaraciones de cambio con solo unos pocos casos en una serie de if / else, por lo que no es más rápido que usar if / else. El compilador convierte declaraciones de cambio más grandes en un Diccionario (la tabla de salto a la que se refiere su colega). Consulte esta respuesta a una pregunta de Stack Overflow sobre el tema para obtener más detalles .

Una declaración de cambio grande es difícil de leer y mantener. Un diccionario de "casos" y funciones es mucho más fácil de leer. Como eso es en lo que se convierte el interruptor, usted y su colega deberían usar los diccionarios directamente.

David Arno
fuente
0

No necesariamente está hablando por el culo. Al menos en las switchdeclaraciones C y C ++ se puede optimizar para saltar tablas, mientras que nunca he visto que suceda con un despacho dinámico en una función que solo tiene acceso a un puntero base. Por lo menos, este último requiere un optimizador mucho más inteligente que busque mucho más código circundante para descubrir exactamente qué subtipo se está utilizando desde una llamada de función virtual a través de un puntero / referencia base.

Además de eso, el despacho dinámico a menudo sirve como una "barrera de optimización", lo que significa que el compilador a menudo no podrá insertar código y asignar registros de manera óptima para minimizar los derrames de pila y todas esas cosas elegantes, ya que no puede entender qué Se llamará a la función virtual a través del puntero base para alinearla y hacer toda su magia de optimización. No estoy seguro de que quiera que el optimizador sea tan inteligente e intente optimizar las llamadas de función indirectas, ya que eso podría conducir a que muchas ramas de código tengan que generarse por separado en una pila de llamadas dada (una función que las llamadas foo->f()tendrían para generar código de máquina totalmente diferente de uno que llamabar->f() a través de un puntero base, y la función que llama a esa función tendría que generar dos o más versiones de código, y así sucesivamente, la cantidad de código de máquina que se genera sería explosiva, tal vez no sea tan malo con un JIT de seguimiento que genera el código sobre la marcha mientras se rastrea a través de rutas de ejecución activas).

Sin embargo, como muchas respuestas se han hecho eco, esa es una mala razón para favorecer una gran cantidad de switchdeclaraciones, incluso si es sin duda más rápida en alguna cantidad marginal. Además, cuando se trata de microeficiencias, las cosas como la ramificación y la alineación generalmente tienen una prioridad bastante baja en comparación con cosas como los patrones de acceso a la memoria.

Dicho eso, salté aquí con una respuesta inusual. Quiero defender la mantenibilidad de las switchdeclaraciones sobre una solución polimórfica cuando, y solo cuando, sepa con certeza que solo habrá un lugar que necesite realizar el switch.

Un buen ejemplo es un controlador de eventos central. En ese caso, generalmente no tiene muchos lugares para manejar eventos, solo uno (por qué es "central"). Para esos casos, no se beneficia de la extensibilidad que proporciona una solución polimórfica. Una solución polimórfica es beneficiosa cuando hay muchos lugares que harían el switchenunciado analógico . Si sabe con certeza que solo habrá uno, una switchdeclaración con 15 casos puede ser mucho más simple que diseñar una clase base heredada por 15 subtipos con funciones anuladas y una fábrica para instanciarlas, solo para luego usarse en una función en todo el sistema En esos casos, agregar un nuevo subtipo es mucho más tedioso que agregar una casedeclaración a una función. En todo caso, abogaría por la mantenibilidad, no por el rendimiento,switch declaraciones en este caso peculiar donde no se beneficia de la extensibilidad en absoluto.


fuente