¿Cuál es el mejor enfoque al escribir funciones para software embebido para obtener un mejor rendimiento? [cerrado]


13

He visto algunas de las bibliotecas para microcontroladores y sus funciones hacen una cosa a la vez. Por ejemplo, algo como esto:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

Luego, aparecen otras funciones que utilizan este código de 1 línea que contiene una función para otros fines. Por ejemplo:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

No estoy seguro, pero creo que de esta manera crearía más llamadas a saltos y crearía una sobrecarga de apilar las direcciones de retorno cada vez que se llama o sale de una función. Y eso haría que el programa funcione lento, ¿verdad?

He buscado y en todas partes dicen que la regla básica de la programación es que una función debe realizar solo una tarea.

Entonces, si escribo directamente un módulo de función InitModule que establece el reloj, agrega alguna configuración deseada y hace algo más sin llamar a las funciones. ¿Es un mal enfoque al escribir software incrustado?


EDITAR 2:

  1. Parece que mucha gente ha entendido esta pregunta como si estuviera tratando de optimizar un programa. No, no tengo intenciones de hacer . Estoy dejando que el compilador lo haga, porque siempre será (¡aunque espero que no!) Mejor que yo.

  2. Todos me culpan por elegir un ejemplo que represente algún código de inicialización . La pregunta no tiene intención de referirse a las llamadas a funciones realizadas para el propósito de inicialización. Mi pregunta es ¿Divide una tarea determinada en pequeñas funciones de varias líneas ( por lo que la línea está fuera de cuestión ) que se ejecuta dentro de un bucle infinito tiene alguna ventaja sobre la escritura de una función larga sin ninguna función anidada?

Considere la legibilidad definida en la respuesta de @Jonk .


28
Eres muy ingenuo (no como un insulto) si crees que cualquier compilador razonable convertirá ciegamente el código tal como está escrito en binarios tal como está escrito. La mayoría de los compiladores modernos son bastante buenos para identificar cuándo una rutina está mejor en línea e incluso cuando se debe usar una ubicación de registro vs RAM para contener una variable. Siga las dos reglas de optimización: 1) no optimice. 2) no optimices todavía . Haga que su código sea legible y mantenible, y ENTONCES solo después de perfilar un sistema de trabajo, busque optimizar.
akohlsmith

10
@akohlsmith IIRC las tres reglas de optimización son: 1) ¡No lo hagas! 2) No, realmente no lo hagas! 3) Perfil primero, luego y solo luego optimizar si es necesario - Michael_A._Jackson
esoterik

3
Solo recuerda que "la optimización prematura es la raíz de todo mal (o al menos la mayor parte) en la programación" - Knuth
Mawg dice que reinstalará a Monica el

1
@Mawg: La palabra operativa allí es prematura . (Como explica el siguiente párrafo en ese documento. Literalmente la siguiente oración: "Sin embargo, no deberíamos dejar pasar nuestras oportunidades en ese crítico 3%"). No optimice hasta que lo necesite, no habrá encontrado la lentitud bits hasta que tenga algo para perfilar, pero tampoco se involucre en pesimismo, por ejemplo, utilizando herramientas descaradamente incorrectas para el trabajo.
cHao

1
@Mawg No sé por qué recibí respuestas / comentarios relacionados con la optimización, ya que nunca mencioné la palabra y tengo la intención de hacerlo. La pregunta es mucho más sobre cómo escribir funciones en la programación integrada para lograr un mejor rendimiento.
MaNyYaCk

Respuestas:


28

Podría decirse que, en su ejemplo, el rendimiento no importaría, ya que el código solo se ejecuta una vez al inicio.

Una regla general que uso: escriba su código lo más legible posible y solo comience a optimizar si nota que su compilador no está haciendo su magia correctamente.

El costo de una llamada de función en un ISR podría ser el mismo que el de una llamada de función durante el inicio en términos de almacenamiento y sincronización. Sin embargo, los requisitos de tiempo durante ese ISR podrían ser mucho más críticos.

Además, como ya han notado otros, el costo (y el significado del 'costo') de una llamada de función difiere según la plataforma, el compilador, la configuración de optimización del compilador y los requisitos de la aplicación. Habrá una gran diferencia entre un 8051 y un cortex-m7, y un marcapasos y un interruptor de luz.


66
OMI, el segundo párrafo debe estar en negrita y en la parte superior. No hay nada de malo en elegir los algoritmos y estructuras de datos correctos desde el principio, pero preocuparse por la sobrecarga de las llamadas de función a menos que haya descubierto que es un cuello de botella real es definitivamente una optimización prematura, y debe evitarse.
Financia la demanda de Mónica

11

No se me ocurre ninguna ventaja (pero vea la nota a JasonS en la parte inferior), envolviendo una línea de código como una función o subrutina. Excepto tal vez que pueda nombrar la función algo "legible". Pero también puedes comentar la línea. Y dado que concluir una línea de código en una función cuesta memoria de código, espacio de pila y tiempo de ejecución, me parece que es principalmente contraproducente. En una situación de enseñanza? Puede tener sentido. Pero eso depende de la clase de estudiantes, su preparación previa, el plan de estudios y el maestro. Sobre todo, creo que no es una buena idea. Pero esa es mi opinión.

Lo que nos lleva a la conclusión. Su amplia área de preguntas ha sido, durante décadas, un tema de debate y sigue siendo hoy en día un tema de debate. Entonces, al menos mientras leo su pregunta, me parece una pregunta basada en la opinión (como usted la hizo).

