Notas de estudio del ensamblaje del brazo (7) -ARM9 tubería de cinco etapas y enclavamiento de tubería

Este artículo analiza principalmente el principio de la tubería de cinco etapas y el enclavamiento de la tubería, para que se pueda escribir un código de ensamblaje más eficiente.


1. Tubería de cinco etapas ARM9

ARM7 utiliza una estructura de canalización típica de tres etapas, que incluye tres partes de recuperación, decodificación y ejecución. Entre ellos, la unidad de ejecución realiza una gran cantidad de trabajo, incluidas las operaciones de lectura y escritura de registros y memorias relacionadas con operandos, operaciones de ALU y transmisión de datos entre dispositivos relacionados. Cada una de estas tres fases generalmente toma un ciclo de reloj, pero si tres instrucciones realizan tres etapas de la tubería de tres etapas al mismo tiempo, todavía se puede alcanzar una instrucción por ciclo. Sin embargo, la unidad de ejecución a menudo toma múltiples ciclos de reloj, convirtiéndose así en el cuello de botella del rendimiento del sistema.

ARM9 utiliza un diseño de tubería de cinco etapas más eficiente. Después de recuperar, decodificar y ejecutar, se agregan las etapas LS1 y LS2. LS1 es responsable de cargar y almacenar los datos especificados en la instrucción, y LS2 es responsable de recuperar y firmar la expansión a través de bytes o medias palabras. Los datos cargados por el comando de carga. Pero LS1 y LS2 solo son válidos para los comandos de carga y almacenamiento, otras instrucciones no necesitan ejecutar estas dos etapas. La siguiente es la definición del documento oficial de ARM:

  • Fetch: recupera de la memoria las instrucciones en la dirección pc . La instrucción se carga en el núcleo y luego se procesa en la tubería principal.

  • Decodificar: decodifica la instrucción que se obtuvo en el ciclo anterior. El proceso también lee los operandos de entrada del banco de registro si no están disponibles a través de una de las rutas de reenvío.

  • ALU: ejecuta la instrucción que fue decodificada en el ciclo anterior. Tenga en cuenta que esta instrucción se obtuvo originalmente de la dirección pc - 8 (estado ARM) o pc - 4 (estado Thumb). Normalmente, esto implica calcular la respuesta para una operación de procesamiento de datos, o la dirección para una operación de carga, almacenamiento o sucursal. Algunas instrucciones pueden pasar varios ciclos en esta etapa. Por ejemplo, las operaciones de cambio controladas por multiplicación y registro toman varios ciclos de ALU. 

  • LS1: cargar o almacenar los datos especificados por una instrucción de carga o almacenamiento. Si la instrucción no es una carga o almacenamiento, entonces esta etapa no tiene efecto.

  • LS2: Extrae y amplía con cero o con signo los datos cargados por un byte o una instrucción de carga de media palabra. Si la instrucción no es una carga de un byte de 8 bits o un elemento de media palabra de 16 bits, entonces esta etapa no tiene efecto. 

En la tubería de cinco etapas ARM9, la operación de lectura del registro se transfiere a la etapa de decodificación, y la etapa de ejecución de la tubería de tres etapas se refina aún más, reduciendo la cantidad de trabajo que debe completarse en cada ciclo de reloj, de modo que cada etapa de la tubería pueda funcionar en Es más equilibrado, evitando conflictos de bus entre el acceso a datos y las instrucciones de extracción, y el número promedio de ciclos por instrucción se reduce significativamente.

2. El problema del enclavamiento de la tubería.

