Tips de lenguaje C

El "problema" siempre es el mismo: no pueden pretender predecir el codigo que va a generar el compilador, y menos aun en cross-compilers multi-target, ya que depende de un par de miles de cosas que son contexto-dependientes.
Hay algunas tecnicas basicas de optimizacion que se pueden hacer manualmente, pero no pueden evitar el karma: "no podes tener algo sin dar algo a cambio", y si queres explotar el ultimo recurso del set de instrucciones no queda otra que usar assembler... y pagar por el excesivo tiempo de desarrollo. Determinar si vale la pena o no es una decision tambien de contexto...
 
No veo muy claro que un bucle while() como el que muestras sea más rápido que un bucle for(), ya que, cuando se crea el código intermedio, tanto uno como otro genera el mismo código (una inicialización, salto el final del bucle donde se coloca la condición, incremento de la variable, salto al comienzo del bucle, y contenido del bucle).

Como dice Dr. Zoidberg, depende del contexto. Un ejercicio como Finalizar rutina cuesta lo mismo escribirlo en C que en ensamblador.
 
Y si es muy complejo el programa y necesitas optimizar un poco. Disculpa (Dr. Zoidberg) si sueno hiriente pero ya habías dado una opinión al respecto y no contribuye con el tema. Es muy interesante Lo de los for ya que en lo personal los Uso mucho aún si me basta con el while.
Otra forma de optimizar es en los if es algo básico pero quizás algunos No lo sepan esto mejora más que todo velocidad.

Si tienes
Código:
if (día == 31|| día % 2 == 0)
Es más eficiente hacerlo así
Código:
if (día % 2 == 0 || día == 31)
Esto debido a que los días transcurren todos los días valga la redundancia pero es 31 una vez al mes por eso es mejor colocar la que es mas frecuente de primera (al cumplirse la primera no evalúa la segunda) a hacerlo al revés.
Eso con el OR pero si es un AND "&&" es mejor al revés (colocando la que tiene más probabilidades de fallar de primera)

PD: habría que comprobar si hay ahorro o no luego lo compruebo
 
Última edición:
sera?

si soy criticado por el for y el while

pero ami me consto cuando cargaba puntos en la 486 , no sabia como cargar un bitmap asi que dibujaba una figura en texto donde una letra indicaba el color.

y con for se veia que tardaba un poco mas que en el while en cargar la imagen.

esto mismo lo lleve al pic pero el resultado NO era similar.

nada que ver con la 486.

a lo que voy es cierto que depende del compilador, no es lo mismo compilar un codigo C para una PC, a un micro sea avr o pic.

el problema personal que veo en el CCS "no es que sea un compilador malo del todo".

pero si uno usa demaciados if por alguna extraña razon empiezan a haber bugs, si uno usa demaciada RAM empiezan problemas de que las variables les entra basura.

y si uno hace uso de un int16 y quiere imprimir con printf
si se imprimira con usando %ld "long int" pero si uno usa varias veces la impresion con %ld

empieza a imprimir basura.

ahi es cuando empieza a ser criticado el compidador que hace sus patrañas.

como digo es muy propio del compilador.

otra cosa yo vi que en C18 el codigo que sale es mas largo en bytes que en el CCS
¿por que?

no tengo idea pero ahi es cuando uno ve que es cuestion de quien hiso el compilador
 
Otra forma de optimizar es en los if es algo básico pero quizás algunos No lo sepan esto mejora más que todo velocidad.

Si tienes
Código:
if (día == 31|| día % 2 == 0)
Es más eficiente hacerlo así
Código:
if (día % 2 == 0 || día == 31)
Esto debido a que los días transcurren todos los días valga la redundancia pero es 31 una vez al mes por eso es mejor colocar la que es mas frecuente de primera (al cumplirse la primera no evalúa la segunda) a hacerlo al revés.
Eso con el OR pero si es un AND "&&" es mejor al revés (colocando la que tiene más probabilidades de fallar de primera)

PD: habría que comprobar si hay ahorro o no luego lo compruebo
No, no hay ahorro de memoria porque el número de operaciones es siempre el mismo. Pero esta optimización es de tiempo de ejecución, y está indicada desde los primeros tiempos del C.

En el libro "The C Programming Language", de Brian W. Kernighan y Dennis M. Ritchie, Prentice Hall, 2ª ed., ISBN 0-13-110362-8, 1988, en el apartado 1.5.4 Word Counting, dice:
Existe el correspondiente operador && para AND; su precedencia es justo un poco más alta que ||. Expresiones conectadas con && o || se evalúan de izquierda a derecha, y se garantiza que la evaluación parará tan pronto como se sepa la verdad o falsedad (de toda la expresión).
¡Gracias por recordarlo!

TRILO-BYTE dijo:
y si uno hace uso de un int16 y quiere imprimir con printf
si se imprimira con usando %ld "long int" pero si uno usa varias veces la impresion con %ld

empieza a imprimir basura.

ahi es cuando empieza a ser criticado el compilador que hace sus patrañas.
Es completamente normal que te saque basura: le estás pidiendo que saque la información de un largo (Int32_t) cuando solo tienes un Int16_t. Esto también me pasó a mi de las primeras veces que programé en C, pero la solución es entender que a C hay que decírselo todo muy claro. Y leer el manual, en la parte que explica el printf :)