Se podría alejar de ser tan basado en la opinión como es, si fuera más detallado sobre la situación y describiera cuidadosamente los objetivos que tenía como primarios. Cuanto mejor defina sus herramientas de medición, más objetivas serán las respuestas.


En términos generales, desea hacer lo siguiente para cualquier codificación. (Para más adelante, supondré que estamos comparando diferentes enfoques, todos los cuales logran los objetivos. Obviamente, cualquier código que no realice las tareas necesarias es peor que el código que tiene éxito, independientemente de cómo se escriba).

  1. Sea coherente con su enfoque, de modo que otra lectura de su código pueda desarrollar una comprensión de cómo aborda su proceso de codificación. Ser inconsistente es probablemente el peor crimen posible. No solo hace que sea difícil para los demás, sino que también te dificulta volver al código años después.
  2. En la medida de lo posible, intente y organice las cosas para que la inicialización de varias secciones funcionales se pueda realizar sin tener en cuenta los pedidos. Donde se requiere ordenar, si se debe a un acoplamiento cercano de dos subfunciones altamente relacionadas, considere una única inicialización para ambas para que pueda reordenarse sin causar daño. Si eso no es posible, documente el requisito de pedido de inicialización.
  3. Encapsula el conocimiento en exactamente un lugar, si es posible. Las constantes no deben duplicarse en todo el lugar del código. Las ecuaciones que resuelven algunas variables deberían existir en un solo lugar. Y así. Si se encuentra copiando y pegando un conjunto de líneas que realizan un comportamiento necesario en una variedad de ubicaciones, considere una forma de capturar ese conocimiento en un lugar y utilizarlo donde sea necesario. Por ejemplo, si tiene una estructura de árbol que se debe caminar de una manera específica, noRepita el código de caminar por el árbol en todos y cada uno de los lugares donde necesita recorrer los nodos del árbol. En cambio, capture el método de caminar por los árboles en un lugar y úselo. De esta manera, si el árbol cambia y el método de caminar cambia, solo tiene un lugar por el cual preocuparse y todo el resto del código "funciona bien".
  4. Si extiende todas sus rutinas en una hoja de papel enorme y plana, con flechas que las conectan como las llaman otras rutinas, verá en cualquier aplicación que habrá "grupos" de rutinas que tienen muchas flechas. entre ellos pero solo unas pocas flechas fuera del grupo. Por lo tanto, habrá límites naturales de rutinas estrechamente acopladas y conexiones poco acopladas entre otros grupos de rutinas estrechamente acopladas. Use este hecho para organizar su código en módulos. Esto limitará la complejidad aparente de su código, sustancialmente.

Lo anterior es generalmente cierto sobre toda la codificación. No discutí el uso de parámetros, variables globales locales o estáticas, etc. La razón es que para la programación incrustada, el espacio de la aplicación a menudo impone nuevas restricciones extremas y muy significativas y es imposible discutir todas ellas sin discutir cada aplicación incrustada. Y eso no está sucediendo aquí, de todos modos.

Estas restricciones pueden ser cualquiera (y más) de estas:

  • Limitaciones de costos severas que requieren MCU extremadamente primitivas con RAM minúscula y casi sin recuento de pines de E / S. Para estos, se aplican conjuntos de reglas completamente nuevos. Por ejemplo, puede que tenga que escribir en el código de ensamblado porque no hay mucho espacio de código. Puede que tenga que usar SOLAMENTE variables estáticas porque el uso de variables locales es demasiado costoso y requiere mucho tiempo. Es posible que deba evitar el uso excesivo de subrutinas porque (por ejemplo, algunas partes de Microchip PIC) solo hay 4 registros de hardware en los que almacenar direcciones de retorno de subrutinas. Por lo tanto, es posible que deba "aplanar" su código dramáticamente. Etc.
  • Limitaciones de potencia severas que requieren código cuidadosamente diseñado para iniciar y apagar la mayoría de la MCU y colocar limitaciones severas en el tiempo de ejecución del código cuando se ejecuta a toda velocidad. Nuevamente, esto puede requerir un poco de codificación de ensamblaje, a veces.
  • Requisitos de tiempo severos. Por ejemplo, hay momentos en los que he tenido que asegurarme de que la transmisión de un drenaje abierto 0 tuviera que tomar EXACTAMENTE el mismo número de ciclos que la transmisión de un 1. Y que el muestreo de esta misma línea también tuviera que realizarse con una fase relativa exacta a este momento. Esto significaba que C NO podía usarse aquí. La ÚNICA manera posible de hacer esa garantía es elaborar cuidadosamente el código de ensamblaje. (E incluso entonces, no siempre en todos los diseños de ALU).

Y así. (El código de cableado para instrumentación médica vital también tiene un mundo entero propio).

El resultado aquí es que la codificación incrustada a menudo no es algo gratuito, donde puede codificar como lo haría en una estación de trabajo. A menudo hay razones severas y competitivas para una amplia variedad de restricciones muy difíciles. Y estos pueden argumentar fuertemente en contra de las respuestas más tradicionales y comunes .


Con respecto a la legibilidad, encuentro que el código es legible si está escrito de una manera consistente que pueda aprender mientras lo leo. Y donde no hay un intento deliberado de ofuscar el código. Realmente no hay mucho más requerido.