Aunque se ha dicho que en las tuberías de tres y cinco niveles, generalmente se puede alcanzar una instrucción por ciclo, pero no todas las instrucciones se pueden completar en un ciclo. Las diferentes instrucciones requieren diferentes ciclos de reloj. Para obtener más información, consulte el Apéndice D: Tiempos de ciclo de instrucciones en el documento oficial de ARM, Guía del desarrollador del sistema de brazo, que no se describirá en detalle aquí. La documentación también se puede encontrar en mis recursos.
Además, diferentes secuencias de instrucciones también causarán diferentes ciclos de reloj. Por ejemplo, la ejecución de una instrucción requiere el resultado de la instrucción anterior. Si el resultado aún no ha salido, entonces debe esperar. Este es el enclavamiento de la tubería.
Tome el ejemplo más simple:
LDR r1, [r2, #4] 
ADD r0, r0, r1
El código anterior requiere tres ciclos de reloj, porque la instrucción LDR calculará el valor de r2 + 4 en la etapa ALU, y la instrucción ADD todavía está en la etapa de decodificación, y este ciclo de reloj no se completa desde [r2, # 4] Elimine los datos de la memoria y vuelva a escribirlos en el registro r1. La ALU de la instrucción ADD deberá usar r1 en el siguiente ciclo de reloj. La fase LS1 de la instrucción se completa antes de pasar a la fase ALU de la instrucción ADD. La siguiente figura muestra el enclavamiento de la tubería en el ejemplo anterior:



Mira el siguiente ejemplo nuevamente:
LDRB r1, [r2, #1] 
ADD r0, r0, r2 
EOR r0, r0, r1
El código anterior toma cuatro ciclos de reloj, porque la instrucción LDRB necesita completar la reescritura en r1 después de que se complete la etapa LS2 (es una instrucción de carga byte byte), por lo que la instrucción EOR necesita esperar un ciclo de reloj. La operación de la tubería es la siguiente:

Mira el siguiente ejemplo nuevamente:

        MOV r1, #1
        B case1
        AND r0, r0, r1 EOR r2, r2, r3 ...
case1:
        SUB r0, r0, r1
El código anterior necesita tomar cinco ciclos de reloj, y una instrucción B toma tres ciclos de reloj, porque cuando se encuentra una instrucción de salto, borrará las instrucciones detrás de la tubería e irá a la nueva dirección para buscar instrucciones nuevamente. La operación de la tubería es la siguiente:


3. Evite el enclavamiento de la tubería para mejorar la eficiencia operativa

La instrucción de carga aparece con mucha frecuencia en el código, y los datos proporcionados en el documento oficial representan aproximadamente un tercio de la probabilidad. Por lo tanto, la optimización de la instrucción de carga y sus instrucciones cercanas puede evitar la aparición de enclavamiento de la tubería, mejorando así la eficiencia operativa.
Mirando el siguiente ejemplo, el código C es convertir las letras mayúsculas en la cadena de entrada a letras minúsculas. Los siguientes experimentos se basan en ARM9TDMI.
void str_tolower(char *out, char *in)
{
  unsigned int c;
do {
    c = *(in++);
    if (c>=’A’ && c<=’Z’)
    {
      c = c + (’a’ -’A’);
    }
    *(out++) = (char)c;
  } while (c);
}
El compilador genera el siguiente código de ensamblaje:
str_tolower
                LDRB r2,[r1],#1        ; c = *(in++)
                SUB r3,r2,#0x41       ; r3=c-‘A’
                CMP r3,#0x19           ; if (c <=‘Z’-‘A’)
                ADDLS r2,r2,#0x20    ; c +=‘a’-‘A’
                STRB r2,[r0],#1         ; *(out++) = (char)c
                CMP r2,#0                 ; if (c!=0)
                BNE str_tolower         ; goto str_tolower
                MOV pc,r14                ; return
Entre ellos (c> = 'A' && c <= 'Z') juicio condicional después de la compilación en el ensamblaje, la variante se convierte en 0 <= c-'A '<=' Z '-' A '.
Se puede ver que cuando el código de ensamblaje anterior LDRB carga caracteres a c, la siguiente instrucción SUB necesita esperar 2 ciclos de reloj más. Hay dos formas de optimizar: precarga y desenrollado.

3.1 Programación de carga mediante precarga

La idea básica de este método es cargar datos al final del ciclo anterior, no al principio del ciclo. El siguiente es el código de ensamblaje optimizado:
out RN 0 ; pointer to output string 
in RN 1 ; pointer to input string
c       RN 2    ; character loaded
t       RN 3    ; scratch register
        ; void str_tolower_preload(char *out, char *in)
        str_tolower_preload
      LDRB    c, [in], #1            ; c = *(in++)
loop
      SUB     t, c, #’A’              ; t = c-’A’
      CMP     t, #’Z’-’A’             ; if (t <= ’Z’-’A’)
      ADDLS   c, c, #’a’-’A’        ;   c += ’a’-’A’;
      STRB    c, [out], #1          ; *(out++) = (char)c;
      TEQ     c, #0                   ; test if c==0
      LDRNEB  c, [in], #1         ; if (c!=0) { c=*in++;
      BNE     loop             ;             goto loop; }
      MOV     pc, lr           ; return
Esta versión del ensamblaje tiene una instrucción más que el ensamblado compilado por el compilador de C, pero ahorra 2 ciclos de reloj, reduciendo el ciclo de reloj de ciclo de 11 a 9 por carácter, la eficiencia es 1.22 veces mayor que la del compilador de C .
Además, el RN es una pseudoinstrucción, utilizada para dar un alias al registro, como c RN 2; es usar c para representar el registro r2.

3.2 Programación de carga desenrollando

La idea básica de este método es expandir el ciclo y luego intercalar el código. Por ejemplo, podemos procesar los tres datos i, i + 1, i + 2. cada ciclo. Cuando no se ha completado la instrucción de procesamiento de i, podemos comenzar el procesamiento de i + 1, de modo que no tengamos que esperar el procesamiento de i Resultó
El código de ensamblaje optimizado es el siguiente:
out     RN 0   ; pointer to output string
in      RN 1   ; pointer to input string
ca0     RN 2   ; character 0
t       RN 3   ; scratch register
ca1     RN 12   ; character 1
ca2     RN 14   ; character 2

	; void str_tolower_unrolled(char *out, char *in)
	str_tolower_unrolled
	STMFD   sp!, {lr}		; function entry
loop_next3
        LDRB    ca0, [in], #1		; ca0 = *in++;
	LDRB    ca1, [in], #1		; ca1 = *in++;
	LDRB    ca2, [in], #1		; ca2 = *in++;
	SUB     t, ca0, #’A’		; convert ca0 to lower case
	CMP     t, #’Z’-’A’
	ADDLS   ca0, ca0, #’a’-’A’
	SUB     t, ca1, #’A’      ; convert ca1 to lower case
	CMP     t, #’Z’-’A’
	ADDLS   ca1, ca1, #’a’-’A’
	SUB     t, ca2, #’A’      ; convert ca2 to lower case
	CMP     t, #’Z’-’A’
	ADDLS   ca2, ca2, #’a’-’A’
	STRB    ca0, [out], #1    ; *out++ = ca0;
	TEQ     ca0, #0           ; if (ca0!=0)
	STRNEB  ca1, [out], #1    ;   *out++ = ca1;
	TEQNE   ca1, #0           ; if (ca0!=0 && ca1!=0)
	STRNEB  ca2, [out], #1    ;   *out++ = ca2;
	TEQNE   ca2, #0		  ; if (ca0!=0 && ca1!=0 && ca2!=0)
	BNE     loop_next3	  ;   goto loop_next3;
	LDMFD   sp!, {pc}	  ; return;
El código anterior es la implementación más eficiente con la que hemos experimentado hasta ahora. Este método requiere solo 7 ciclos de reloj para cada carácter, que es 1.57 veces más eficiente que la versión compilada en C.
Sin embargo, el tiempo de ejecución total de este método es el mismo que el de la versión compilada en C, porque su volumen de código es más del doble que el de la versión compilada en C. Y el código anterior puede estar fuera de los límites al leer caracteres. Aquí es solo para proporcionar un método y una idea de optimización, puede usar este método en la aplicación donde los requisitos de tiempo son estrictos y la cantidad de datos a procesar es relativamente grande.



Publicado 60 artículos originales · Me gusta 44 · Visitas 340,000+

Supongo que te gusta

Origin blog.csdn.net/beyond702/article/details/52232269
Recomendado
Clasificación