2011-01-29 17 views
8

En una entrevista, ¿me preguntaron si usar punteros de función sería beneficioso (en términos de velocidad) al escribir código para sistemas integrados? No tenía idea sobre el sistema integrado, así que no pude responder la pregunta. Solo una respuesta turbia o vaga. ¿Cuáles son los beneficios reales? Velocidad, legibilidad, mantenimiento, costo?Punteros de función en sistemas integrados, ¿son útiles?

+1

El entrevistador claramente no tenía mucha experiencia con sistemas integrados y/o programación, para hacer esa pregunta. ¡Los beneficios podrían ser tanto velocidad, legibilidad y mantenimiento! Aunque los punteros de función "nativos" tienen una sintaxis bastante horrible, que debes borrar, o no serán legibles. – Lundin

+3

Esta pregunta se puede leer de dos maneras: la mayoría de las personas asume que la velocidad de ejecución es el problema, pero también se puede leer literalmente para preguntarse sobre el beneficio en la velocidad de escritura del código. Eso puede ser sustancial, si los usa como medio para colapsar las características comunes entre casos levemente diferentes u objetos de datos.Partes del trabajo incorporado necesitan ejecutarse rápidamente; en otros casos, como las UI integradas, lo más importante para lograr que la funcionalidad enriquecida funcione en uno es a menudo un corto ciclo de desarrollo. –

+4

El truco cuando se le formula una pregunta ambigua por parte de un entrevistador es hacer contra-preguntas inteligentes para aclarar. Eso puede ser * exactamente * lo que el entrevistador está buscando. – Clifford

Respuesta

19

Creo que tal vez la respuesta de Viren Shakya omita el punto que el entrevistador estaba tratando de obtener. En algunas construcciones, el uso de un puntero a función puede acelerar la ejecución. Por ejemplo, si tiene un índice, usarlo para indexar una matriz de indicadores de funciones puede ser más rápido que un cambio grande.

Sin embargo, si está comparando una llamada a función estática con una llamada a través de un puntero, entonces Viren tiene razón al señalar que hay una operación adicional para cargar la variable del puntero. Pero nadie trata razonablemente de usar un puntero de función de esa manera (solo como una alternativa a llamar directamente).

Llamar a una función a través de un puntero no es una alternativa a una llamada directa. Entonces, la pregunta de "ventaja" es defectuosa; se usan en diferentes circunstancias, a menudo para simplificar otra lógica de código y controlar el flujo y no simplemente para evitar una llamada de función estática. Su utilidad radica en que la determinación de la función a la que se llama se realiza dinámicamente en tiempo de ejecución por su código en lugar de hacerlo de forma estática por el vinculador. En ese sentido, son, por supuesto, útiles en los sistemas integrados, pero no por ninguna razón relacionada específicamente con los sistemas integrados.

+1

Buena respuesta, haciendo la distinción de la decisión dinámica (en tiempo de ejecución) de qué llamar a través de la función puntero. También es bueno señalar que las ventajas/usos de los indicadores de función realmente no tienen nada que ver con los sistemas integrados per se. – Dan

+0

Gracias Clifford por aclaración. – Viren

3

Hubiera dicho que son beneficiosas (en términos de velocidad) en cualquier entorno, no solo incrustado. La idea es que una vez que el apuntador ha sido apuntado a la función correcta, no se requiere más lógica de decisión para llamar a esa función.

+0

Ok, editado la pregunta, puede ser más claro ahora –

+0

Editado del mismo modo. – trojanfoe

+0

Puede que aún no esté del todo claro, pero creo que su punto es que el uso de punteros a funciones puede en algunas circunstancias simplificar o eliminar la lógica de selección. La velocidad no proviene simplemente de reemplazar una llamada estática con una llamada de puntero; eso no es práctico y, como señaló @Viren, no es más rápido. – Clifford

0

Veamos ...

velocidad (digamos que estamos en ARM): entonces (teóricamente):

(tamaño de llamadas instrucciones ARM Función normal) < (Función puntero de instrucciones establecimiento de llamada (s) tamaño)