Entonces, o le dices que quieres imprimir un entero sin más ("%d"), o le haces un casting al int16 para convertirlo en int32 antes de pasárselo al printf (algo así como printf "%ld", (long) variable). Es obvio que la primera solución suele ser la mejor.

Aparte de todo esto, los padres del C no recomiendan programar pensando en el orden de evaluación: podemos llevarnos alguna sorpresa.

En el mismo libro de C comentado antes, al final del segundo capítulo, dice (hablando del orden de ejecución de las sentencias):
Los compiladores pueden interpretar esto (el último ejemplo) de formas diferentes, y generar diferentes respuestas dependiendo de su interpretación. El estándar deja muchos de esos asuntos sin especificar. Cuando ocurren efectos colaterales (asignación a variables) dentro de una expresión, se deja a la discreción del compilador. ya que el mejor orden depende estrechamente de la arquitectura de la máquina. (El estándar especifica todos los efectos colaterales en los argumentos antes de llamar a una función, pero eso no ayudaría en la llamada al printf anterior). La moraleja es que escribir código que dependa del orden de evaluación es una mala práctica de programación en cualquier lenguaje. Naturalmente, es necesario saber qué cosas evitar, pero si no sabes cómo se realizan en diferentes máquinas, no tendrás la tentación de aprovecharte de una ventaja de una determinada implementación.
 
Última edición por un moderador:
Y si es muy complejo el programa y necesitas optimizar un poco. Disculpa (Dr. Zoidberg) si sueno hiriente pero ya habías dado una opinión al respecto y no contribuye con el tema.
Te pido disculpas, pero lo que no contribuye con el tema, y menos aún con el conocimiento es lo que vos has escrito:

Otra forma de optimizar es en los if es algo básico pero quizás algunos No lo sepan esto mejora más que todo velocidad.

Si tienes
Código:
if (día == 31|| día % 2 == 0)
Es más eficiente hacerlo así
Código:
if (día % 2 == 0 || día == 31)
Esto debido a que los días transcurren todos los días valga la redundancia pero es 31 una vez al mes por eso es mejor colocar la que es mas frecuente de primera (al cumplirse la primera no evalúa la segunda) a hacerlo al revés.
Eso con el OR pero si es un AND "&&" es mejor al revés (colocando la que tiene más probabilidades de fallar de primera)

Y lo ha puesto muy claramente el quote de Joaquin:

Los compiladores pueden interpretar esto (el último ejemplo) de formas diferentes, y generar diferentes respuestas dependiendo de su interpretación. El estándar deja muchos de esos asuntos sin especificar. Cuando ocurren efectos colaterales (asignación a variables) dentro de una expresión, se deja a la discreción del compilador. ya que el mejor orden depende estrechamente de la arquitectura de la máquina. (El estándar especifica todos los efectos colaterales en los argumentos antes de llamar a una función, pero eso no ayudaría en la llamada al printf anterior). La moraleja es que escribir código que dependa del orden de evaluación es una mala práctica de programación en cualquier lenguaje. Naturalmente, es necesario saber qué cosas evitar, pero si no sabes cómo se realizan en diferentes máquinas, no tendrás la tentación de aprovecharte de una ventaja de una determinada implementación.

que, por supuesto, fué lo que dije en el mismo comentario que calificaste como que "no aporta".
 
Otro truco que me han contado hace un par de horas. Depende del compilador, desde luego, pero el gcc-avr a veces tiene problemas para distinguir estos casos.

