2009-03-20 11 views
14

He estado programando desde hace algunos años y he usado punteros de función en ciertos casos. Lo que me gustaría saber es cuándo es apropiado o no utilizarlos por motivos de rendimiento y me refiero al contexto de los juegos, no al software comercial.Las ventajas de utilizar los punteros de función

punteros de función son rápidos, John Carmack los utilizó para la magnitud del abuso en el código fuente de Quake y Doom y porque él es un genio :)

me gustaría utilizar punteros a funciones más, pero quiero usar ellos donde son más apropiados.

Actualmente, ¿cuáles son los mejores y más prácticos usos de los punteros a las funciones en lenguajes modernos de estilo c como C, C++, C# y Java, etc.?

+0

Escriba un código en JavaScript donde las funciones sean de primera clase y se enamorará. Solo un pequeño ejemplo: tengo una clase que supervisa un archivo con la configuración de los cambios. Cuando crea la clase, le pasa un delegado para invocar cada vez que cambia el archivo. – core

Respuesta

23

No hay nada especialmente "rápido" sobre los punteros de función. Le permiten llamar a una función que se especifica en el tiempo de ejecución. Pero tiene exactamente la misma sobrecarga que obtendría de cualquier otra llamada a función (más la indirección del puntero adicional). Además, dado que la función de llamada se determina en tiempo de ejecución, el compilador normalmente no puede alinear la llamada de función como podría hacerlo en cualquier otro lugar. Como tal, los punteros a las funciones pueden, en algunos casos, sumarse para ser significativamente más lentos que una llamada de función normal.

Los punteros de función no tienen nada que ver con el rendimiento, y nunca deben usarse para obtener rendimiento.

En su lugar, son un leve guiño al paradigma de programación funcional, ya que le permiten pasar una función como parámetro o valor de retorno en otra función.

Un ejemplo simple es una función de clasificación genérica. Tiene que tener alguna forma de comparar dos elementos para determinar cómo se deben ordenar. Esto podría ser un puntero de función pasado a la función de ordenamiento, y de hecho, el std :: sort() de C++ se puede usar exactamente así. Si le pide que ordene secuencias de un tipo que no define el operador menor, debe pasar un puntero de función al que pueda llamar para realizar la comparación.

Y esto nos lleva muy bien a una alternativa superior. En C++, no está limitado a los indicadores de función. A menudo usa funtores, es decir, clases que sobrecargan al operador(), para que puedan ser "llamadas" como si fueran funciones. Funtores tienen un par de grandes ventajas con respecto a los punteros de función:

  • Ofrecen una mayor flexibilidad: son clases de pleno derecho, con las variables del constructor, destructor y miembros. Pueden mantener el estado y pueden exponer otras funciones miembro que el código circundante puede invocar.
  • Son más rápidos: a diferencia de los punteros de función, cuyo tipo solo codifica la firma de la función (una variable de tipo void (*)(int) puede ser cualquier función que toma un int y devuelve nula. No podemos saber cuál), El tipo de functor codifica la función precisa que debe llamarse (dado que un funtor es una clase, llámenlo C, sabemos que la función a llamar es, y siempre será, C :: operator()). Y esto significa que el compilador puede alinear la llamada a la función. Esa es la magia que hace que std :: sort genérico sea tan rápido como su función de clasificación codificada a mano diseñada específicamente para su tipo de datos. El compilador puede eliminar todos los gastos generales de llamar a una función definida por el usuario.
  • Son más seguros: hay muy poco tipo de seguridad en un puntero de función. No tiene garantía de que apunte a una función válida. Podría ser NULL. Y la mayoría de los problemas con los punteros también se aplican a los indicadores de función. Son peligrosos y propensos a errores.