El código legible puede ser bastante eficiente y puede cumplir con todos los requisitos anteriores que ya he mencionado. Lo principal es que comprenda completamente lo que produce cada línea de código que escribe a nivel de ensamblaje o máquina, a medida que lo codifica. C ++ pone una carga seria para el programador aquí porque hay muchas situaciones en las que fragmentos idénticos de código C ++ realmente generan fragmentos diferentes de código de máquina que tienen rendimientos muy diferentes. Pero C, en general, es principalmente un lenguaje de "lo que ves es lo que obtienes". Entonces es más seguro en ese sentido.


EDITAR por JasonS:

He estado usando C desde 1978 y C ++ desde aproximadamente 1987 y he tenido mucha experiencia usando ambos para mainframes, minicomputadoras y (principalmente) aplicaciones integradas.

Jason saca un comentario sobre el uso de 'en línea' como modificador. (En mi perspectiva, esta es una capacidad relativamente "nueva" porque simplemente no existió durante quizás la mitad de mi vida o más usando C y C ++.) El uso de funciones en línea puede hacer tales llamadas (incluso para una línea de código) bastante práctico. Y es mucho mejor, siempre que sea posible, que usar una macro debido a la tipificación que el compilador puede aplicar.

Pero también hay limitaciones. La primera es que no puede confiar en el compilador para "tomar la pista". Puede o no puede. Y hay buenas razones para no entender la indirecta. (Para un ejemplo obvio, si se toma la dirección de la función, esto requiere la creación de instancias de la función y el uso de la dirección para hacer la llamada ... requerirá una llamada. El código no se puede insertar en línea entonces). otras razones también. Los compiladores pueden tener una amplia variedad de criterios por los cuales juzgan cómo manejar la pista. Y como programador, esto significa que debesDedique un tiempo a aprender sobre ese aspecto del compilador o, de lo contrario, es probable que tome decisiones basadas en ideas erróneas. Por lo tanto, agrega una carga tanto al escritor del código como a cualquier lector y también a cualquiera que planee portar el código a otro compilador también.

Además, los compiladores C y C ++ admiten compilación separada. Esto significa que pueden compilar una pieza de código C o C ++ sin compilar ningún otro código relacionado para el proyecto. Para insertar código en línea, suponiendo que el compilador de otra manera pudiera elegir hacerlo, no solo debe tener la declaración "en alcance" sino que también debe tener la definición. Por lo general, los programadores trabajarán para asegurarse de que este sea el caso si están usando 'en línea'. Pero es fácil que los errores se cuelen.

En general, aunque también uso inline donde creo que es apropiado, tiendo a suponer que no puedo confiar en él. Si el rendimiento es un requisito importante, y creo que el OP ya ha escrito claramente que ha habido un impacto significativo en el rendimiento cuando se dirigieron a una ruta más "funcional", entonces ciertamente elegiría evitar confiar en línea como práctica de codificación y en su lugar, seguiría un patrón de escritura de código ligeramente diferente, pero completamente coherente.

Una nota final sobre 'en línea' y las definiciones están "dentro del alcance" para un paso de compilación separado. Es posible (no siempre confiable) que el trabajo se realice en la etapa de vinculación. Esto puede ocurrir si y solo si un compilador de C / C ++ oculta suficientes detalles en los archivos de objeto para permitir que un vinculador actúe sobre solicitudes 'en línea'. Personalmente, no he experimentado un sistema de enlace (fuera de Microsoft) que admita esta capacidad. Pero puede ocurrir. Una vez más, si se debe confiar o no dependerá de las circunstancias. Pero generalmente asumo que esto no se ha incluido en el enlazador, a menos que sepa lo contrario en base a buena evidencia. Y si confío en ello, se documentará en un lugar destacado.


C ++

Para aquellos interesados, aquí hay un ejemplo de por qué sigo siendo bastante cauteloso con C ++ al codificar aplicaciones integradas, a pesar de su disponibilidad actual. Lanzaré algunos términos que creo que todos los programadores de C ++ integrados deben saber en frío :

  • especialización parcial de plantilla
  • vtables
  • objeto base virtual
  • marco de activación
  • marco de activación desenrollar
  • uso de punteros inteligentes en constructores, y por qué
  • optimización del valor de retorno

Eso es solo una lista corta. Si aún no sabe todo acerca de esos términos y por qué los enumeré (y muchos más que no enumeré aquí), le aconsejaría que no use C ++ para el trabajo incorporado, a menos que no sea una opción para el proyecto .

Echemos un vistazo rápido a la semántica de excepciones de C ++ para obtener solo un sabor.

UNsi separada, compilada por separado y en un momento diferente.

UN

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

UN

si

El compilador de C ++ ve la primera llamada a foo () y solo puede permitir que se produzca un desenlace de activación normal, si foo () arroja una excepción. En otras palabras, el compilador de C ++ sabe que no se necesita ningún código adicional en este punto para admitir el proceso de desenrollado de trama involucrado en el manejo de excepciones.

Pero una vez que se ha creado String s, el compilador de C ++ sabe que debe destruirse correctamente antes de que se pueda permitir un desenrollado de trama, si se produce una excepción más adelante. Entonces, la segunda llamada a foo () es semánticamente diferente de la primera. Si la segunda llamada a foo () arroja una excepción (que puede o no puede hacer), el compilador debe haber colocado un código diseñado para manejar la destrucción de String s antes de permitir que se produzca el desenrollado habitual del marco. Esto es diferente al código requerido para la primera llamada a foo ().