Dado que su es un nivel adicional de indirección para configurar una función de llamada de puntero, implicará una instrucción ARM adicional.

PS: Llamada a una función normal: una llamada de función que está configurada con BL.

PSS: No conoce los tamaños reales pero debe ser fácil de verificar.

+3

Dudo que el entrevistador estuviera buscando esa respuesta. – Clifford

+1

¿Por qué sigues hablando de ARM aquí? ¿Qué en la pregunta original te está dirigiendo a adaptar tu respuesta a la arquitectura ARM? – Dan

+1

De los chips incrustados que funcionan bien con C, las variantes ARM son populares en este momento ... –

1

Una parte negativa de los punteros a las funciones es que nunca se insertarán en las listas de llamadas. Esto puede o no ser beneficioso, dependiendo de si está compilando para velocidad o tamaño. Si es este último, no deberían ser diferentes a las llamadas a funciones normales.

1

Otra desventaja de los punteros de función (con respecto a las funciones virtuales, ya que no son más que los punteros de función en el nivel básico):

hacer una función en línea & & virtuales obligará compilador para crear fuera de la línea de copiado de la misma función. Esto aumentará el tamaño del binario final (suponiendo que se hace un uso intensivo).

Regla de oro: 1: No hacer llamadas virtuales en línea

5

Se gana en velocidad, pero pierde algo en la legibilidad y mantenimiento. En lugar de un árbol if-then-else, si a, entonces fun_a(), else si b entonces fun_b(), else si c entonces fun_c() else fun_default(), y tener que hacer eso cada vez, en su lugar si a continuación, un divertido = fun_a, else si b luego fun = fun_b, etc. y lo haces una vez, a partir de ese momento solo debes llamar a fun(). Mucho mas rápido. Como se señaló, no se puede alinear, que es otro truco de velocidad, pero incluir en el árbol if-then-else no necesariamente lo hace más rápido que sin forzar y generalmente no es tan rápido como el puntero a la función.

Pierde un poco de legibilidad y mantenimiento porque tiene que averiguar dónde se configura fun(), con qué frecuencia cambia, asegúrese de que no lo llame antes de configurarlo, pero sigue siendo un nombre de búsqueda único que puede usar para encontrar y mantener todos los lugares donde se usa.

Básicamente es un truco de velocidad para evitar árboles if-then-else cada vez que desee realizar una función. Si el rendimiento no es crítico, si nada más divertido() podría ser estático y tener el árbol if-then-else en él.

EDITAR Agregando algunos ejemplos para explicar de lo que estaba hablando.

 
extern unsigned int fun1 (unsigned int a, unsigned int b); 

unsigned int (*funptr)(unsigned int, unsigned int); 

void have_fun (unsigned int x, unsigned int y, unsigned int z) 
{ 
    unsigned int j; 
    funptr=fun1; 

    j=fun1(z,5); 
    j=funptr(y,6); 
} 

