@@ Hadix Máquina Ficticia @ Maqueta en assembler de ARM Cortex-M4 @ Actualmente se trata de algo que puede correr dumbfib: @ fib(n) = 1 if n < 2 else fib(n-1) + fib(n-2) @ o en RPN pseudo-forth: @ my.0 lit.2 @ que se mide a 38 BogoMIPS y puede correr a hasta 2.2GHz, @ corre en 221–236ms. Si es 2.2GHz, sería unos 490'000'000 @ ciclos del reloj, unos 42% más de lo predicho arriba. Pero @ quizás el celu está corriendo a una velocidad de reloj @ menor. @ En este momento el código del intérprete es 78 instrucciones @ y 232 bytes, pero eso no cuenta la tabla de bytecodes. .macro make_fib .align 4 fib: @ Estructura de cabecera de subrutina: .byte 1 @ número de argumentos .byte 1 @ número total de variables locales .byte 1 @ número de literales en vector .byte 0 @ byte de alineamiento .long fib @ único item en vector de literales fib_body .endm @ Sin embargo, todavía tenemos que definir las instrucciones. .macro define_16_bytecodes name, high_nibble .equiv nib_\name, \high_nibble .macro i_\name arg .byte nib_\name << 4 | ((\arg) & 0xf) @ XXX verificar que var_idx es menor que 15 .endm .endm .macro define_bytecode_niladico name, byte .equiv b_\name, \byte .macro i_\name .byte b_\name .endm .endm define_16_bytecodes my, 1 define_16_bytecodes lit4, 2 define_16_bytecodes call, 3 .equiv b_bge, 1 .macro i_bge dest .byte b_bge, \dest - . - 1 .endm define_bytecode_niladico ret, 2 define_bytecode_niladico add, 3 define_bytecode_niladico fin, 4 @ Ahora que tenemos las instrucciones definidas podemos @ emitirlo: .data make_fib @ también pongamos el programa: .align 4 programa: .byte 0 @ número de argumentos .byte 0 @ número total de variables locales .byte 1 @ número de literales en vector .byte 0 @ byte de alineamiento .long fib @ único item en vector de literales i_lit4 7 i_lit4 7 i_add i_lit4 7 i_add i_lit4 7 i_add i_lit4 2 i_add i_call 0 i_fin .section .text,"ax",%progbits .syntax unified .thumb .thumb_func .fpu fpv4-sp-d16 .cpu cortex-m4 @ Registros para máquina virtual: @ r3: al entrar un handler de bytecode, es el byte original @ r4: puntero de pila de operandos, apuntando arriba del @ segundo ítem en la pila @ r5: punto de pila de operandos, el primer ítem en la pila @ r6: contador de programa, apuntando a la próxima instrucción @ r7: puntero a tabla de bytecodes @ r8: puntero a pila de devolución @ r9: puntero a vector de literales @ Así despachar al próximo bytecode se ve así: .macro dispatch ldrb r3, [r6], #1 @ cargar próxima instrucción, post-index ldr pc, [r7, r3, lsl #2] @ indexar tabla de bytecodes apuntada por r7 .endm .bss pilas: .fill 1024 .equiv fin_de_pilas, . - 4 .text .global correr_interprete .thumb .thumb_func .type correr_interprete, %function correr_interprete: push {r4-r10, lr} ldr.n r3, =programa ldr.n r4, =pilas ldr r8, =fin_de_pilas ldr.n r7, =bytecode_table b invocar_subrutina_r3 .ltorg handler_add: ldmdb r4!, {r3} @ cargar segundo item de la pila r4 en r3 temporario add r5, r5, r3 @ sumar número cargado al primero en pila dispatch @ Eso resulta en lo siguiente: @ 20: f854 3d04 ldr.w r3, [r4, #-4]! 2 ciclos @ 24: 441d add r5, r3 1 ciclo @ 26: f816 3b01 ldrb.w r3, [r6], #1 2 ciclos @ 2a: f857 f023 ldr.w pc, [r7, r3, lsl #2] 5 ciclos? @ total: 10 ciclos? O más si hay wait states, sobre todo con r6. @ Si se puede meter la pila de operandos y la tabla de handlers en @ memoria rápida, no hace falta hacer eso con el código bytecode. @ 7 de los 10 ciclos son el dispatch. handler_my: @ Tenemos el byte de la instrucción en r3, cuyos 4 bits menos @ significativos son el índice del variable. and r3, #0xf @ Para hacer espacio en la pila tenemos que pushear r5: stmia r4!, {r5} @ Ahora tenemos que indexar el cuadro de pila de devolución: ldr r5, [r8, r3, lsl #2] @ Eso implica que el cuadro de pila de devolución tiene que @ apuntar a los variables locales, no a la dirección de @ devolución. dispatch @ Resultado ensamblado: @ 2e: f003 030f and.w r3, r3, #15 1 ciclo @ 32: c420 stmia r4!, {r5} 2 ciclos @ 34: f858 5023 ldr.w r5, [r8, r3, lsl #2] 2 ciclos @ 38: f816 3b01 ldrb.w r3, [r6], #1 2 ciclos @ 3c: f857 f023 ldr.w pc, [r7, r3, lsl #2] 5 ciclos? @ Eso es 12 ciclos: 7 de dispatch más 5 de trabajo de verdad. @ lit4 se divide en dos handlers: uno para números positivos y @ otro para negativos. handler_lit4_positive: stmia r4!, {r5} and r5, r3, #0xf dispatch handler_lit4_negative: stmia r4!, {r5} orr r5, r3, #0xFfffFff0 dispatch @ Eso termina siendo así: @ 4e: c420 stmia r4!, {r5} 2 ciclos @ 50: f063 050f orn r5, r3, #15 1 ciclo @ 54: f816 3b01 ldrb.w r3, [r6], #1 2 ciclos @ 58: f857 f023 ldr.w pc, [r7, r3, lsl #2] 5 ciclos? @ Otra vez 10 ciclos. handler_bge: @ Consume dos operandos, salta si el primero es mayor o igual ldmdb r4!, {r1, r3} @ creo que r1 tiene el valor no consumido ldrb r2, [r6], #1 @ byte de operando: distancia de salto cmp r3, r5 @ comparar con último operando itt ge @ creo que es la condición correcta? sxtbge r2, r2 @ convertir byte a desplazamiento con signo addge r6, r2 @ sumar al contador de programa mov r5, r1 @ sobreescribir último operando dispatch @ 44: e934 000a ldmdb r4!, {r1, r3} @ 3 ciclos @ 48: f816 2b01 ldrb.w r2, [r6], #1 @ 2 ciclos @ 4c: 42ab cmp r3, r5 @ 1 ciclo @ 4e: bfa4 itt ge @ porque cmp es 16-bit, 0 ciclos @ 50: b252 sxtbge r2, r2 @ 1 ciclo @ 52: 18b6 addge r6, r6, r2 @ 1 ciclo @ 54: 460d mov r5, r1 @ 1 ciclo @ 56: f816 3b01 ldrb.w r3, [r6], #1 @ 5a: f857 f023 ldr.w pc, [r7, r3, lsl #2] @ total 9 ciclos + 5 dispatch = 14 ciclos total handler_ret: @ El formato de la pila de devolución es así: @ ddd dfp dvp my.0 my.1 my.2 my.3 ddd dfp dvp my.0 ... @ ^ @ r8---/ @ Es decir, r8 (el puntero de cuadro de pila) apunta al primer @ variable local, seguidos por los otros variables locales, y @ después el llamador. A direcciones más bajos, encontramos el @ r9 guardado (dvp), el r8 guardado (dfp), y la dirección de @ devolución guardada. ldmdb r8, {r6, r8, r9} dispatch @ 8 ciclos. @ @ Se supone que el efecto en la pila de operandos de dejar un @ único valor de devolución se verifica anteriormente. handler_call: @ Así tenemos que construir ese cuadro de pila, según la @ cabecera de subrutina llamada. Primero sacamos el índice @ del literal: and r3, #0xf ldr r3, [r9, r3, lsl #2] @ dirección de la cabecera de subrutina llamada invocar_subrutina_r3: ldr r10, [r3] @ contenidos de la cabecera uxtb r0, r10, ror #8 @ número de variables locales subs r1, r8, #12 @ dirección después del último variable local cmp r0, #0 beq 2f movs r2, #0 @ inicialicemos variables locales a 0 1: str r2, [r1, #-4]! @ en el bucle decrementamos antes de guardar subs r0, #1 bne 1b 2: @ ahora r1 apunta al primer variable local, r3 apunta a la cabecera, @ r10 tiene sus contenidos, y r2 y r0 están libres. @ después tenemos que sacar argumentos de la pila de operandos @ y guardarlos en la pila; uxtb r0, r10 @ extraer número de args para sobreescribir cmp r0, #0 @ y solo copiar si no es 0 beq 1f mov r2, r1 @ copiar puntero a r2 2: stmia r2!, {r5} @ guardar primer ítem de pila ldmdb r4!, {r5} @ agarrar próximo item de pila. subs r0, #1 bne 2b @ y finalmente terminamos de construir el cuadro: 1: stmdb r1, {r6, r8, r9} @ capaz mejor hacer estas cosas al revés? primero stmdb y @ después ir cargando parámetros y ceros; así se puede quizás @ mantener vivos menos tiempo todos estos valores? mov r8, r1 @ apuntar a nuevo cuadro ya listo uxtb r0, r10, ror #16 @ largo de vector de constantes add r9, r3, #4 @ apuntar a vector de constantes add r6, r9, r0, lsl #2 @ sumar largo de vector para encontrar código dispatch @ Mi estimación de lo arriba es 42 ciclos, suponiendo un caso @ como nuestro fib con 1 variable local que es un argumento. handler_fin: mov r0, r5 @ agarrar un valor de la pila pop {r4-r10, pc} .data .align 4 bytecode_table: @ 0x0X .word 0, handler_bge+1, handler_ret+1, handler_add+1 .word handler_fin+1, 0, 0, 0 .word 0, 0, 0, 0 .word 0, 0, 0, 0 @ 0x1x .rept 16 .word handler_my+1 .endr @ 0x2x .rept 8 .word handler_lit4_positive+1 .endr .rept 8 .word handler_lit4_negative+1 .endr @ 0x3x .rept 16 .word handler_call+1 .endr