(Es posible agregar decoraciones adicionales en C ++ para ayudar a limitar este problema. Pero el hecho es que los programadores que usan C ++ simplemente deben ser mucho más conscientes de las implicaciones de cada línea de código que escriben).

A diferencia de Malloc de C, el nuevo C ++ usa excepciones para señalar cuando no puede realizar la asignación de memoria sin procesar. También lo hará 'dynamic_cast'. (Consulte la tercera edición de Stroustrup, The C ++ Programming Language, páginas 384 y 385 para ver las excepciones estándar en C ++.) Los compiladores pueden permitir que este comportamiento se deshabilite. Pero, en general, incurrirá en una sobrecarga debido a los prólogos y epílogos de manejo de excepciones correctamente formados en el código generado, incluso cuando las excepciones realmente no tienen lugar e incluso cuando la función que se está compilando en realidad no tiene ningún bloque de manejo de excepciones. (Stroustrup lo ha lamentado públicamente).

Sin una especialización parcial de la plantilla (no todos los compiladores de C ++ lo admiten), el uso de plantillas puede significar un desastre para la programación integrada. Sin él, la floración de código es un riesgo grave que podría matar un proyecto incrustado de memoria pequeña en un instante.

Cuando una función C ++ devuelve un objeto, se crea y destruye un compilador temporal sin nombre. Algunos compiladores de C ++ pueden proporcionar un código eficiente si se usa un constructor de objetos en la declaración de retorno, en lugar de un objeto local, reduciendo las necesidades de construcción y destrucción en un objeto. Pero no todos los compiladores hacen esto y muchos programadores de C ++ ni siquiera son conscientes de esta "optimización del valor de retorno".

Proporcionar un constructor de objetos con un tipo de parámetro único puede permitir que el compilador de C ++ encuentre una ruta de conversión entre dos tipos de maneras completamente inesperadas para el programador. Este tipo de comportamiento "inteligente" no es parte de C.

Una cláusula catch que especifique un tipo base "dividirá" un objeto derivado lanzado, porque el objeto lanzado se copia utilizando el "tipo estático" de la cláusula catch y no el "tipo dinámico" del objeto. Una fuente no común de miseria de excepción (cuando siente que incluso puede permitirse excepciones en su código incrustado).

Los compiladores de C ++ pueden generar automáticamente constructores, destructores, constructores de copias y operadores de asignación para usted, con resultados no deseados. Lleva tiempo ganar facilidad con los detalles de esto.

Pasar matrices de objetos derivados a una función que acepta matrices de objetos base, rara vez genera advertencias del compilador, pero casi siempre produce un comportamiento incorrecto.

Dado que C ++ no invoca el destructor de objetos parcialmente construidos cuando se produce una excepción en el constructor de objetos, el manejo de excepciones en los constructores generalmente exige "punteros inteligentes" para garantizar que los fragmentos construidos en el constructor se destruyan adecuadamente si se produce una excepción allí . (Consulte Stroustrup, página 367 y 368.) Este es un problema común al escribir buenas clases en C ++, pero, por supuesto, se evita en C ya que C no tiene la semántica de construcción y destrucción incorporada. Escribir el código adecuado para manejar la construcción de subobjetos dentro de un objeto significa escribir código que debe hacer frente a este problema semántico único en C ++; en otras palabras, "escribir alrededor" de los comportamientos semánticos de C ++.

C ++ puede copiar objetos pasados ​​a parámetros de objeto. Por ejemplo, en los siguientes fragmentos, la llamada "rA (x);" puede hacer que el compilador de C ++ invoque un constructor para el parámetro p, para luego llamar al constructor de copia para transferir el objeto x al parámetro p, y luego otro constructor para el objeto de retorno (un temporario sin nombre) de la función rA, que por supuesto es copiado del parámetro p. Peor aún, si la clase A tiene sus propios objetos que necesitan construcción, esto puede telescopizar desastrosamente. (El programador de AC evitaría la mayor parte de esta basura, optimizando a mano ya que los programadores de C no tienen una sintaxis tan práctica y tienen que expresar todos los detalles uno por uno).

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

Finalmente, una breve nota para los programadores de C. longjmp () no tiene un comportamiento portátil en C ++. (Algunos programadores de C usan esto como una especie de mecanismo de "excepción".) Algunos compiladores de C ++ en realidad intentarán configurar las cosas para limpiar cuando se toma el longjmp, pero ese comportamiento no es portátil en C ++. Si el compilador limpia los objetos construidos, no es portátil. Si el compilador no los limpia, entonces los objetos no se destruyen si el código abandona el alcance de los objetos construidos como resultado de longjmp y el comportamiento no es válido. (Si el uso de longjmp en foo () no deja un alcance, entonces el comportamiento puede estar bien). Esto no es usado con demasiada frecuencia por los programadores incrustados en C, pero deben ser conscientes de estos problemas antes de usarlos.


44
Este tipo de funciones que se usan solo una vez nunca se compilan como llamadas de función, el código simplemente se coloca allí sin ninguna llamada.
Dorian

66
@Dorian: su comentario puede ser cierto en ciertas circunstancias para ciertos compiladores. Si la función es estática dentro del archivo, el compilador tiene la opción de hacer que el código esté en línea. si es visible externamente, incluso si nunca se llama realmente, tiene que haber una manera de que la función sea invocable.
uɐɪ