Compilación da esto:

 
have_fun: 
    stmfd sp!, {r3, r4, r5, lr} 
    .save {r3, r4, r5, lr} 
    ldr r4, .L2 
    mov r5, r1 
    mov r0, r2 
    mov r1, #5 
    ldr r2, .L2+4 
    str r2, [r4, #0] 
    bl fun1 
    ldr r3, [r4, #0] 
    mov r0, r5 
    mov r1, #6 
    blx r3 
    ldmfd sp!, {r3, r4, r5, pc} 

lo que supongo que Clifford estaba hablando es que una llamada directa, si están cerca de suficiente (dependiendo de la arquitectura), es una instrucción

 
    bl fun1 

Donde un puntero a la función, le va a costar al menos dos

 
    ldr r3, [r4, #0] 
    blx r3 

También mencioné que la diferencia entre directo e indirecto era la carga extra en la que incurre.

Antes de continuar, vale la pena mencionar los pros y los contras de la inlining. En el caso de ARM, que es lo que utilizan estos ejemplos, la convención de llamada utiliza r0-r3 para los parámetros entrantes a una función y r0 para devolver. Entonces la entrada en have_fun() con tres parámetros significa que r0-r3 tiene contenido. Con ARM también se supone que una función puede destruir r0-r3, por lo que have_fun() necesita conservar las entradas y luego colocar dos entradas en fun1() en r0 y r1, por lo que se produce un poco de registro.

 
    mov r5, r1 
    mov r0, r2 
    mov r1, #5 
    ldr r2, .L2+4 
    str r2, [r4, #0] 
    bl fun1 

El compilador fue lo suficientemente inteligente como para ver que nunca necesitamos la primera entrada a la función have_fun(), por lo que R0 se descartó y se deja ser cambiado de inmediato. Además, el compilador fue lo suficientemente inteligente como para saber que nunca necesitaríamos el tercer parámetro, z (r2), después de enviarlo a fun1() en la primera llamada, por lo que no fue necesario guardarlo en un registro alto . R1 sin embargo, el segundo parámetro para have_fun() necesita ser preservado por lo que se pone en un regsiter que no será destruido por fun1().

Puede ver el mismo tipo de cosas que suceden para la segunda llamada a la función.

fun1 Suponiendo() es la función simple:

 
inline unsigned int fun1 (unsigned int a, unsigned int b) 
{ 
    return(a+b); 
} 

Cuando fun1 en línea() se obtiene algo como esto:

 
    stmfd sp!, {r4, lr} 
    mov r0, r1 
    mov r1, #6 
    add r4, r2, #5 

El compilador no tiene que mezclar los registros más bajos sobre a prepararse para una llamada. Del mismo modo, lo que puede haber notado es que r4 y lr se conservan en la pila cuando ingresamos hello_fun(). Con esta convención de llamadas ARM una función puede destruir r0-r3 pero debe conservar todos los demás registros, ya que have_fun() en este caso necesitó más que cuatro registros para hacer lo suyo guardó el contenido de r4 en la pila que podría usarlo Del mismo modo esta función compilé y llamé a otra función, la instrucción bl/blx usa/destruye el registro lr (r14) así que para que have_fun() regrese también tenemos para preservar lr en la pila.El ejemplo simplificado para fun1() hizo no mostrar esto, pero otro ahorro que obtiene de la alineación es que en la entrada la función llamada no tiene que configurar un marco de pila y conservar los registros , realmente es como si tomara el código desde la función y lo insertó en línea con la función de llamada.

¿Por qué no has estado en línea todo el tiempo? Bueno, primero puede y usará más registros y eso puede conducir a un mayor uso de la pila, y la pila es lenta en relación con los registros. Sin embargo, lo más importante es que aumenta el tamaño de tu binario , si fun1() era una función de buen tamaño y llamaste a 20 veces en have_fun() tu binario sería considerablemente más grande. Para computadoras modernas con gigabytes de RAM, unos pocos cientos o pocas docenas de miles de bytes no es gran cosa, pero para recursos integrados incrustados esto puede hacer o romper. En un escritorio gigahertz multinúcleo moderno, ¿con qué frecuencia necesita rasurar una instrucción o cinco de todas formas? A veces sí, pero no todo el tiempo para cada función. Entonces, porque probablemente pueda salirse con la suya en un escritorio que probablemente no debería.

Volver a los indicadores de función. Así que el punto que estaba tratando de hacer con mi respuesta es, ¿qué situaciones es probable que desee utilizar un puntero a función de todos modos, cuáles son los casos de uso y en esos casos de uso cuánto le ayuda o duele ?

Los tipos de casos en los que estaba pensando son complementos, o código específico para un parámetro de llamada o código genérico que reacciona al hardware específico detectado. Por ejemplo, un programa de alquitrán hipotético puede querer generar en una unidad de cinta, sistema de archivos u otro y puede optar por escribir el código con funciones genéricas llamadas mediante punteros a funciones. En la entrada del programa, los parámetros de la línea de comando indican la salida y en el ese punto se configuran los punteros a las funciones específicas del dispositivo .

 
if(outdev==OUTDEV_TAPE) data_out=data_out_tape; 
else if(outdev==OUTDEV_FILE) 
{ 
    //open the file, etc 
    data_out=data_out_file; 
} 
... 

O tal vez usted no sabe si está ejecutando en un procesador con una FPU o qué tipo FPU que tiene, pero se sabe que un punto de división flotante que quiere hacer se puede ejecutar mucho más rápido utilizando la FPU:

 
if(fputype==FPU_FPA) fdivide=fdivide_fpa; 
else if(fputype==FPU_VFP) fdivide=fdivide_vfp; 
else fdivide=fdivide_soft; 

y absolutamente se puede utilizar una declaración de caso en lugar de un if-then-else árboles, los pros y los contras de cada uno, algunos compiladores girar un int declaración de caso un if-then-else árbol de todos modos, por lo no siempre importa. El punto que estaba tratando de hacer es que si usted hace esto una sola vez:

 
if(fputype==FPU_FPA) fdivide=fdivide_fpa; 
else if(fputype==FPU_VFP) fdivide=fdivide_vfp; 
else fdivide=fdivide_soft; 

y hacer esto en todo el resto del programa:

 
a=fdivide(b,c); 

En comparación con una alternativa no la función de puntero donde haces esto cada donde desea dividir:

enfoque puntero
 
if(fputype==FPU_FPA) a=fdivide_fpa(b,c); 
else if(fputype==FPU_VFP) a=fdivide_vfp(b,c); 
else a=fdivide_soft(b,c); 

La función, a pesar de que le cuesta un LDR adicional en cada llamada, es mucho más barato que las muchas instrucciones requeridas para el árbol if-then-else.Se paga un poco por adelantado para configurar el fdivide puntero de una sola vez y luego pagar un LDR extra en cada caso, pero en general es más rápido que esto:

 

unsigned int fun1 (unsigned int a, unsigned int b); 
unsigned int fun2 (unsigned int a, unsigned int b); 
unsigned int fun3 (unsigned int a, unsigned int b); 

unsigned int (*funptr)(unsigned int, unsigned int); 

unsigned int have_fun (unsigned int x, unsigned int y, unsigned int z) 
{ 
    unsigned int j; 

    switch(x) 
    { 
     default: 
     case 1: j=fun1(y,z); break; 
     case 2: j=fun2(y,z); break; 
     case 3: j=fun3(y,z); break; 
    } 
    return(j); 
} 

unsigned int more_fun (unsigned int x, unsigned int y, unsigned int z) 
{ 
    unsigned int j; 
    j=funptr(y,z); 
    return(j); 
} 

nos da esto:

 
    cmp r0, #2 
    beq .L3 
    cmp r0, #3 
    beq .L4 
    mov r0, r1 
    mov r1, r2 
    b fun1 
.L3: 
    mov r0, r1 
    mov r1, r2 
    b fun2 
.L4: 
    mov r0, r1 
    mov r1, r2 
    b fun3 

en lugar de esto

 
    mov r0, r1 
    ldr r3, .L7 
    mov r1, r2 
    blx r3 

Para el caso por defecto el árbol if-then-else quema de dos y dos compara EQB de antes de llamar la función directamente. Básicamente, a veces el árbol if-then-else será más rápido y, a veces, el puntero a la función es más rápido.

Otro comentario que hice fue: ¿qué ocurre si usaste en línea para hacer ese árbol de if-then-else más rápido, en lugar de un puntero a función, en línea es siempre más rápido, ¿verdad?

 
unsigned int fun1 (unsigned int a, unsigned int b) 
{ 
    return(a+b); 
} 
unsigned int fun2 (unsigned int a, unsigned int b) 
{ 
    return(a-b); 
} 
unsigned int fun3 (unsigned int a, unsigned int b) 
{ 
    return(a&b); 
} 

unsigned int have_fun (unsigned int x, unsigned int y, unsigned int z) 
{ 
    unsigned int j; 

    switch(x) 
    { 
     default: 
     case 1: j=fun1(y,z); break; 
     case 2: j=fun2(y,z); break; 
     case 3: j=fun3(y,z); break; 
    } 
    return(j); 
} 

da

 
have_fun: 
    cmp r0, #2 
    rsbeq r0, r2, r1 
    bxeq lr 
    cmp r0, #3 
    addne r0, r2, r1 
    andeq r0, r2, r1 
    bx lr 

LOL, ARM me consiguió en que uno. Eso es bueno. Se puede imaginar, aunque para un procesador genérico que se obtendría algo así como

 
    cmp r0, #2 
    beq .L3 
    cmp r0, #3 
    beq .L4 
    and r0,r1,r2 
    bx lr 
.L3: 
    sub r0,r1,r2 
    bx lr 
.L4: 
    add r0,r1,r2 
    bx lr 

Todavía se quema la compara, los más casos usted tiene la más larga sea la árbol if-then-else. No es necesario mucho para el caso promedio tomar más tiempo que una solución de puntero a función.

 
    mov r0, r1 
    ldr r1, .L7 
    ldr r3,[r1] 
    mov r1, r2 
    blx r3 

Luego mencionó también la legibilidad y mantenimiento, utilizando el enfoque puntero de función que necesita para estar siempre al tanto de si o no el puntero de función ha sido asignado antes de usarlo. Usted no puede siempre grep para el nombre de esa función y encontrar lo que está buscando en el código de alguien más, idealmente se encuentra un lugar donde se asigna ese puntero, entonces puede grep para los nombres de las funciones reales.

Sí, hay muchos otros casos de uso para punteros a funciones, y los que he descrito pueden resolverse de muchas otras formas, eficiente o no. Estaba tratando de darle al cartel algunas ideas sobre cómo pensar a través de diferentes escenarios.

Creo que la respuesta más importante a esta pregunta de la entrevista no es que hay una respuesta correcta o incorrecta, porque creo que no es así. Pero para ver lo que el entrevistado sabe acerca de lo que hacen o no hacen los compiladores , el tipo de cosas que he descrito anteriormente. La pregunta de la entrevista para mí es algunas preguntas, ¿entiendes qué hace realmente el compilador , qué instrucciones genera? ¿Entiende que menos o más instrucciones no es necesariamente más rápido. ¿Entiende estas diferencias en diferentes procesadores, o al menos tiene un conocimiento de trabajo para al menos un procesador. Luego pasa a legibilidad y mantenimiento.Esa es otra secuencia de preguntas que tiene que ver con su experiencia al leer el código de otras personas, y y luego mantener su propio código u otro código de personas. Es una pregunta ingeniosamente diseñada en mi opinión.

+0

y no tiene nada que ver con incrustado vs no incrustado. –

+1

Dudo que su uso sugerido sea más rápido, mucho menos "mucho más rápido". En la primera vez que llamas a una función, en el segundo configuras una variable y luego llamas a una función; ¿cómo es eso más rápido, y el árbol si entonces entonces no se erradica en absoluto. Muy ligeramente más lento sugeriría. Puede reducir marginalmente el tamaño del código. – Clifford

+1

@Clifford en un brazo, por ejemplo, bl r0 es una solución bastante rápida en el momento de la ejecución, en comparación con cantidades aleatorias de código, luego dirección bl, si no cantidades aleatorias de código, entonces dirección bl, sino cantidades aleatorias de código ... Estaba hablando de uso práctico. si compara directamente en línea vs directamente usando la función vs un puntero a esa función (y no uso) entonces la velocidad está en ese orden, en línea (do function), directo (guardar y preparar regs, bl address, apilar cosas, función, apilar cosas , bx lr), indirecto (guardar y preparar reg, ldr rx, dirección a la función, rx rx, apilar cosas, función, apilar cosas, bx lr) –

16

Hay muchos usos.

El uso más importante de punteros a funciones en los sistemas integrados es crear tablas de vectores. Muchas arquitecturas de MCU usan una tabla de direcciones ubicada en NVM, donde cada dirección apunta a un ISR (rutina de servicio de interrupción). Dicha tabla de vectores se puede escribir en C como una matriz de indicadores de función.

Los punteros de función también son útiles para las funciones de devolución de llamada. Como un ejemplo del mundo real, el otro día estaba escribiendo un controlador para un reloj en tiempo real en el chip. Solo había un reloj en el chip, pero necesitaba muchos temporizadores. Esto se solucionó guardando un contador para cada temporizador de software, que se incrementó mediante la interrupción del reloj en tiempo real. El tipo de datos se veía algo como esto:

typedef struct 
{ 
    uint16_t counter; 
    void (*callback)(void); 

} Timer_t; 

Cuando el temporizador de hardware fuera de igualdad con el temporizador de software, la función de devolución de llamada especificado por el usuario se llamaba, a través del puntero de función de almacenamiento junto con el contador. Algo similar a lo anterior es una construcción bastante común en los sistemas integrados.

Los punteros de función también son útiles al crear gestores de arranque, etc., donde escribirás código en NVM en tiempo de ejecución y luego lo invocarás. Puede hacerlo a través de un puntero de función, pero nunca a través de una función vinculada, ya que el código no está realmente allí en el momento del enlace.

Los indicadores de función son, por supuesto, como ya se mencionó, útiles para muchas optimizaciones, como la optimización de una declaración de interruptor donde cada "caso" es un número adyacente.

+0

Buena respuesta. Gracias de hecho –

2

Sí, son útiles. No estoy seguro de a qué estaba llegando el entrevistador. Básicamente es irrelevante si el sistema está incrustado o no. A menos que tengas una pila severamente limitada.

  • velocidad No, el sistema más rápido sería una sola función, y sólo utilizar las variables globales y de Goto dispersos por todo. Buena suerte con eso.
  • Legibilidad Sí, puede confundir a algunas personas, pero en general cierto código es más legible con punteros a funciones.También le permitirá aumentar la separación de las preocupaciones entre los diversos aspectos del código fuente.
  • Maintainable Sí, con los punteros de función tendrá menos condicionales, menos código duplicado, mayor separación de código y, en general, más software ortogonal.
7

Otra cosa a considerar es que esta pregunta sería una buena oportunidad para demostrar cómo se toman decisiones de diseño durante el proceso de desarrollo. Una respuesta que podría imaginar sería dar la vuelta y considerar cuáles son sus alternativas de implementación. Tomando una página de las respuestas de Casey y Lundin, he encontrado que las funciones de devolución de llamada son muy útiles para aislar mis módulos entre sí y facilitar los cambios de código porque mi código está en una etapa de creación de prototipos permanente y las cosas cambian rápidamente y con frecuencia. Cuáles son mis preocupaciones actuales es la facilidad de desarrollo, no tanta velocidad.

En mi caso, mi código generalmente implica tener múltiples módulos que necesitan señalizarse mutuamente para sincronizar el orden de las operaciones. Previamente había implementado esto como una gran cantidad de indicadores y estructuras de datos con enlaces externos. Con esta implementación, dos cuestiones en general, aspirados mi tiempo:

  1. Desde cualquier módulo puede tocar las variables extern mucho de mi tiempo se dedica a la vigilancia de cada módulo para asegurarse de que se están utilizando esas variables según lo previsto.
  2. Si otro desarrollador introdujo una nueva marca, me encontré buceando a través de múltiples módulos buscando la declaración original y (con suerte) una descripción de uso en los comentarios.

Con funciones de devolución de ese problema desaparece porque la función se convierte en el mecanismo de señalización y tomar ventaja de estos beneficios: interacciones

  1. módulo son impuestas por las interfaces de función y se puede probar para pre/post condiciones
  2. Menos necesidad de estructuras de datos compartidas globalmente, ya que la devolución de llamada sirve como interfaz para módulos externos.
  3. Acoplamiento reducido significa que puedo cambiar el código de forma relativamente fácil.

En este momento daré el golpe de rendimiento ya que mi dispositivo aún funciona adecuadamente incluso con todas las llamadas a funciones adicionales. Consideraré mis alternativas cuando ese desempeño empiece a ser un problema mayor.

Volviendo a la pregunta de la entrevista, aunque no seas tan técnicamente competente en los aspectos prácticos de los indicadores de función, creo que seguirías siendo un candidato valioso sabiendo que eres consciente de las compensaciones hechas durante el proceso de diseño.

1

Esa fue una pregunta capciosa. Hay industrias donde los indicadores están prohibidos.

Cuestiones relacionadas