Supongamos que queremos hacer un bucle de 25 vueltas. Lo escribiríamos así:
PHP:
for (i = 0; i < 25; i++) {
pero, según el compilador -repito-, si lo escribimos de otra manera, podemos ahorrar alguna instrucción:
PHP:
i = 25;
do {
    # ...

} while(--i);
La clave está en la última línea: el compilador la traduce a una instrucción de decremento de la variable, y si el resultado es distinto de cero, repite el bucle, algo que en muchos códigos en ensamblador lo hace en una sola instrucción.

Si hacés esto, deberías obetener un resultado similar:

PHP:
for (i = 25; i ; i--)

Tal como dijiste:

La clave está en la última línea: el compilador la traduce a una instrucción de decremento de la variable, y si el resultado es distinto de cero, repite el bucle, algo que en muchos códigos en ensamblador lo hace en una sola instrucción.

Al decrementar, en la preguna del salto, solo se hace por el flag "zero" del registro de los bits de estado, en cambio si la operatoria fuera incremental, si o si se debe agregar una instrucción más de comparación (o resta) previamente a la pregunta.
 
No, no hay ahorro de memoria porque el número de operaciones es siempre el mismo. Pero esta optimización es de tiempo de ejecución, y está indicada desde los primeros tiempos del C.

Sorry joaquin, no me especifique, me refería a que comprobaría el ahorro de memoria del for, no del orden de los operadores, por eso especifique antes que era una mejora de velocidad. Ese tip lo leí en el libro con el que aprendí a programar y hombre debes tener memoria fotografica porque recuerdas de que libro proviene (o lo buscas por internet xd).

Te pido disculpas, pero lo que no contribuye con el tema, y menos aún con el conocimiento es lo que vos has escrito:



Y lo ha puesto muy claramente el quote de Joaquin:



que, por supuesto, fué lo que dije en el mismo comentario que calificaste como que "no aporta".
Afff... Mira no quiero ponerme intenso pero no estoy diciendo que no tengas razón respecto a los compiladores. Lo que me refiero es que antes ya habías especificado que puede afectar como lo interprete el compilador. Ahora, si quieres podemos llenar el tema de mensajes repetidos sobre que el compilador puede interpretarlo de otra manera, no sé quizás al lector se le olvide que el compilador tiene que ver luego de darle clic para ver la próxima pagina y tengamos que recordarlo, no, por si se le olvida, uno no sabe quizás tenga problema como tener memoria de pez. Sarcasmo aparte no es necesario que repitas una, otra y otra vez lo mismo, ya que desde el inicio del tema has expresado tu desacuerdo con el tema.

Dime algo, es igual:
Código:
void add(float x, float y, float *z) 
{ 
*z = x + y; 
}
...

main() 
{ 
int i, j; 
static float x[N], y[N], z[N];

for(j=0; j<ITER; j++) 
    for(i=0; i<N; i++){ 
        add(x[i], y[i], &z[i]); 
        } 
}

Que hacer las iteraciones dentro de la función (dejando aparte la reusabilidad del código)
Código:
void add2(float *x, float *y, float *z) 
{ 
register int i, j;
for(j=0; j<ITER; j++) 
   for(i=0; i<N; i++) 
       z[i] = x[i] + y[i]; 
}

...

main() 
{ 
static float x[N], y[N], z[N];

add2(x, y, z); 
}
O usar operaciones de desplazamiento las cuales son instrucciones que maneja de manera natural el procesador.

Si el compilador es tan inteligente para discriminar esos casos o casos mas complejos y optimizarlos, si es así entonces me retracto tu ganas...

Ahora cuando existan computadoras mas inteligentes que el ser humano, quizás si me dedique a escribir basura de código ya que igual ella lo optimizara, ahh cierto porque para ese momento ellas serán capaces de escribir el código solas sin ayuda, ¿si los compiladores ya son tan inteligentes por que no lo hacen ya ellos solo?. Hoy en día no existe ese lujo, así que según mi muy humilde opinión deberíamos escribir código eficiente.

Respecto a la ultima cita de juaquin.

Se refiere a esto:
Código:
printf("%d %d", n++, funcion(n));

Ahí no se sabe que ocurrirá si se llama a función primero antes de sumar n o sumar n y luego llamar, esto dependería del compilador la manera de corregirlo seria:
Si quieres que primero se sume

Código:
n++
printf("%d %d", n, funcion(n));

o si quieres llamar primero a la función y luego sumar:

Código:
printf("%d %d, n, funcion(n));
n++;

o llamar a la funcion sin sumar n y mostrar n sumado:
Código:
tmp = n;
printf("%d", n++, funcion(tmp));
y podría seguir indefinidamente según el caso.

Así que no entiendo sinceramente a que te refieres, la cita trata sobre la forma en que se ejecuta el printf (si se ejecuta de derecha a izquierda o izquierda a derecha en una llamada a funcion) osea que se realiza primero si la función, o el uso de n ya que el estándar no especifica esto.

Ahora dime en que no aporta mi comentario si la cita final no tiene nada que ver con mi orden de colocación de los operando, me podrías explicar tengo rato dando vueltas pero no entiendo :confused:

Otra cosa ¿tu crees que existiría el orden de precedencia para que los compiladores no lo sigan? sumar antes que multiplicar "a + b * c" o multiplicar fuera del paréntesis ignorándolo? "a + (b + c) * c" me parece muy descabellado re analiza el enunciado enfría tu cabeza y piensa tu próxima respuesta por favor.

PD: Cualquier error en mi respuesta por favor corrijan que muy gratamente tomare la observación.
PDD: Si alguien pudiera esclarecer este tema lo agradecería quizás soy yo el que me equivoco.
 
Última edición:
Aquí no se trata de discutir, sino de aclarar cosas. Y lo único claro es que cada compilador es de su padre y de su madre.

Ejemplo: el gcc y el gcc-avr tienen la misma raíz por lo que cuentan con el mismo conjunto de optimizaciones en el árbol sintáctico del programa una vez interpretado, *pero* la traducción de los opcodes a lenguaje ensamblador es muy mala en el caso del gcc-avr (o al menos, no tan buena como en las arquitecturas 80x86).

Entonces, lo que estamos diciendo es que tú puedes trabajar mucho el código para intentar optimizarle, pero dependes también del comportamiento del compilador para obtener un resultado bueno o menos bueno.

No se trata de ver quién tiene razón porque, es que en realidad no sabemos de en qué escenario estamos hablando :)

En cuanto al ejemplo de código que pones, en el primero estás llamando ITER*N llamadas a la función add(), más que el segundo código, así que el segundo es más eficiente.

Hay una parte de una asignatura en Informática (ahora no recuerdo el nombre), donde se explica cómo calcular la eficiencia, pero básicamente consiste en sumar todas las operaciones matemáticas, de salto, y de condición.

Sobre la cita del libro (que me he leído varias veces), lo que quería decir es que los padres del C recomiendan escribir código limpio frente a código "supuestamente" optimizado (los ingleses lo llaman "no pasarse de listo") porque a) puede llegar el momento en que escribas un código que pueda ser complejo de mantener en el futuro, y b) -lo dicho antes- no podemos fiarnos de cómo va a ser traducido nuestro programa cuando cambiemos de arquitectura o compilador. Así que recomiendan escribir código "limpio y claro", sin más.