1
@jonk: otro truco que no ha mencionado en una buena respuesta es escribir funciones macro simples que realicen la inicialización o configuración como código en línea expandido. Esto es especialmente útil en los procesadores muy pequeños donde la profundidad de la llamada RAM / stack / function es limitada.
uɐɪ

@ ʎəʞouɐɪ Sí, eché de menos hablar de macros en C. Esas son obsoletas en C ++, pero una discusión sobre ese punto podría ser útil. Puedo abordarlo, si puedo imaginar algo útil para escribir sobre él.
jonk

1
@jonk: estoy completamente en desacuerdo con tu primera oración. Un ejemplo como el inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }que se llama en numerosos lugares es un candidato perfecto.
Jason S

8

1) Código de legibilidad y mantenibilidad primero. El aspecto más importante de cualquier código base es que está bien estructurado. El software bien escrito tiende a tener menos errores. Es posible que deba realizar cambios en un par de semanas / meses / años, y es de gran ayuda si su código es agradable de leer. O tal vez alguien más tiene que hacer un cambio.

2) El rendimiento del código que se ejecuta una vez no importa mucho. Cuida el estilo, no el rendimiento

3) Incluso el código en bucles estrechos debe ser correcto en primer lugar. Si enfrenta problemas de rendimiento, optimice una vez que el código sea correcto.

4) Si necesita optimizar, ¡tiene que medir! No importa si piensas o alguien te dice que static inlinees solo una recomendación para el compilador. Tienes que echar un vistazo a lo que hace el compilador. También debe medir si la alineación mejoró el rendimiento. En los sistemas integrados, también debe medir el tamaño del código, ya que la memoria de código suele ser bastante limitada. Esta es LA regla más importante que distingue la ingeniería de las conjeturas. Si no lo midió, no ayudó. La ingeniería es medir. La ciencia lo está escribiendo;)


2
La única crítica que tengo de su excelente publicación es el punto 2). Es cierto que el rendimiento del código de inicialización es irrelevante, pero en un entorno integrado, el tamaño puede importar. (Pero eso no anula el punto 1; comience a optimizar el tamaño cuando lo necesite, y no antes)
Martin Bonner apoya a Monica el

2
El rendimiento del código de inicialización podría ser irrelevante al principio. Cuando agrega el modo de bajo consumo y desea recuperarse rápidamente para manejar el evento de activación, entonces se vuelve relevante.
berendi - protestando el

5

Cuando se llama a una función solo en un lugar (incluso dentro de otra función), el compilador siempre coloca el código en ese lugar en lugar de llamar realmente a la función. Si la función se llama en muchos lugares, entonces tiene sentido usar una función al menos desde el punto de vista del tamaño del código.

Después de compilar el código no tendrá las llamadas múltiples, en cambio, la legibilidad mejorará enormemente.

También querrá tener, por ejemplo, el código de inicio de ADC en la misma biblioteca con otras funciones de ADC que no están en el archivo c principal.

Muchos compiladores le permiten especificar diferentes niveles de optimización para la velocidad o el tamaño del código, por lo que si tiene una función pequeña llamada en muchos lugares, la función se "alineará", se copiará allí en lugar de llamar.

La optimización de la velocidad alineará las funciones en todos los lugares que pueda, la optimización del tamaño del código llamará a la función, sin embargo, cuando se llama a una función solo en un lugar, en su caso siempre estará "alineada".

Código como este:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

se compilará para:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

sin usar ninguna llamada.

Y la respuesta a su pregunta, en su ejemplo o similar, la legibilidad del código no afecta el rendimiento, no hay mucha velocidad o tamaño del código. Es común usar varias llamadas solo para que el código sea legible, al final se cumplen como un código en línea.

Actualice para especificar que las declaraciones anteriores no son válidas para compiladores de versión gratuita paralizados a propósito, como la versión gratuita Microchip XCxx. Este tipo de llamadas a funciones es una mina de oro para Microchip para mostrar cuánto mejor es la versión paga y si compila esto encontrará en el ASM exactamente las llamadas que tiene en el código C.

Tampoco es para programadores tontos que esperan usar el puntero a una función en línea.

Esta es la sección de electrónica, no la sección general de C C ++ o de programación, la pregunta es sobre la programación de microcontroladores donde cualquier compilador decente hará la optimización anterior por defecto.

Por lo tanto, deje de votar solo porque, en casos raros e inusuales, esto podría no ser cierto.


15
Si el código se vuelve en línea o no es un problema específico de implementación del proveedor del compilador; incluso el uso de la palabra clave en línea no garantiza el código en línea. Es una pista para el compilador. Ciertamente, los buenos compiladores incluirán funciones en línea utilizadas solo una vez si las conocen. Sin embargo, generalmente no lo hará si hay objetos "volátiles" en el alcance.
Peter Smith

99
Esta respuesta simplemente no es verdad. Como dice @PeterSmith, y de acuerdo con la especificación del lenguaje C, el compilador tiene la opción de insertar el código en línea, pero puede que no lo haga, y en muchos casos no lo hará. Hay tantos compiladores diferentes en el mundo para tantos procesadores de destino diferentes que hacer el tipo de declaración general en esta respuesta y suponer que todos los compiladores colocarán el código en línea cuando solo tienen la opción no es sostenible.
uɐɪ

