En muchos casos, la forma óptima de realizar alguna tarea puede depender del contexto en el que se realiza la tarea. Si una rutina está escrita en lenguaje ensamblador, generalmente no será posible variar la secuencia de instrucciones según el contexto. Como un ejemplo simple, considere el siguiente método simple:
inline void set_port_high(void)
{
(*((volatile unsigned char*)0x40001204) = 0xFF);
}
Un compilador para código ARM de 32 bits, dado lo anterior, probablemente lo representaría de la siguiente manera:
ldr r0,=0x40001204
mov r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]
o quizás
ldr r0,=0x40001000 ; Some assemblers like to round pointer loads to multiples of 4096
mov r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]
Eso podría optimizarse ligeramente en código ensamblado a mano, ya sea:
ldr r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]
o
mvn r0,#0xC0 ; Load with 0x3FFFFFFF
add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]
Ambos enfoques ensamblados a mano requerirían 12 bytes de espacio de código en lugar de 16; este último reemplazaría una "carga" con un "complemento", que en un ARM7-TDMI se ejecutaría dos ciclos más rápido. Si el código se ejecutara en un contexto en el que r0 era no sabe / no importa, las versiones en lenguaje ensamblador serían algo mejores que la versión compilada. Por otro lado, suponga que el compilador sabía que algún registro [por ejemplo, r5] iba a tener un valor que estaba dentro de 2047 bytes de la dirección deseada 0x40001204 [por ejemplo, 0x40001000], y además sabía que algún otro registro [por ejemplo, r7] iba para mantener un valor cuyos bits bajos eran 0xFF. En ese caso, un compilador podría optimizar la versión C del código para simplemente:
strb r7,[r5+0x204]
Mucho más corto y más rápido que incluso el código de ensamblaje optimizado a mano. Además, supongamos que set_port_high ocurrió en el contexto:
int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this
Nada inverosímil cuando se codifica para un sistema embebido. Si set_port_high
está escrito en el código de ensamblaje, el compilador tendría que mover r0 (que contiene el valor de retorno function1
) a otro lugar antes de invocar el código de ensamblaje, y luego mover ese valor nuevamente a r0 después (ya function2
que esperará su primer parámetro en r0), entonces el código de ensamblaje "optimizado" necesitaría cinco instrucciones. Incluso si el compilador no supiera de ningún registro que contenga la dirección o el valor para almacenar, su versión de cuatro instrucciones (que podría adaptar para usar cualquier registro disponible, no necesariamente r0 y r1) superaría al ensamblado "optimizado" versión en lenguaje. Si el compilador tuviera la dirección y los datos necesarios en r5 y r7 como se describió anteriormente, function1
no alteraría esos registros y, por lo tanto, podría reemplazarset_port_high
con una sola strb
instrucción: cuatro instrucciones más pequeñas y más rápidas que el código de ensamblaje "optimizado a mano".
Tenga en cuenta que el código de ensamblaje optimizado a mano a menudo puede superar a un compilador en los casos en que el programador conoce el flujo preciso del programa, pero los compiladores brillan en los casos en que se escribe un fragmento de código antes de que se conozca su contexto, o donde se puede encontrar un fragmento de código fuente invocado desde múltiples contextos [si set_port_high
se usa en cincuenta lugares diferentes en el código, el compilador podría decidir independientemente para cada uno de ellos cuál es la mejor manera de expandirlo].
En general, sugeriría que el lenguaje ensamblador es apto para producir las mayores mejoras de rendimiento en aquellos casos en los que cada fragmento de código puede abordarse desde un número muy limitado de contextos, y es perjudicial para el rendimiento en lugares donde un fragmento de código El código puede ser abordado desde muchos contextos diferentes. Curiosamente (y convenientemente) los casos en que el ensamblaje es más beneficioso para el rendimiento son a menudo aquellos en los que el código es más sencillo y fácil de leer. Los lugares donde el código del lenguaje ensamblador se convertiría en un desastre pegajoso son a menudo aquellos en los que escribir en ensamblaje ofrecería el menor beneficio de rendimiento.
[Nota menor: hay algunos lugares donde el código de ensamblaje se puede usar para producir un desastre pegajoso hiper optimizado; por ejemplo, un fragmento de código que hice para ARM necesitaba recuperar una palabra de RAM y ejecutar una de las doce rutinas basadas en los seis bits superiores del valor (muchos valores asignados a la misma rutina). Creo que optimicé ese código para algo como:
ldrh r0,[r1],#2! ; Fetch with post-increment
ldrb r1,[r8,r0 asr #10]
sub pc,r8,r1,asl #2
El registro r8 siempre contenía la dirección de la tabla de despacho principal (dentro del bucle donde el código pasó el 98% de su tiempo, nada lo usó para ningún otro propósito); Las 64 entradas se refieren a direcciones en los 256 bytes que le preceden. Dado que el ciclo primario tenía en la mayoría de los casos un límite de tiempo de ejecución difícil de aproximadamente 60 ciclos, la recuperación y el despacho de nueve ciclos fue muy instrumental para alcanzar ese objetivo. El uso de una tabla de 256 direcciones de 32 bits habría sido un ciclo más rápido, pero habría engullido 1 KB de RAM muy valiosa [la memoria flash habría agregado más de un estado de espera]. El uso de 64 direcciones de 32 bits habría requerido agregar una instrucción para enmascarar algunos bits de la palabra obtenida, y aún habría engullido 192 bytes más que la tabla que realmente usé. El uso de la tabla de compensaciones de 8 bits produjo un código muy compacto y rápido, pero no es algo que esperaría que un compilador pudiera encontrar; Tampoco esperaría que un compilador dedique un registro "a tiempo completo" para mantener la dirección de la tabla.
El código anterior fue diseñado para ejecutarse como un sistema autónomo; podría llamar periódicamente al código C, pero solo en ciertos momentos cuando el hardware con el que se comunicaba podría ponerse en estado "inactivo" de forma segura durante dos intervalos de aproximadamente un milisegundo cada 16 ms.