Y solo, *solo*, en el caso de que tengamos problemas de memoria o consumo de CPU, sí que le dedicaremos tiempo a investigar trucos de optimización (que es justo el tema de este hilo que abriste).

Del ejemplo del printf(), el estándar dice que se tienen que resolver los argumentos, pero el compilador puede decidir cambiar el orden de evaluación. Entonces K&R recomiendan escribir esos casos de otra manera (por ejemplo, evaluar los argumentos antes de pasarlos al printf), y así queda claro el orden de ejecución.

Como regla general, hoy en día, se puede decir que los compiladores son muy buenos aunque el código sea muy malo (detectan muchos casos extremos), así que al final es mucho más importante escribir código que sea más fácil de leer por el mantenedor del código (otro ser humano), ya que es la parte más cara de todo el proceso.
 
Última edición por un moderador:
Si hacés esto, deberías obtener un resultado similar:
PHP:
for (i = 25; i ; i--)

Al decrementar, en la pregunta del salto, solo se hace por el flag "zero" del registro de los bits de estado, en cambio si la operatoria fuera incremental, si o si se debe agregar una instrucción más de comparación (o resta) previamente a la pregunta.

A ver... cuidado con la palabra "similar". Estamos hablando de ahorrar unos pocos bytes o de hacer que el código sea más eficiente.

Estamos hilando fino y eso implica que un breve cambio implica unos bytes más o menos.

Pero voy a poner un ejemplo que va a clarificar lo que quería decir.

Supongamos que queremos hacer un programa para un PIC16F877A a 4 Mhz que consiste en leer el puerto A 25 veces, con un retraso de 1 ms entre lecturas.

Tenemos este programa:
PHP:
#include <xc.h>
#include <pic16f877a.h>
#include <stdint.h>

#define _XTAL_FREQ 4000000                  // Fosc  frequency for _delay()  library

void main(void) {
    uint8_t i;
    uint8_t a;

    for (i = 0; i < 25; i++) {
        a = PORTA;
        __delay_ms(1);
    }

    return;
}
El código resultado es este:
PHP:
19:                for (i = 0; i < 25; i++) {
07DF  01F2     CLRF i                       ; i = 0

07E0  3019     MOVLW 0x19
07E1  0272     SUBWF i, W
07E2  1803     BTFSC STATUS, 0x0            ; ¿ 0 < 25 - i ? Sí, seguir
07E3  2FF9     GOTO 0x7F9                   ; no, acabar

20:                    a = PORTA;
07E4  1283     BCF STATUS, 0x5              ; <-
07E5  1303     BCF STATUS, 0x6
07E6  0805     MOVF PORTA, W
07E7  00F0     MOVWF __pcstackCOMMON
07E8  0870     MOVF __pcstackCOMMON, W
07E9  00F1     MOVWF a
21:                    __delay_ms(1);
07EA  30F9     MOVLW 0xF9
07EB  00F0     MOVWF __pcstackCOMMON
07EC  0000     NOP
07ED  0BF0     DECFSZ __pcstackCOMMON, F
07EE  2FEC     GOTO 0x7EC
07EF  2FF0     GOTO 0x7F0
07F0  0000     NOP
22:                }

07F1  3001     MOVLW 0x1                    ; i++
07F2  00F0     MOVWF __pcstackCOMMON
07F3  0870     MOVF __pcstackCOMMON, W
07F4  07F2     ADDWF i, F

07F5  3019     MOVLW 0x19
07F6  0272     SUBWF i, W
07F7  1C03     BTFSS STATUS, 0x0            ; ¿ 0 < 25 - i ? no, terminar
07F8  2FE4     GOTO 0x7E4                   ; sí, repetir  ->
23:
24:                return;
25:            }

07F9  120A     BCF PCLATH, 0x4
Las partes

a = PORTA;
__delay_ms(1);

siempre sin las mismas, pero lo que cambia es la forma de hacer el bucle.