2
@ ʎəʞouɐɪ Está señalando casos raros en los que no es posible y sería una mala idea no llamar a una función en primer lugar. Nunca he visto un compilador tan tonto como para usar realmente call en el ejemplo simple dado por el OP.
Dorian

66
En los casos en que estas funciones se invocan solo una vez, la optimización de la función de llamada no es un problema. ¿Realmente necesita el sistema recuperar cada ciclo de reloj durante la configuración? Como es el caso con la optimización en cualquier lugar: escriba un código legible y optimice solo si el perfil muestra que es necesario .
Baldrickk

55
@MSalters No me preocupa lo que el compilador termina haciendo aquí, más sobre cómo el programador lo aborda. No hay un impacto de rendimiento insignificante o insignificante al romper la inicialización como se ve en la pregunta.
Baldrickk

2

En primer lugar, no hay mejor o peor; Todo es cuestión de opinión. Tienes mucha razón en que esto es ineficiente. Se puede optimizar o no; depende. Por lo general, verá este tipo de funciones, reloj, GPIO, temporizador, etc. en archivos / directorios separados. Los compiladores generalmente no han sido capaces de optimizar estas brechas. Hay uno que puede conocer, pero que no se usa ampliamente para cosas como esta.

Archivo único:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Elegir un objetivo y un compilador con fines de demostración.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

Esto es lo que la mayoría de las respuestas aquí le están diciendo, que es ingenuo y que todo esto se optimiza y se eliminan las funciones. Bueno, no se eliminan, ya que están definidos globalmente de forma predeterminada. Podemos eliminarlos si no es necesario fuera de este archivo.

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

los elimina ahora ya que están en línea.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

Pero la realidad es cuando adquieres proveedores de chips o bibliotecas BSP,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

Definitivamente comenzará a agregar gastos generales, lo que tiene un costo notable para el rendimiento y el espacio. De unos pocos a cinco por ciento de cada uno, dependiendo de cuán pequeña sea cada función.

¿Por qué se hace esto de todos modos? Parte de esto es el conjunto de reglas que los profesores enseñarían o aún enseñarían para facilitar el código de calificación. Las funciones deben caber en una página (cuando imprimiste tu trabajo en papel), no hagas esto, no hagas eso, etc. Mucho de esto es hacer bibliotecas con nombres comunes para diferentes objetivos. Si tiene decenas de familias de microcontroladores, algunos de los cuales comparten periféricos y otros no, tal vez tres o cuatro diferentes sabores UART mezclados entre las familias, diferentes GPIO, controladores SPI, etc. Puede tener una función genérica gpio_init (), get_timer_count (), etc. Y reutilice esas abstracciones para los diferentes periféricos.

Se convierte principalmente en un caso de mantenimiento y diseño de software, con cierta legibilidad posible. Mantenibilidad, legibilidad y rendimiento que no puede tener todo; solo puedes elegir uno o dos a la vez, no los tres.

Esta es una pregunta basada en la opinión, y lo anterior muestra las tres formas principales en que esto puede ir. En cuanto a qué camino es MEJOR, eso es estrictamente opinión. ¿Está haciendo todo el trabajo en una función? Una pregunta basada en la opinión, algunas personas se inclinan por el rendimiento, algunas definen la modularidad y su versión de legibilidad como la MEJOR. El tema interesante con lo que mucha gente llama legibilidad es extremadamente doloroso; para "ver" el código, debe tener 50-10,000 archivos abiertos a la vez y de alguna manera tratar de ver linealmente las funciones en orden de ejecución para ver lo que está sucediendo. Creo que es lo opuesto a la legibilidad, pero a otros les resulta legible ya que cada elemento cabe en la ventana de la pantalla / editor y puede consumirse en su totalidad después de haber memorizado las funciones que se están llamando y / o tener un editor que puede entrar y salir de cada función dentro de un proyecto.

Ese es otro factor importante cuando ves varias soluciones. Los editores de texto, IDEs, etc. son muy personales y van más allá de vi vs Emacs. Eficiencia de programación, las líneas por día / mes aumentan si se siente cómodo y eficiente con la herramienta que está utilizando. Las características de la herramienta pueden / se inclinarán intencionalmente o no hacia la forma en que los fanáticos de esa herramienta escriben el código. Y como resultado, si un individuo está escribiendo estas bibliotecas, el proyecto en cierta medida refleja estos hábitos. Incluso si se trata de un equipo, el desarrollador principal o los hábitos / preferencias del jefe pueden verse obligados al resto del equipo.

Estándares de codificación que tienen muchas preferencias personales enterradas, vi muy religioso contra Emacs nuevamente, pestañas versus espacios, cómo se alinean los corchetes, etc. Y estos juegan en cómo las bibliotecas están diseñadas hasta cierto punto.

¿Cómo deberías escribir el tuyo? Como quieras, realmente no hay una respuesta incorrecta si funciona. Hay un código malo o arriesgado, pero si está escrito de tal manera que pueda mantenerlo según sea necesario, cumple con sus objetivos de diseño, renuncia a la legibilidad y algo de mantenimiento si el rendimiento es importante, o viceversa. ¿Te gustan los nombres cortos de variables para que una sola línea de código se ajuste al ancho de la ventana del editor? O nombres demasiado largos y descriptivos para evitar confusiones, pero la legibilidad disminuye porque no se puede obtener una línea en una página; ahora está visualmente dividido, jugando con el flujo.

