¿Cuál es el costo de usar un puntero a una función miembro vs.¿Un interruptor?

StackOverflow https://stackoverflow.com/questions/113150

  •  02-07-2019
  •  | 
  •  

Pregunta

Tengo la siguiente situación:


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

En mi implementación actual guardo el valor de whichFoo en un miembro de datos en el constructor y usar un switch en callFoo() para decidir a cuál de los foo llamar.Alternativamente, puedo usar un switch en el constructor para guardar un puntero a la derecha fooN() ser llamado callFoo().

Mi pregunta es qué camino es más eficiente si un objeto de clase A solo se construye una vez, mientras que callFoo() Se llama un gran número de veces.Entonces, en el primer caso tenemos múltiples ejecuciones de una declaración de cambio, mientras que en el segundo solo hay un cambio y múltiples llamadas de una función miembro usando el puntero a él.Sé que llamar a una función miembro usando un puntero es más lento que simplemente llamarla directamente.¿Alguien sabe si estos gastos generales son mayores o menores que el costo de un switch?

Aclaración:Me doy cuenta de que nunca se sabe realmente qué enfoque ofrece un mejor rendimiento hasta que lo pruebas y lo cronometras.Sin embargo, en este caso ya tengo implementado el enfoque 1 y quería saber si el enfoque 2 puede ser más eficiente, al menos en principio.Parece que puede serlo, y ahora tiene sentido que me moleste en implementarlo y probarlo.

Ah, y también me gusta más el enfoque 2 por razones estéticas.Supongo que estoy buscando una justificación para implementarlo.:)

¿Fue útil?

Solución

¿Qué tan seguro está de que llamar a una función miembro mediante un puntero es más lento que simplemente llamarla directamente?¿Puedes medir la diferencia?

En general, no debes confiar en tu intuición al realizar evaluaciones de desempeño.Siéntese con su compilador y una función de sincronización y, de hecho, medida las diferentes opciones.¡Quizás te sorprendas!

Más información:Hay un excelente articulo. Punteros de funciones miembro y los delegados de C++ más rápidos posibles que entra en detalles muy profundos sobre la implementación de punteros de funciones miembro.

Otros consejos

Puedes escribir esto:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

El cálculo del puntero de función real es lineal y rápido:

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

Tenga en cuenta que ni siquiera es necesario corregir el número de función real en el constructor.

He comparado este código con el conjunto generado por un switch.El switch La versión no proporciona ningún aumento de rendimiento.

Para responder a la pregunta formulada:en el nivel más fino, el puntero a la función miembro funcionará mejor.

Para abordar la pregunta no formulada:¿Qué significa "mejor" aquí?En la mayoría de los casos, esperaría que la diferencia fuera insignificante.Sin embargo, dependiendo de lo que haga la clase, la diferencia puede ser significativa.Evidentemente, probar el rendimiento antes de preocuparse por la diferencia es el primer paso correcto.

