Principalmente uso dispositivos Cortex-M4F o Cortex-M0/0+ como:
A veces veo blogs como este que dicen "Evitar módulo" para reducir el consumo de energía.
Comparando estos dos programas, ¿es esta una forma adecuada de evitar la operación de módulo?
Suponga que el voltaje de suministro de la MCU podría ser de 1,8 V o 3,3 V.
while(1) { // CODE X, I thought removing conditional statements could perform better
my_index++;
if (my_index >= 8) my_index = 0;
}
contra
while(1) { // CODE Y
my_index = (my_index + 1) % 8;
}
Además, ¿por qué la operación de módulo consume más?
Ese conjunto de reglas tiene sentido, más o menos. Pero son limitados.
Específicamente, la regla general "no usar módulo" es algo equivocada y realmente debería significar "evitar el uso de código que resulte en una operación de división".
(Mi conjunto de reglas sería comprender cómo funciona el hardware, compilar para ensamblar e inspeccionar el resultado, perfilar su código y comparar, comparar, comparar).
Si tenía una línea de código que decía a = b % c;
entonces (suponiendo que a, b y c son números enteros), le está especificando al compilador que c
podría ser cualquier valor entero . Tendría que compilar en una operación de división. Las operaciones de división toman mucho tiempo o área lógica; en cualquier caso, eso se traduce en energía consumida para realizar una división.
En su caso específico , donde dice my_index = (my_index + 1) % 8;
que el compilador, incluso con las optimizaciones establecidas en su nivel más bajo, probablemente lo convertirá en el lenguaje de máquina equivalente a my_index = (my_index + 1) & 0x0007;
. En otras palabras, no se dividirá (muy costoso), ni siquiera se bifurcará (menos costoso), pero se enmascarará (menos costoso en la mayoría de los procesadores actuales ). Pero esto solo sucederá si el módulo es una potencia de dos.
Puede asegurarse de que simplemente use my_index = (my_index + 1) & 0x0007;
, a costa de la comprensión del código y, por lo tanto, de la capacidad de mantenimiento del código. Coméntalo bien si vas por ese camino.
Entonces, en su caso específico , siempre que eso % 8
no cambie, o solo cambie a % N
donde N
siempre está
y el compilador sabe que la velocidad no cambiará. Pero si usted o alguien más aparece más tarde y lo cambia a my_index = (my_index + 1) % 17;
(o cualquier otro que no sea potencia de 2), de repente su código tendrá una operación de división y consumirá más energía. En ese caso, usar la declaración condicional será menos costoso.
(En C/C++, se asegura de que el compilador conozca el valor de una constante con anticipación mediante el uso de una # define
instrucción o (dependiendo del optimizador) declarándola const unsigned int
o (más fuerte, si es C++) declarándola constexpr int
. Otros lenguajes compilados ( es decir, Rust) tienen sus propias formas de hacer que esto suceda).
Nota: no me sorprendería que un buen compilador de optimización no convirtiera la construcción 'si' en una máscara, pero no me sorprendería si no lo hiciera. Lo mismo, un compilador de optimización realmente bueno podría ver my_index = (my_index + 1) % 17;
e inferir la construcción condicional. No creo que contaría con eso sin mirar el ensamblado, y no creo que confiaría en él al 100%; podría usarlo , pero pondría un comentario en el código sobre cruzar mi dedos y esperando que el compilador funcione bien.
A menos que esté absolutamente contra la pared por el consumo de energía, también debe pensar en la legibilidad y la fragilidad del código. Alguien vendrá más tarde y necesitará entender ese código, y lo apreciará si no es un campo minado lleno de oportunidades para arruinarlo. Ese alguien puede ser el futuro-tú, ¡así que sé amable!
my_index
es un tipo firmado, %8
será más caro que &7
, a menos que el compilador logre probar que el valor no es negativo en ese punto. (Porque el asm tendrá que producir un resto negativo para negativo my_index
).%8
formulario sea más "legible" que el &7
formulario y, en cualquier caso, if (my_index == 8)
podría decirse que es más autodocumentado. Un problema con el uso de & es que, si tuviera una macro, my_index = (my_index + 1) & (INDEX_MAX -1)
podría suponer que INDEX_MAX
debe ser una potencia de dos, pero es posible que alguien más adelante no comparta esa suposición, luego lo configuran en 10 y rompen su código.SER_RXQUEUE_SIZE
y SER_RXQUEUE_MASK
, con la última definida justo debajo de la primera, como (SER_RXQUEUE_SIZE-1)
. Entonces se podría usar a #if
para chirriar si (SER_RXQUEUE_SIZE & SER_RX_MASK)
no es cero.it
/ ite
(para predicar hasta 4 instrucciones posteriores). godbolt.org/z/shqaGW6d5 . Tiene razón en que M0 no lo admite, o al menos los compiladores eligen no usarlo para esa prueba. IIRC, el predicado se evalúa por separado para cada instrucción predicada, por lo que las instrucciones de establecimiento de banderas, como cmp
dentro de un it
bloque, funcionan como en el modo ARM, por lo que it
no necesariamente se puede manejar como un salto sobre la parte falsa. Sin embargo , existe una restricción para que una rama predicada sea la última insn en un it
bloque.(a % 8) == (a & 0x0007)
siempre sea cierto. Por lo general, será lo que sea que haga el procesador debajo del capó.int % 8
puede optimizarse a solo una instrucción AND o TEST / TST, a menos que lo derrote con a%2 == 1
(y los compiladores reales hacen esto) godbolt.org/z/xPnjPWq7f ). El ejemplo en la pregunta no era uno de esos casos. En ISO C99 y versiones posteriores, a%8
está completamente definido como un resto, no módulo, para ir con /
una definición completa como truncamiento hacia cero, no como Python. (Y sí, esto es lo que hacen todas las CPU convencionales modernas con división HW debajo del capó) IIRC, C89 permitió la elección que sugiere.…or only ever changes to % N where N is always 2^n…
y el compilador lo sabe (es una constante de tiempo de compilación, no una variable pasada a una función, por ejemplo)En primer lugar, en caso de que no sea obvio: un mayor tiempo de ejecución significa un mayor consumo de energía. Aunque si lo que más le interesa es reducir el consumo de energía, mire el reloj del sistema antes que nada.
¿Por qué la operación de módulo consume más energía?
No lo hace en un compilador moderno. La división y el módulo son operaciones pesadas de CPU para la mayoría de los núcleos, pero el código C que generaba div
instrucciones reales, etc., ocurría principalmente hasta hace unos 15 o 20 años. Los compiladores modernos elegirán el mejor código cuando habilite las optimizaciones y evitarán la división cuando se pueda evitar. Además, la división es un problema mayor en cuanto al rendimiento en MCU de gama baja como 8 y 16 amargos.
Sin embargo, debe mencionarse que es una práctica algo común en los sistemas integrados ejecutar con todas las optimizaciones deshabilitadas. Principalmente porque los optimizadores de compiladores de varios compiladores integrados de calidad mediocre se ganaron legítimamente una mala reputación de tener errores, allá por los 90 y principios de los 2000.
Si ejecuta con las optimizaciones deshabilitadas, entonces, por supuesto, está solo y tiene que realizar todas las optimizaciones manualmente, lo que definitivamente no es una práctica recomendada a menos que tenga un conocimiento profundo sobre C y la CPU de destino.
Vamos a desensamblar sus ejemplos de código particulares en gcc-arm-none-eabi, con -O3
. Hice estos ejemplos independientes:
void func1 (void)
{
static unsigned int my_index;
while(1)
{
my_index++;
if (my_index >= 8) my_index = 0;
volatile unsigned int out = my_index;
}
}
void func2 (void)
{
static unsigned int my_index;
while(1)
{
my_index = (my_index + 1) % 8;
volatile unsigned int out = my_index;
}
}
Los volátiles son necesarios como efecto secundario para impedir que el optimizador elimine el código por completo. Ahora, al desmontar esto usando Godbolt https://godbolt.org/z/bM5M5v38h , obtenemos un código de máquina casi idéntico. Ninguna división a la vista. La versión con adición en realidad está funcionando un poco peor debido a la cmp
instrucción (rama).
Pensé que eliminar declaraciones condicionales podría funcionar mejor
Sí, en general, y en su caso realmente lo hace, aunque es una microoptimización. Cortex M en general no tiene predicción de rama avanzada ni memoria caché. En un M0, no vale la pena el dolor de cabeza siquiera considerarlo. Creo que algunos STM32x4 tienen soporte de hardware para una forma simple de predicación de bifurcación. M7 de gama alta, etc. tendrá caché y luego evitar las ramas es más importante.
En general:
Debe esforzarse por escribir código lo más legible posible. Luego optimice cuando haya cuellos de botella de rendimiento reales en su código. Las optimizaciones manuales son un trabajo altamente calificado y requieren mucha experiencia.
En este caso particular, diría que la versión de adición/contador es más legible, por lo que la usaría independientemente de unos pocos tics de CPU más o menos.
En cuanto al blog que vinculó, el autor no es un novato completo y tiene algunos puntos buenos, pero se mencionan algunas cosas extrañas e incluso equivocadas. Déjame comentar sobre esa lista de viñetas de la que obtuviste el comentario del módulo:
Utilice la clase "Static Const" tanto como sea posible para evitar la copia en tiempo de ejecución de matrices, estructuras, etc. que consume energía.
No tengo idea de lo que se supone que significa una clase de "const. estática". C distingue notablemente entre mayúsculas y minúsculas y no tiene una palabra clave de clase. Supongo que el autor no conoce la terminología adecuada de C y en realidad quiere decir algo como: Use static
especificadores de clase de almacenamiento y corrección constante siempre que sea posible. Si eso es lo que querían decir, es un buen consejo general.
Utilice punteros. Probablemente son la parte más difícil de entender del lenguaje C para los principiantes, pero son los mejores para acceder a estructuras y uniones de manera eficiente.
Es como decirle al trabajador de la construcción que use concreto... es obligatorio, no una opción. Los punteros son un bloque de construcción fundamental de C.
¡Evita Módulo!
No es realmente un buen consejo como se demostró anteriormente.
Variables locales sobre variables globales cuando sea posible. Las variables locales están contenidas en la CPU mientras que las variables globales se almacenan en la RAM, la CPU accede a las variables locales más rápido.
Generalmente correcto, aunque las variables de alcance de archivo ("globales") también pueden almacenarse temporalmente en registros. La principal razón para evitar variables verdaderamente globales (enlace externo) es el diseño del programa, no el rendimiento.
Además, la diferencia entre el registro y el acceso a la RAM no es tan grande en la mayoría de las MCU, este comentario se aplica principalmente a las CPU de gama alta como x86, Cortex A, Power PC, etc. Al optimizar manualmente el acceso a la memoria para las MCU de gama media como Cortex M, debe más bien considere flash vs RAM, ya que flash a menudo tiene estados de espera.
Sin embargo, reducir el alcance de las variables siempre es bueno para la legibilidad, para minimizar los errores y reducir el desorden del espacio de nombres.
Los tipos de datos sin firmar son su mejor amigo siempre que sea posible.
Cierto, pero no por el rendimiento, sino por las conversiones implícitas y el comportamiento mal definido de los operandos con signo/negativos cuando se usan en operaciones bit a bit.
Adopte la "cuenta regresiva" para los bucles cuando sea posible.
Ok, este es un consejo de dinosaurio aún peor que el del módulo. La razón de esto es muy conocida, que comparar con cero es más rápido que comparar con un valor. ¡Pero los compiladores han podido hacer esa optimización durante mucho tiempo! No escriba bucles de conteo regresivo, eso es simplemente ofuscación por nada ganado. Este fue un consejo válido alrededor del año 1993, no en el año 2022.
En lugar de campos de bits para enteros sin signo, utilice máscaras de bits.
Buen consejo, pero tampoco relacionado con el rendimiento, sino con la portabilidad y el comportamiento mal definido.
En general, la calidad de la publicación del blog es diversa: los consejos muy sólidos se mezclan con los malos. Dejaría de leer ese blog. Tenga en cuenta con qué frecuencia en esta respuesta tengo que retroceder 20-30 años en el tiempo al comentarla.
const
variable global tiene un enlace externo por defecto. Por lo tanto, a menos que realice una optimización del tiempo de enlace, se asignará en la RAM. Un global static const
tiene enlace interno, y el compilador tiene la oportunidad de optimizar su almacenamiento.~
de un número entero pequeño puede convertir números sin signo en signos y negativos. El desbordamiento aritmético con signo es un comportamiento indefinido en C pero bien definido en la CPU ISA. Etcétera.const
un calificador en el alcance del archivo. Podría ser RAM, podría ser ROM, o podría ser algún enlace de rareza de arquitectura de Harvard en caso de que se aplique. Agregar static
es una buena práctica con seguridad, pero no hay garantías. Siempre verifique el archivo del mapa para estar seguro.x >>= 1; // divide by 2
.
uwe
Mirón
usuario_1818839
ablando
Rodney
Super gato