No vas a pegar un jonrón la primera vez al bate. Puede / debería tomar décadas definir realmente su estilo. Al mismo tiempo, durante ese tiempo, su estilo puede cambiar, inclinarse hacia un lado por un tiempo y luego inclinarse hacia otro.

Vas a escuchar mucho de no optimizar, nunca optimizar y optimización prematura. Pero como se muestra, los diseños como este desde el principio crean problemas de rendimiento, luego comienzas a ver hacks para resolver ese problema en lugar de rediseñar desde el principio para realizar. Estoy de acuerdo en que hay situaciones, una sola función, unas pocas líneas de código que puede tratar de manipular con dificultad el compilador basándose en el temor de lo que el compilador hará de otra manera (tenga en cuenta que con experiencia este tipo de codificación se vuelve fácil y natural, optimizando a medida que escribe sabiendo cómo compilará el código el compilador), entonces desea confirmar dónde está realmente el ladrón de ciclos, antes de atacarlo.

También debe diseñar su código para el usuario hasta cierto punto. Si este es su proyecto, usted es el único desarrollador; es lo que quieras Si está tratando de hacer una biblioteca para regalar o vender, probablemente quiera hacer que su código se vea como todas las demás bibliotecas, cientos de miles de archivos con funciones pequeñas, nombres largos de funciones y nombres largos de variables. A pesar de los problemas de legibilidad y problemas de rendimiento, IMO encontrará que más personas podrán usar ese código.


44
De Verdad? ¿Qué "algún objetivo" y "algún compilador" utiliza?
Dorian

Me parece más un ARM8 de 32/64 bits, tal vez de un raspbery PI y luego un microcontrolador habitual. ¿Has leído la primera oración de la pregunta?
Dorian

Bueno, el compilador no elimina las funciones globales no utilizadas, pero el vinculador sí. Si está configurado y se usa correctamente, no se mostrarán en el ejecutable.
berendi - protestando el

Si alguien se pregunta qué compilador puede optimizar a través de las brechas de archivos: los compiladores IAR admiten la compilación de múltiples archivos (así es como lo llaman), lo que permite la optimización de archivos cruzados. Si arrojas todos los archivos c / cpp de una vez, terminas con un ejecutable que contiene una sola función: main. Los beneficios de rendimiento pueden ser bastante profundos.
Arsenal

3
@Arsenal Por supuesto, gcc admite la alineación, incluso en unidades de compilación si se llama correctamente. Ver gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html y busque la opción -flto.
Peter - Restablece a Mónica

1

Regla muy general: el compilador puede optimizar mejor que usted. Por supuesto, hay excepciones si está haciendo cosas muy intensivas en bucle, pero en general si desea una buena optimización para la velocidad o el tamaño del código, elija sabiamente su compilador.


Lamentablemente, es cierto para la mayoría de los programadores de hoy.
Dorian

0

De seguro depende de su propio estilo de codificación. Una regla general que existe es que los nombres de las variables, así como los nombres de las funciones, deben ser lo más claros y explicativos posibles. Cuantas más sub-llamadas o líneas de código pongas en una función, más difícil será definir una tarea clara para esa función. En su ejemplo, tiene una función initModule()que inicializa cosas y llama a subrutinas que luego configuran el reloj o configuran la configuración . Puedes decir eso simplemente leyendo el nombre de la función. Si coloca todo el código de las subrutinas en suinitModule() directamente, se vuelve menos obvio lo que realmente hace la función. Pero como a menudo, es solo una guía.


Gracias por su respuesta. Podría cambiar el estilo si fuera necesario para el rendimiento, pero la pregunta aquí es si la legibilidad del código afecta el rendimiento.
MaNyYaCk

Una llamada a la función dará como resultado una llamada o un comando jmp, pero eso es un sacrificio insignificante de recursos en mi opinión. Si usa patrones de diseño, a veces termina con una docena de capas de llamadas a funciones antes de llegar al código real cortado.
po.pe

@Humpawumpa: si está escribiendo para un microcontrolador con solo 256 o 64 bytes de RAM, una docena de capas de llamadas a funciones no es un sacrificio insignificante, simplemente no es posible
uɐɪ

Sí, pero estos son dos extremos ... por lo general, tiene más de 256 bytes y está utilizando menos de una docena de capas, con suerte.
po.pe

0

Si una función realmente solo hace una cosa muy pequeña, considere hacerla static inline.

Agréguelo a un archivo de encabezado en lugar del archivo C y use las palabras static inlinepara definirlo:

static inline void setCLK()
{
    //code to set the clock
}

Ahora, si la función es incluso un poco más larga, como tener más de 3 líneas, podría ser una buena idea evitar static inline y agregarla al archivo .c. Después de todo, los sistemas integrados tienen memoria limitada y no desea aumentar demasiado el tamaño del código.

Además, si define la función file1.cy la usa file2.c, el compilador no la alineará automáticamente. Sin embargo, si lo define file1.hcomostatic inline función, es probable que su compilador lo incorpore.

Estas static inlinefunciones son extremadamente útiles en la programación de alto rendimiento. He descubierto que a menudo aumentan el rendimiento del código en un factor de más de tres.