Si vas a seguir usando un interruptor, lo cual está perfectamente bien, entonces probablemente deberías poner la lógica en un método auxiliar y llamarlo desde el constructor.Alternativamente, éste es un caso clásico de Patrón de estrategia.Podrías crear una interfaz (o clase abstracta) llamada IFoo que tenga un método con la firma de Foo.Haría que el constructor tomara una instancia de IFoo (constructor Inyección de dependencia que implementó el método foo que desea.Tendrías un IFoo privado que se configuraría con este constructor, y cada vez que quisieras llamar a Foo llamarías a la versión de tu IFoo.

Nota:No he trabajado con C++ desde la universidad, por lo que mi jerga podría no estar bien aquí, pero las ideas generales son válidas para la mayoría de los lenguajes OO.

Si su ejemplo es código real, entonces creo que debería revisar el diseño de su clase.Pasar un valor al constructor y usarlo para cambiar el comportamiento es realmente equivalente a crear una subclase.Considere refactorizar para hacerlo más explícito.El efecto de hacerlo es que su código terminará usando un puntero de función (todos los métodos virtuales son, en realidad, punteros de función en tablas de salto).

Sin embargo, si su código fuera solo un ejemplo simplificado para preguntar si, en general, las tablas de salto son más rápidas que las declaraciones de cambio, entonces mi intuición diría que las tablas de salto son más rápidas, pero usted depende del paso de optimización del compilador.Pero si el rendimiento es realmente una preocupación, nunca confíe en la intuición: cree un programa de prueba y pruébelo, o mire el ensamblador generado.

Una cosa es segura: una declaración de cambio nunca será más lenta que una tabla de salto.La razón es que lo mejor que puede hacer el optimizador de un compilador será realizar una serie de pruebas condicionales (es decir,un interruptor) en una mesa de salto.Entonces, si realmente quiere estar seguro, saque el compilador del proceso de decisión y use una tabla de salto.

Suena como si deberías hacer callFoo una función virtual pura y crear algunas subclases de A.

A menos que realmente necesite velocidad, haya realizado una extensa elaboración de perfiles e instrumentación y haya determinado que las llamadas a callFoo son realmente el cuello de botella.¿Tiene?

Los punteros de función casi siempre son mejores que los si encadenados.Crean un código más limpio y casi siempre son más rápidos (excepto quizás en el caso en el que solo se trata de elegir entre dos funciones y siempre se predice correctamente).

Debería pensar que el puntero sería más rápido.

Las CPU modernas captan previamente instrucciones;Las ramas mal predichas vacían el caché, lo que significa que se detiene mientras lo recarga.Un puntero no hace eso.

Por supuesto, debes medir ambos.

Optimice solo cuando sea necesario

Primero:La mayoría de las veces lo más probable es que no te importe, la diferencia será muy pequeña.Primero asegúrese de que optimizar esta llamada realmente tenga sentido.Sólo si sus mediciones muestran que realmente se invierte un tiempo significativo en la sobrecarga de la llamada, proceda a optimizarla (enchufe descarado - Cf. ¿Cómo optimizar una aplicación para hacerla más rápida?) Si la optimización no es significativa, prefiera el código más legible.

El costo de las llamadas indirectas depende de la plataforma de destino

Una vez que haya determinado que vale la pena aplicar la optimización de bajo nivel, es el momento de comprender su plataforma de destino.El costo que puede evitar aquí es la penalización por predicción errónea de la sucursal.En las CPU x86/x64 modernas, es probable que esta predicción errónea sea muy pequeña (pueden predecir llamadas indirectas bastante bien la mayor parte del tiempo), pero cuando se apunta a PowerPC u otras plataformas RISC, las llamadas/saltos indirectos a menudo no se predicen en absoluto y se evitan. ellos pueden causar una ganancia significativa en el rendimiento.Ver también El costo de la llamada virtual depende de la plataforma.

El compilador también puede implementar el cambio usando la tabla de salto

Un problema:A veces, el cambio también se puede implementar como una llamada indirecta (usando una tabla), especialmente cuando se cambia entre muchos valores posibles.Un cambio de este tipo presenta el mismo error de predicción que una función virtual.Para que esta optimización sea confiable, probablemente se preferiría usar if en lugar de switch en el caso más común.

Utilice cronómetros para ver cuál es más rápido.Aunque, a menos que este código se repita una y otra vez, es poco probable que notes alguna diferencia.

Asegúrese de que si está ejecutando código desde el constructor, si la construcción falla, no perderá memoria.

Esta técnica se utiliza mucho con el sistema operativo Symbian:http://www.titu.jyu.fi/modpa/Patterns/pattern-TwoPhaseConstruction.html

Si solo llama a callFoo() una vez, entonces más probable el puntero de función será más lento en una cantidad insignificante.Si lo llamas muchas veces más probable el puntero de función será más rápido en una cantidad insignificante (porque no necesita seguir pasando por el interruptor).

De cualquier manera, mire el código ensamblado para asegurarse de que está haciendo lo que cree que está haciendo.

Una ventaja que a menudo se pasa por alto al cambiar (incluso frente a la clasificación y la indexación) es saber que se utiliza un valor particular en la gran mayoría de los casos.Es fácil pedir el interruptor para comprobar primero los más comunes.

PD.Para reforzar la respuesta de Greg, si te importa la velocidad, mide.Mirar el ensamblador no ayuda cuando las CPU tienen precarga/ramificación predictiva y paradas de canalización, etc.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top