Lo que nos interesa está aquí:
PHP:
19:                for (i = 0; i < 25; i++) {
07DF  01F2     CLRF i                   ; i = 0

07E0  3019     MOVLW 0x19
07E1  0272     SUBWF i, W
07E2  1803     BTFSC STATUS, 0x0        ; ¿ 0 < 25 - i ? Sí, seguir
07E3  2FF9     GOTO 0x7F9               ; no, acabar

; contenido del bucle

07F1  3001     MOVLW 0x1                ; i++
07F2  00F0     MOVWF __pcstackCOMMON
07F3  0870     MOVF __pcstackCOMMON, W
07F4  07F2     ADDWF i, F

07F5  3019     MOVLW 0x19
07F6  0272     SUBWF i, W
07F7  1C03     BTFSS STATUS, 0x0        ; ¿ 0 < 25 - i ? no, terminar
07F8  2FE4     GOTO 0x7E4               ; sí, repetir
Esto es lo que ha generado:
  1. inicializa i
  2. comprobar la condición: continuar o salir
  3. bucle: ejecutar el contenido del bucle
  4. incrementar i
  5. comprobar la condición: salir o
  6. saltar a repetir bucle
Total: 26 bytes.

Veamos ahora la propuesta de cosmefulanito04:
PHP:
19:                for (i = 25; i; i--) {
07E2  3019     MOVLW 0x19               ; i = 25
07E3  00F0     MOVWF __pcstackCOMMON
07E4  0870     MOVF __pcstackCOMMON, W
07E5  00F2     MOVWF i

07E6  0872     MOVF i, W                ; <-
07E7  1903     BTFSC STATUS, 0x2        ; ¿ i == 0 ? No, continuar
07E8  2FF9     GOTO 0x7F9               ; Sí, salir

; contenido del bucle

07F6  3001     MOVLW 0x1                ; i--
07F7  02F2     SUBWF i, F
07F8  2FE6     GOTO 0x7E6               ; ->
Es decir:
  1. inicializa i
  2. comprobar la condición: continuar o salir
  3. bucle: ejecutar el contenido del bucle
  4. decrementar i
  5. saltar a comprobar condición
Total: 20 bytes.

y ahora, el código que yo comentaba, con el do{ } while():
PHP:
19:                i = 25;
07E4  3019     MOVLW 0x19               ; i = 25
07E5  00F0     MOVWF __pcstackCOMMON
07E6  0870     MOVF __pcstackCOMMON, W
07E7  00F2     MOVWF i
20:                do {

; contenido del bucle

24:                } while (--i);
07F5  3001     MOVLW 0x1                ; --i
07F6  02F2     SUBWF i, F
07F7  1D03     BTFSS STATUS, 0x2        ; ¿ i == 0 ? Sí, terminar
07F8  2FE8     GOTO 0x7E8               ; No, repetir
O sea:
  1. inicializa i
  2. bucle: ejecutar el contenido del bucle
  3. decrementar i
  4. comprobar la condición: continuar o salir
  5. saltar a repetir bucle
Total: 16 bytes.

¡Vaya! un 38 % menos que el for() original, y un 20 % menos que lo que proponía cosmefulanito04.

Bueno, sí... hay trampa... ;) Ese 20 % menos es debido a que no hemos hecho la comprobación al inicio del bucle. Pero es que este es un bucle en el que sabemos que siempre será cierta la condición en la primera vuelta. Por eso podemos entrar en el do{} sin miedo.
 
Última edición por un moderador:
A ver, discutir por 4 bytes desvirtua todo, ya es demasiado. Si querés ahorrarte código hasta en ese nivel, directamente programá en assembler. Si vas a programar en C, 4 bytes de código no existen.

Además, yo en realidad apunto al loop (que en eso es donde realmente hay que poner la lupa) y no tanto al peso en código y de su inicialización previa. Compará el loop del for decremental que mencioné y del do while, vas a ver que es el mismo, no así con el otro for incremental.

Si hacemos un for de 500 repeticiones, el for incremental va a tener que laburar bastante más que los otros dos, ahora bien, esa optimización está buena cuando realmente vas muy justo de tiempo, sino... bien gracias, prefiero el for/while incremental que se entiende mucho mejor, además la exploración de un por ej. un vector es muchísimo más fácil de ver empezando desde su inicio hasta su final.
 
Última edición:
En realidad hoy las cosas ya no van por ese camino. Hace 20 años o más puede ser, hoy ya los sistemas embebidos no tienen un gran limitante en cuanto a memoria de código.

Si te falta memoria de código, no vale la pena matarse en reducir (salvo que sean cosas muy sencillas de corregir), por tres motivos, el tiempo de desarrollo es plata, a futuro muy posiblemente la capacidad que ya era un limitante, te va a limitar las posibles mejoras y por último, si estás corto en memoria de código es porque estas usando una familia de uC equivocada para el proyecto que querés desarrollar.

Por ej. en la línea AVR pasar de un Atmega8 a un 16 en términos de precios es casi lo mismo y mágicamente duplicaste esa capacidad de programa. Ni te cuento los ARM, tenés memoria de código para reírte. Lamentablemente este tipo de discusión es prehistórica.

En lo que se debería hacer mucho énfasis (en los casos más críticos), no es tanto en el tamaño del código, sino en la velocidad de ejecución que tendrá una cierta rutina. Ese es mi punto de vista y probablemente con el avance de la tecnología, los uC serán más y más rápido hasta tal punto que esos tiempos críticos sean cosas muuuy raras.

Supongo que el Dr. Zoidberg puede dar muchísimo más detalles sobre como se vino desarrollando todo lo que son sistemas embebidos.
 
En realidad hoy las cosas ya no van por ese camino. Hace 20 años o más puede ser, hoy ya los sistemas embebidos no tienen un gran limitante en cuanto a memoria de código.

Si te falta memoria de código, no vale la pena matarse en reducir (salvo que sean cosas muy sencillas de corregir), por tres motivos, el tiempo de desarrollo es plata, a futuro muy posiblemente la capacidad que ya era un limitante, te va a limitar las posibles mejoras y por último, si estás corto en memoria de código es porque estas usando una familia de uC equivocada para el proyecto que querés desarrollar.

Por ej. en la línea AVR pasar de un Atmega8 a un 16 en términos de precios es casi lo mismo y mágicamente duplicaste esa capacidad de programa. Ni te cuento los ARM, tenés memoria de código para reírte. Lamentablemente este tipo de discusión es prehistórica.

En lo que se debería hacer mucho énfasis (en los casos más críticos), no es tanto en el tamaño del código, sino en la velocidad de ejecución que tendrá una cierta rutina. Ese es mi punto de vista y probablemente con el avance de la tecnología, los uC serán más y más rápido hasta tal punto que esos tiempos críticos sean cosas muuuy raras.

Supongo que el Dr. Zoidberg puede dar muchísimo más detalles sobre como se vino desarrollando todo lo que son sistemas embebidos.

Coincido Cosme. Hoy por hoy me parece que un aspecto mucho más importante es la posibilidad de expandir el sistema, agregar nueva funcionalidad, conectarle nuevo hardware...
Un proyecto arranca siendo un sensor de temperatura para un compartimento, después te dicen que estaría bueno que controle un ventilador, después que haga un log en memoria flash, y por supuesto -no puede faltar- poder controlarlo desde un celular. Ah sí, y dejalo abierto para un módulo GSM que pueda mandar los datos al servidor XXX que administra 100 aparatitos a la vez.
Y no te olvides del bootloader para solucionar los bugs a futuro (aún si tu código es 100% correcto, que no lo es; las librerías que estás usando seguramente van a tener bugs y vas a precisar actualizarlo remotamente).

Para eso es necesario tener una arquitectura de software clara, no importa tanto cual: algunos prefieren un RTOS, otros máquinas de estado jerárquicas, otros un esquema productor/consumidor, sistema orientado a eventos...
El tema es que en el código quede clara la funcionalidad del sistema y como agregarle funcionalidad nueva, para que el día de mañana cuando se quiera agregar algo nuevo después de 6 meses o 1 año de tener el proyecto en el freezer se pueda hacer sin tener que estudiar todo el código de vuelta, y sin miedo de que si toco esto acá se rompe el sistema o esta otra parte deja de funcionar. Y si hay que meterse en la funcionalidad básica del sistema entonces que sea un código legible, con nombres de variables significativos, comentarios que expliquen lo que se pretende hacer,etc
 
Por favor, no desvirtuemos el espíritu del hilo (ver #1).

Todos estamos de acuerdo en la opinión de cosmefulanito04. Incluso yo mismo se lo he aconsejado a otro miembro, en otro hilo, cuando le he dicho que por unos céntimos más tiene el doble de memoria, así que no perdiera más el tiempo.

En cambio, este hilo es justo lo contrario: partimos del interés en intentar hacer el código más eficiente, aunque sea ahorrando medio byte (un nibble).
 
Aunque mis comentarios "parezcan" desalentadores, voy a insistir en que optimizar ejemplos simples de programas no representa absolutamente nada. Las optimizaciones siempre son contextuales: no solo dependen de los lazos y contadores, sino que tambien dependen del mapa de memoria utilizado, del uso de los registros y disponibilidad de los mismos, etc, etc, etc.
Si alguien cree que puede predecir que diablos va a hacer el compilador en todas las oportunidades y arquitecturas, entonces recien podemos hablar de tips generales de optimizacion, pero como dudo que eso suceda deberemos atarnos a optimizaciones especificas. Por eso opino que si vamos a tratar de encontrar patrones de optimizacion (que por lo visto son todas de tamaño y ninguna de velocidad) entonces hay que analizar una aplicacion completa y el codigo assembler generado para ver cuales son las decisiones que tomó el compilador EN ESE CASO y asi poder hacer alguna estimacion mas o menos consistente.
 
Última edición:
Dejo un pequeño tip para cuando se usa muchos retardos por soft, yo lo utilizo en CCS. Para evitar la repeticion de mucho codigo en demoras, se hace uso de una funcion que contenga una demora fija y que a partir de un contador, repita ese delay.
Código:
void Demora_ms(long tiempo)
{
    while (tiempo--)
       delay_ms(1); Demora de un mseg
}
...........
Demora_ms (100); // Retardo 100 mseg
...........
...........
Demora_ms (350); // Retardo 350 mseg
En tiempo se pone el numero que representaria en mseg. Para tiempos cortos (<500 mseg) y un cristal de 4MHz o mas la diferencia en el tiempo es casi imperseptible, 1 o 2 mseg de mas. Para useg se podria aplicar pero ya el error es mucho mayor.
 
Última edición:
Eso se me hace redundante y menos preciso.
Si ya tienes instrucciones para retardos, ¿para qué crear otra/s?
Da lo mismo que ejecutes un delay_ms(100); a que llames a otra rutina que también usa la instrucción delay_ms(time);

Aparte, las instrucciones involucradas en la rutina harán que el retardo sea menos preciso que la misma instrucción.
Y si requieres mayores tiempo de retardo, para eso están los Timers.
 
sobre todo que es una aberracion usar un delay

pero si se puede hacer esto con un delay

int16 contador;
while(1)
{
if(contador>= 1000) //un segundo
{
prende_led;
contador=0;
}
else
{
apaga_led;
}

contador++;
delay_ms(1);
}

sigue siendo una solucion marrana pero solo atoro el micro 1 milisegundo
 
Por favor, no desvirtuemos el espíritu del hilo (ver #1).

Todos estamos de acuerdo en la opinión de cosmefulanito04. Incluso yo mismo se lo he aconsejado a otro miembro, en otro hilo, cuando le he dicho que por unos céntimos más tiene el doble de memoria, así que no perdiera más el tiempo.

En cambio, este hilo es justo lo contrario: partimos del interés en intentar hacer el código más eficiente, aunque sea ahorrando medio byte (un nibble).

Entre los tips de C los tips de optimización son solo una parte. No todos los tips tienen por qué ser de optimización.
Me pareció que era una crítica válida no invertir un esfuerzo excesivo en optimizar sacrificando por ello cosas como capacidad de expansión, claridad de código, etc.

Pero bueno, ya se dijo de sobra, siendo que tiramos una de cal, va otra de arena: dividir por una constante cuando en el micro hay un multiplicador por hardware.

Los micros de medio pelo para arriba incorporan multiplicadores hardware, mínimo de 8 bits (con resultado de 16 bits), y se suele dar el caso donde es necesario dividir por una constante conocida de antemano.
Que pasa si queremos hacer algo como:

uint8_t variable = otraVariable/17; //otraVariable también es de 8 bits

Si tengo un multiplicador de 8 bits que me da un resultado en 16 bits (digamos que da el resultado en 2 registros tamaño byte llamados RESL y RESH, siendo parte baja y alta respectivamente), puedo hacer la división con una multiplicación:

otraVariable / 17 = otraVariable /17 * (256 / 256) = otraVariable * (256/17) / 256

La división por 256 está implícita al quedarnos con la parte alta del resultado de la multiplicación (con el registro que llamamos RESH).
256/17 = 15.0588... no es un número redondo, voy a tener que convivir con el error de redondearlo a 15.

Entonces, en vez de escribir la línea

Código:
uint8_t variable = otraVariable/17;
Puedo hacer
Código:
uint8_t variable;
uint16_t variable16 = otraVariable * 15;
variable = ParteAlta(variable16);
Donde parteAlta puede ser una macro que devuelva el byte alto de una variable de 16 bits.

Ejemplo con números:
Si variable = 210
Queremos obtener 210/17 = 12.35 -> 12
Para hacer la división multiplicando el factor será 256/17 = 15.05.. -> 15
La cuenta que vamos a hacer es 210*15 = 3150 = 0x0C4E
Parte alta = 0x0C = 12 (por lo menos dió sin error en este caso particular, aunque no es lo normal, suele haber error).

¿Y sirve de algo meterse en este menester?. Puede que sí, puede que no. A veces pasa que el compilador ignora el multiplicador hardware, o al menos no lo usa para hacer divisiones por constantes, aunque sí para multiplicaciones.
Para un micro ARM de 32 bits no creo que sirva porque ya tienen unidades aritmético lógicos muchos más potentes (división incluída) y código bastante maduro. Puede andar para micros de 8 bits (si tienen multiplicador y una ALU sin división).

¿Y tanto le cuesta a un micro de 8 bits hacer una división?, y... gratis no le sale.
Según recuerdo para compilador gcc (o derivados, cada vez es más común que los compiladores para micros sean derivados de gcc) eso viene en la librería stdlib/libgcc donde hay funciones con nombres *divmod*.
A ver si lo encuentro... uff, costó... acá está la versión para AVR de la página oficial de gcc (tarda un rato largo en cargar)
https://gcc.gnu.org/viewcvs/gcc/trunk/libgcc/config/avr/lib1funcs.S
La función de división 8/8 está hecha en assembler:
https://gcc.gnu.org/viewcvs/gcc/trunk/libgcc/config/avr/lib1funcs.S?view=markup#l1335
Código (no borré los números de línea):
Código:
1335     .section .text.libgcc.div, "ax", @progbits
1336     
1337     /*******************************************************
1338            Division 8 / 8 => (result + remainder)
1339     *******************************************************/
1340     #define r_rem   r25     /* remainder */
1341     #define r_arg1  r24     /* dividend, quotient */
1342     #define r_arg2  r22     /* divisor */
1343     #define r_cnt   r23     /* loop count */
1344     
1345     #if defined (L_udivmodqi4)
1346     DEFUN __udivmodqi4
1347             sub     r_rem,r_rem     ; clear remainder and carry
1348             ldi     r_cnt,9         ; init loop counter
1349             rjmp    __udivmodqi4_ep ; jump to entry point
1350     __udivmodqi4_loop:
1351             rol     r_rem           ; shift dividend into remainder
1352             cp      r_rem,r_arg2    ; compare remainder & divisor
1353             brcs    __udivmodqi4_ep ; remainder <= divisor
1354             sub     r_rem,r_arg2    ; restore remainder
1355     __udivmodqi4_ep:
1356             rol     r_arg1          ; shift dividend (with CARRY)
1357             dec     r_cnt           ; decrement loop counter
1358             brne    __udivmodqi4_loop
1359             com     r_arg1          ; complement result
1360                                     ; because C flag was complemented in loop
1361             ret
1362     ENDF __udivmodqi4
1363     #endif /* defined (L_udivmodqi4) */
Como se ve usa un algoritmo con un loop que se repite 8 veces donde se hacen comparaciones, saltos, decremento, y resta. Pongamos un número redondo (a mi favor :D ) de 50 instrucciones.
Si eso se puede reemplazar por 2 instrucciones entonces no está tan mal (ok ok, no son solo 2, digamos que pueden rondar las 5?).

No tengo un compilador instalado para AVR en este momento, ¿alguien lo puede compilar a ver que pasa?.
Código 1 (normal):

Código:
#include blabla... lo que requiere el avr
#include <stdint.h>

uint8_t dividendo;
uint8_t divisor;

void main(void)
{
    //TODO Lo que requiera el micro para inicializar
    
    divisor = dividendo/17;

    while(1)
    {
        nop();
    }
}
Código 2 (optimizado?):

Código:
#include blabla... lo que requiere el avr
#include <stdint.h>

uint8_t dividendo;
uint8_t divisor;
uint16_t mul;

void main(void)
{
    //TODO Lo que requiera el micro para inicializar
    
    mul = dividendo * 15;
    divisor = high(mul);
    while(1)
    {
        nop();
    }
}
Ojalá no haya escrito de gusto :oops:


Actualización

Ojalá no haya escrito de gusto :oops: -> SI, escribí de gusto... ahí va el descargo.
Me instalé el avr gcc (para Ubuntu) siguiendo:
http://maxembedded.com/2015/06/setting-up-avr-gcc-toolchain-on-linux-and-mac-os-x/

Afortunadamente, parece que siguiendo las instrucciones al pie de la letra no hay ningún error raro, pude compilar el código de ejemplo sin ningún problema (y también lo hice fallar borrando un punto y coma y tiro el error como debe ser).

Listo, vamos a compilar para atmega32u4 que es popular y tiene multiplicador por hardware.

Archivo divNormal.c:
Código:
//Compilar con: 
//avr-gcc -g -O2 -mmcu=atmega32u4 -c divNormal.c
//avr-gcc -g -mmcu=atmega32u4 -o divNormal.elf divNormal.o

//Desensamblar con:
//avr-objdump -S divNormal.elf > divNormalDisAsm.txt

//Compilar sin optimizar 
//avr-gcc -g -O0 -mmcu=atmega32u4 -c divNormal.c
//avr-gcc -g -mmcu=atmega32u4 -o divNormalO0.elf divNormal.o
//avr-objdump -S divNormalO0.elf > divNormalDisAsmO0.txt

#ifndef F_CPU
#define F_CPU 16000000UL // or whatever may be your frequency
#endif
 
#include <avr/io.h>
#include <stdint.h>

uint8_t dividendo = 148;
uint8_t divisor;

int main(void)
{
    //TODO Lo que requiera el micro para inicializar
    
    divisor = dividendo/17;

    while(1)
    {
        ;
    }
    
    return 0;
}
Archivo divOpt.c:

Código:
//Compilar con: 
//avr-gcc -g -O2 -mmcu=atmega32u4 -c divOpt.c
//avr-gcc -g -mmcu=atmega32u4 -o divOpt.elf divOpt.o

//Desensamblar con:
//avr-objdump -S divOpt.elf > divOptDisAsm.txt

#ifndef F_CPU
#define F_CPU 16000000UL // or whatever may be your frequency
#endif
 
#include <stdint.h>

uint8_t dividendo = 148;
uint8_t divisor;
uint16_t mul;

void main(void)
{
    //TODO Lo que requiera el micro para inicializar
    
    mul = dividendo * 15;
    divisor = high(mul);
    while(1)
    {
        nop();
    }
}
Compilar y desensamblar como dicen los comentarios, queda
Archivo divNormalDisAsm.txt (solo pongo la parte relevante)
Código:
int main(void)
{
    //TODO Lo que requiera el micro para inicializar
    
    divisor = dividendo/17;
  ea:    90 91 00 01     lds    r25, 0x0100
  ee:    81 ef           ldi    r24, 0xF1    ; 241
  f0:    98 9f           mul    r25, r24
  f2:    81 2d           mov    r24, r1
  f4:    11 24           eor    r1, r1
  f6:    82 95           swap    r24
  f8:    8f 70           andi    r24, 0x0F    ; 15
  fa:    80 93 02 01     sts    0x0102, r24
  fe:    ff cf           rjmp    .-2          ; 0xfe <main+0x14>

00000100 <_exit>:
 100:    f8 94           cli

00000102 <__stop_program>:
 102:    ff cf           rjmp    .-2          ; 0x102 <__stop_program>
Archivo divOptDisAsm.txt:

Código:
000000d4 <main>:
uint8_t dividendo = 148;
uint8_t divisor;
uint16_t mul;

void main(void)
{
  d4:    90 91 01 01     lds    r25, 0x0101
  d8:    81 ef           ldi    r24, 0xF1    ; 241
  da:    98 9f           mul    r25, r24
  dc:    81 2d           mov    r24, r1
  de:    11 24           eor    r1, r1
  e0:    82 95           swap    r24
  e2:    8f 70           andi    r24, 0x0F    ; 15
  e4:    80 93 00 01     sts    0x0100, r24
  e8:    ff cf           rjmp    .-2          ; 0xe8 <main+0x14>

000000ea <_exit>:
  ea:    f8 94           cli

000000ec <__stop_program>:
  ec:    ff cf           rjmp    .-2          ; 0xec <__stop_program>
Como se ve el compilador generó código (casi) idéntico.. así que nada, por lo menos me queda el compilador para avr instalado :rolleyes:.
Ni siquiera sirve cuando se compila sin optimización (con O0), así que a llorar a la iglesia :cry:
 
Última edición:
Atrás
Arriba