sirven que punteros puntero para operaciones los declaracion con cadenas aritmetica c++ function-pointers

que - punteros c++ pdf



¿Cuál es el costo de usar un puntero a la función miembro frente a un interruptor? (12)

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, whichFoo el valor de whichFoo en un miembro de datos en el constructor y uso 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() para ser llamado en callFoo() .

Mi pregunta es qué camino es más eficiente si un objeto de clase A se construye solo una vez, mientras que callFoo() se llama una gran cantidad de veces. Entonces, en el primer caso tenemos múltiples ejecuciones de una instrucción switch, mientras que en el segundo solo hay un switch, y múltiples llamadas de un miembro funcionan usando el puntero al mismo. Sé que llamar a una función miembro usando un puntero es más lento que simplemente llamarlo directamente. ¿Alguien sabe si esta sobrecarga es más o menos que el costo de un switch ?

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

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


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

En general, no debe confiar en su intuición cuando haga evaluaciones de desempeño. Siéntese con su compilador y una función de tiempo, y realmente mida las diferentes opciones. ¡Te sorprenderás!

Más información: hay un artículo excelente Pointers de función de miembros y los delegados de C ++ más rápidos posibles, que profundiza en los detalles sobre la implementación de los punteros de funciones de los miembros.


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

Las CPU modernas captan las instrucciones; las ramas mal pronosticadas limpian el caché, lo que significa que se detiene mientras recarga el caché. Un puntero no hace eso.

Por supuesto, debes medir ambos.


Los punteros de función son casi siempre mejores que los ifs encadenados. Hacen un código más limpio, y casi siempre son más rápidos (excepto tal vez en el caso en que solo es una elección entre dos funciones y siempre se predice correctamente).


Para responder a la pregunta planteada: en el nivel de grano 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 la clase que esté haciendo, la diferencia puede ser significativa. Las pruebas de rendimiento antes de preocuparse por la diferencia obviamente son el primer paso correcto.


Parece que debes hacer que callFoo una función virtual pura y crear algunas subclases de A

A menos que realmente necesite la velocidad, ha realizado una amplia elaboración de perfiles e instrumentos, y ha determinado que las llamadas a callFoo son realmente el cuello de botella. ¿Tienes?


Si solo llama a callFoo () una vez, lo más probable es que el puntero a la función sea más lento en una cantidad insignificante. Si lo llama muchas veces, lo más probable es que el puntero de la función sea más rápido en una cantidad insignificante (porque no necesita seguir el interruptor).

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


Si su ejemplo es código real, entonces creo que debería volver a visitar su diseño de clase. Pasarle 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, realmente, punteros de función en tablas de salto).

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

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


Si vas a seguir usando un conmutador, que está perfectamente bien, entonces probablemente debas poner la lógica en un método de ayuda y llamar si proviene del constructor. Alternativamente, este es un caso clásico del Patrón de Estrategia . Podrías crear una interfaz (o clase abstracta) llamada IFoo que tiene un método con la firma de Foo. El constructor debería tener en cuenta una instancia de IFoo (constructor Dependancy Injection) que implementó el método foo que usted desea. Usted tendría un IFoo privado que se establecería con este constructor, y cada vez que quisiera llamar a Foo llamaría a su La versión de IFoo

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


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

PD. Para reforzar la respuesta de Greg, si te importa la velocidad: mide. Mirar el ensamblador no ayuda cuando las CPU tienen precaptura / ramificación predictiva y puestos de tuberías, etc.


Use temporizadores para ver cuál es más rápido. Aunque a menos que este código vaya a ser una y otra vez, es poco probable que note alguna diferencia.

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

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


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 tiene que corregir el número de función real en el constructor.

He comparado este código con el asm generado por un switch . La versión del switch no proporciona ningún aumento en el rendimiento.


Optimizar 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. Asegúrese de que la optimización de esta llamada realmente tenga sentido primero. Solo si sus mediciones muestran que hay un tiempo realmente importante en la sobrecarga de llamadas, proceda a optimizarlo (plug sin vergüenza - 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 indirecto de la llamada depende de la plataforma objetivo

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

El compilador puede implementar el interruptor usando la mesa de salto también

One gotcha: Switch también se puede implementar a veces como una llamada indirecta (usando una tabla), especialmente al cambiar entre muchos valores posibles. Tal interruptor exhibe la misma predicción errónea que una función virtual. Para que esta optimización sea confiable, probablemente se prefiera usar if en lugar de cambiar para el caso más común.