punteros de función (en C) o funtores (en C++) o delegados (en C#) todos resuelven el mismo problema, con diferentes niveles de elegancia y flexibilidad: Que le permiten tratar a funciones como valores de primera clase, pasándolos como lo harías con cualquier otra variable. Puede pasar una función a otra función y llamará a su función en momentos específicos (cuando un temporizador expira, cuando la ventana necesita volver a dibujar, o cuando necesita comparar dos elementos en su matriz)

Por lo que yo Sé (y podría estar equivocado, porque no he trabajado con Java durante siglos), Java no tiene un equivalente directo. En su lugar, debe crear una clase que implemente una interfaz y defina una función (llámese Execute(), por ejemplo). Y luego, en lugar de llamar a la función suministrada por el usuario (con la forma de un puntero de función, functor o delegado), llama a foo.Execute(). Similar a la implementación de C++ en principio, pero sin la generalidad de las plantillas de C++, y sin la sintaxis de la función que le permite tratar los punteros de funciones y funtores de la misma manera.

De modo que es allí donde usa los punteros a las funciones: cuando las alternativas más sofisticadas no están disponibles (es decir, está atrapado en C), y necesita pasar una función a otra. El escenario más común es una devolución de llamada. Define una función F a la que desea que el sistema llame cuando ocurre X. Entonces crea un puntero a la función que apunta a F y lo pasa al sistema en cuestión.

Así que realmente, olvídate de John Carmack y no asumas que todo lo que ves en su código mágicamente mejorará tu código si lo copias. Usó indicadores de función porque los juegos que mencionas estaban escritos en C, donde las alternativas superiores no están disponibles, y no porque sean un ingrediente mágico cuya mera existencia hace que el código corra más rápido.

+1

"Los punteros de función no tienen nada que ver con el rendimiento y nunca deben usarse para obtener rendimiento". En realidad, se pueden usar para mejorar el rendimiento, y también lo pueden hacer los funtores/delegados, etc. Vea el ejemplo en mi respuesta ... ¿por qué nunca se debe hacer esto? No veo qué es malo al respecto. – jheriko

+1

Es cierto. Mi punto era simplemente que simplemente llamar a un puntero de función en lugar de llamar a la función no es más rápido, ya que parecía ser lo que el OP estaba implicando. Tienes razón, si lo usas para cambiar la lógica del programa, también puede afectar el rendimiento. – jalf

+0

después de que recibí un aumento de rep por mi respuesta ... volver a leer me hizo pensar, en realidad el rendimiento varía según el mecanismo de llamada a la función, a veces drásticamente, por ejemplo. en C++, las llamadas a funciones virtuales y la herencia virtual agregan capas adicionales de direccionamiento indirecto, hasta el punto en que posiblemente pueda contaminar todo el caché. sin embargo, este es un código incorrecto por muchas otras razones ... los punteros de función pueden implementar la misma funcionalidad (herencia compleja), con sobrecarga de memoria extra, pero dejan solo la llamada dinámica, p. golpeando las cachés de instrucciones y datos una vez cada uno, contra 2..n veces – jheriko

6

Cada vez que utiliza un controlador de eventos o un delegado en C#, está utilizando efectivamente un puntero a la función.

Y no, no se trata de velocidad. Los indicadores de función son acerca de la conveniencia.

Jonathan

5

punteros de función se utilizan como devoluciones de llamada en muchos casos. Un uso es como una función de comparación en algoritmos de clasificación. Por lo tanto, si intenta comparar objetos personalizados, puede proporcionar un puntero a la función de comparación que sepa cómo manejar esos datos.

Dicho esto, voy a ofrecer una cita que recibí de un antiguo profesor mío:

Tratar con un nuevo C++, como lo haría con un arma automática cargado en una habitación llena de gente: nunca utilice sólo porque parece ingenioso. Espere hasta que comprenda las consecuencias, no se ponga lindo, escriba lo que sabe y sepa lo que escribe.

+0

jaja, eso es tan cierto! me recuerda a http://www.gotw.ca/publications/advice97.htm :) –

+0

¡Eso es exactamente a lo que se refería! Increíble. –

+0

"Escribe lo que sabes" funciona si trabajas solo en el proyecto. Pero si eres bueno en matrices ** y no estás familiarizado con STL, entonces no es bueno trabajar en un equipo. Lo mismo con funciones de C puro en todo el código del gran proyecto. –

3

Hablando de C#, pero los punteros de función se usan en todo C#. Los delegados y eventos (y Lambdas, etc.) son punteros de función bajo el capó, por lo que casi cualquier proyecto C# va a estar plagado de indicadores de función. Básicamente, cada controlador de eventos, cerca de cada consulta LINQ, etc., utilizará punteros de función.

1

punteros de función son rápidos

En qué contexto? ¿Comparado con?

Parece que solo quiere usar punteros a la función por el solo hecho de usarlos. Eso sería malo.

Un puntero a una función se utiliza normalmente como un controlador de devolución de llamada o evento.

+0

Rápido en el sentido de que son efectivamente un salto a otra parte de tu código que te evita regresar en el método actual y probablemente abandona tu ciclo y luego volviendo más tarde. –

+0

Veo su punto y estoy de acuerdo, no debería usarlo solo por el simple hecho de hacerlo. –

+0

Las llamadas a funciones regulares también son "un salto hacia otra parte de tu código". Sí, este es un comportamiento útil, pero generalmente lo obtienes mediante llamadas a funciones simples, no hay necesidad de incluir punteros a las funciones – jalf

5

¿Cuáles son los mejores y más prácticos usos de los enteros en los lenguajes modernos de estilo c?

+0

¿Pido disculpas pero no las sigo? –

+0

Si necesita un número entero, use un número entero. Si necesita un puntero a la función, use un puntero a la función. Ambos (como todos los constructos de programación) son solo herramientas, ninguno es especialmente rápido o mágico de ninguna manera. –

+0

Yo, por mi parte, disfrute de su sarcasmo, señor. – Marcin

9

Pueden ser útiles si no conoce la funcionalidad admitida por su plataforma de destino hasta el tiempo de ejecución (por ejemplo, la funcionalidad de la CPU, la memoria disponible).La solución obvia es escribir funciones de la siguiente manera:

int MyFunc() 
{ 
    if(SomeFunctionalityCheck()) 
    { 
    ... 
    } 
    else 
    { 
    ... 
    } 
} 

Si esta función se llama profundamente dentro de los bucles importantes, entonces es probable que sea mejor utilizar un puntero de función para MyFunc:

int (*MyFunc)() = MyFunc_Default; 

int MyFunc_SomeFunctionality() 
{ 
    // if(SomeFunctionalityCheck()) 
    .. 
} 

int MyFunc_Default() 
{ 
    // else 
    ... 
} 

int MyFuncInit() 
{ 
    if(SomeFunctionalityCheck()) MyFunc = MyFunc_SomeFunctionality; 
} 

Hay otros usos por supuesto, como callback functions, ejecutando código de bytes desde la memoria o para crear un lenguaje interpretado.

Para ejecutar Intel compatible byte code en Windows, lo que puede ser útil para un intérprete. Por ejemplo, aquí es una función stdcall regresar 42 (0x2A) almacenados en una matriz que puede ser ejecutado:

code = static_cast<unsigned char*>(VirtualAlloc(0, 6, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); 
// mov eax, 42 
code[0] = 0x8b; 
code[1] = 0x2a; 
code[2] = 0x00; 
code[3] = 0x00; 
code[4] = 0x00; 
// ret 
code[5] = 0xc3; 
// this line executes the code in the byte array 
reinterpret_cast<unsigned int (_stdcall *)()>(code)(); 

... 

VirtualFree(code, 6, MEM_RELEASE); 

);

+2

Realmente perspicaz, gracias por el ejemplo de código –

+0

Debe haber Estrategia o algún otro patrón de diseño como Decorador. Por supuesto si hablamos de C++. –

4

En las oscuras edades antes de C++, había un patrón común que utilicé en mi código que era definir una estructura con un conjunto de indicadores de función que (típicamente) operaban en esa estructura de alguna manera y proporcionaban comportamientos particulares para ello. En términos de C++, estaba construyendo un vtable. La diferencia era que podía aplicar un efecto secundario a la estructura en tiempo de ejecución para cambiar comportamientos de objetos individuales sobre la marcha según fuera necesario. Esto ofrece un modelo de herencia mucho más rico a costa de la estabilidad y la facilidad de depuración. El mayor costo, sin embargo, fue que había exactamente una persona que podría escribir este código de manera efectiva: yo.

Utilicé esto en gran medida en un marco de interfaz de usuario que me permitió cambiar la forma en que se pintaron los objetos, quién era el objetivo de los comandos, y así sucesivamente, algo que ofrecían muy pocas UI.

Tener este proceso formalizado en OO idiomas es mejor en todo sentido.

+0

Exactamente. He usado referencias de código en varios idiomas para emular OO. FYI: no todas las clases de idiomas "cercanas". Ruby, Javascript y Objective C te permiten hacer lo que dijiste sobre el cambio de implementaciones en tiempo de ejecución. – Roboprog

3

Los punteros de función son un intento de un hombre pobre para ser funcional. Incluso podría argumentar que tener punteros de función hace que un lenguaje sea funcional, ya que puede escribir funciones de orden superior con ellos.

Sin cierres y sintaxis fácil, son muy burdos. Entonces tiendes a usarlos mucho menos de lo deseable. Principalmente para funciones de "devolución de llamada".

A veces, el diseño de OO funciona alrededor del uso de funciones al crear en su lugar un tipo de interfaz completa para pasar la función necesaria.

C# tiene cierres, por lo que los punteros de función (que en realidad almacenan un objeto por lo que no es solo una función en bruto, sino también el estado tipeado) son mucho más utilizables allí.

Editar Uno de los comentarios dice que debe haber una demostración de funciones de orden superior con punteros a funciones.Cualquier función que tome una función de devolución de llamada es una función de orden superior. Como, por ejemplo, EnumWindows:

BOOL EnumWindows(   
    WNDENUMPROC lpEnumFunc, 
    LPARAM lParam 
); 

El primer parámetro es la función para pasar, bastante fácil. Pero como no hay cierres en C, obtenemos este precioso segundo parámetro: "Especifica un valor definido por la aplicación que se pasará a la función de devolución de llamada". El valor definido por la aplicación le permite pasar manualmente el estado sin tipo para compensar la falta de cierres.

El .NET framework también está lleno de diseños similares. Por ejemplo, IAsyncResult. AsyncState: "Obtiene un objeto definido por el usuario que califica o contiene información sobre una operación asincrónica". Como el IAR es todo lo que obtienes en tu devolución de llamada, sin cierres, necesitas una forma de introducir algunos datos en la operación asíncrona para poder expulsarlos más tarde.

+0

Tengo curiosidad por qué alguien votó este a -1. Parece una respuesta correcta (y bien pensada). +1 desde aquí para compensar. – jalf

+0

+1, totalmente de acuerdo con jalf - WTF? –

+0

No bajé la voz, pero ¿quizás debería demostrar cómo construir funciones de orden superior con fps? –

3

Hay ocasiones en que el uso de punteros de función puede acelerar el procesamiento. Se pueden usar tablas de despacho sencillas en lugar de sentencias de cambio largas o secuencias if-then-else.

2

Según mi experiencia personal, pueden ayudarte a guardar líneas de código importantes.

considerar la condición: {

switch(sample_var) 
{ 

case 0: 
      func1(<parameters>); 
      break; 

case 1: 
      func2(<parameters>); 
      break; 











up to case n: 
       funcn(<parameters>); 
       break; 

}

donde func1() ... funcn() son funciones con protype misma. Lo que podríamos hacer es: declarar una matriz de punteros de función arrFuncPoint que contiene las direcciones de las funciones func1() a funcn()

Entonces todo el caso del interruptor sería reemplazado por

* arrFuncPoint [sample_var];

Cuestiones relacionadas