"como tener más de 3 líneas": el recuento de líneas no tiene nada que ver con eso; el costo de alineación tiene mucho que ver con eso. Podría escribir una función de 20 líneas que sea perfecta para alinear, y una función de 3 líneas que sea horrible para alinear (por ejemplo, functionA () que llama a functionB () 3 veces, functionB () que llama a functionC () 3 veces, y Un par de otros niveles).
Jason S

Además, si define la función file1.cy la usa file2.c, el compilador no la alineará automáticamente. Falso . Ver, por ejemplo, -fltoen gcc o clang.
berendi - protestando el

0

Una dificultad al intentar escribir código eficiente y confiable para microcontroladores es que algunos compiladores no pueden manejar ciertas semánticas de manera confiable a menos que el código use directivas específicas del compilador o inhabilite muchas optimizaciones.

Por ejemplo, si tiene un sistema de un solo núcleo con una rutina de servicio de interrupción [ejecutada por un tic del temporizador o lo que sea]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

Debería ser posible escribir funciones para iniciar una operación de escritura en segundo plano o esperar a que se complete:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

y luego invoque dicho código usando:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

Desafortunadamente, con las optimizaciones completas habilitadas, un compilador "inteligente" como gcc o clang decidirá que no hay forma de que el primer conjunto de escrituras pueda tener algún efecto en la observabilidad del programa y, por lo tanto, pueden optimizarse. Compiladores de calidad comoicc son menos propensos a hacer esto si el acto de establecer una interrupción y esperar la finalización involucra tanto escrituras volátiles como lecturas volátiles (como es el caso aquí), pero la plataforma dirigida poricc no es tan popular para los sistemas integrados.

El Estándar ignora deliberadamente los problemas de calidad de implementación, y considera que hay varias formas razonables de manejar la construcción anterior:

  1. A implementaciones de calidad destinadas exclusivamente a campos como el procesamiento de números de alta gama podría razonablemente esperar que el código escrito para tales campos no contenga construcciones como las anteriores.

  2. Una implementación de calidad puede tratar todos los accesos a los volatileobjetos como si pudieran desencadenar acciones que accederían a cualquier objeto que sea visible para el mundo exterior.

  3. Una implementación simple pero de calidad decente destinada al uso de sistemas integrados podría tratar todas las llamadas a funciones no marcadas como "en línea" como si pudieran acceder a cualquier objeto que haya estado expuesto al mundo exterior, incluso si no se trata volatilecomo se describe en # 2)

La Norma no intenta sugerir cuál de los enfoques anteriores sería el más apropiado para una implementación de calidad, ni para exigir que las implementaciones "conformes" sean de una calidad suficientemente buena como para ser utilizables para un propósito particular. En consecuencia, algunos compiladores como gcc o clang efectivamente requieren que cualquier código que quiera usar este patrón se compile con muchas optimizaciones deshabilitadas.

En algunos casos, asegurarse de que las funciones de E / S están en una unidad de compilación separada y un compilador no tendrá más remedio que asumir que podrían acceder a cualquier subconjunto arbitrario de objetos que hayan estado expuestos al mundo exterior puede ser un mínimo razonable. forma de escribir código que funcionará de manera confiable con gcc y clang. En tales casos, sin embargo, el objetivo no es evitar el costo adicional de una llamada de función innecesaria, sino aceptar el costo que debería ser innecesario a cambio de obtener la semántica requerida.


"asegurarse de que las funciones de E / S estén en una unidad de compilación separada" ... no es una forma segura de prevenir problemas de optimización como estos. Al menos LLVM y creo que GCC realizará la optimización de todo el programa en muchos casos, por lo que podría decidir incorporar sus funciones de E / S incluso si están en una unidad de compilación separada.
Julio

@Jules: no todas las implementaciones son adecuadas para escribir software embebido. Deshabilitar la optimización de todo el programa puede ser la forma menos costosa de forzar a gcc o clang a comportarse como una implementación de calidad adecuada para ese propósito.
supercat

@Jules: una implementación de mayor calidad destinada a la programación integrada o de sistemas debe ser configurable para tener una semántica adecuada para ese propósito sin tener que deshabilitar por completo la optimización de todo el programa (por ejemplo, al tener una opción para tratar los volatileaccesos como si pudieran desencadenar potencialmente accesos arbitrarios a otros objetos), pero por cualquier razón gcc y clang preferirían tratar los problemas de calidad de implementación como una invitación a comportarse de manera inútil.
supercat

1
Incluso las implementaciones de "más alta calidad" no corregirán el código defectuoso. Si buffno se declara volatile, no se tratará como una variable volátil, los accesos pueden ser reordenados u optimizados por completo si aparentemente no se usan más tarde. La regla es simple: marque todas las variables a las que se pueda acceder fuera del flujo normal del programa (como lo ve el compilador) como volatile. ¿Se buffaccede al contenido de un controlador de interrupciones? Si. Entonces debería ser volatile.
berendi - protestando el

@berendi: los compiladores pueden ofrecer garantías más allá de lo que exige el estándar y los compiladores de calidad lo harán. Una implementación independiente de calidad para el uso de sistemas integrados permitirá a los programadores sintetizar construcciones mutex, que es esencialmente lo que hace el código. Cuando magic_write_countes cero, el almacenamiento es propiedad de la línea principal. Cuando no es cero, es propiedad del controlador de interrupciones. Haciendo buffvolátil requeriría que todas las funciones en cualquier lugar que opera sobre ella utiliza volatilepunteros Calificado, que puedan menoscabar la optimización mucho más que tener un compilador ...
supercat
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.