La copia exacta del código de la máquina funciona un 50% más lento que la función original

11

He estado experimentando un poco con la ejecución desde RAM y memoria flash en sistemas integrados. Para la creación rápida de prototipos y pruebas, actualmente estoy usando un Arduino Due (SAM3X8E ARM Cortex-M3). Hasta donde puedo ver, el tiempo de ejecución Arduino y el gestor de arranque no deberían hacer ninguna diferencia aquí.

Aquí está el problema: tengo una función ( calc ) que está escrita en ARM Thumb Assembly. calc calcula un número y lo devuelve. (> 1s de tiempo de ejecución para la entrada dada) Ahora extraje manualmente el código de máquina ensamblado de esa función y lo puse como bytes sin procesar en otra función. Se confirma que ambas funciones residen en la memoria flash (Dirección 0x80149 y 0x8017D, una al lado de la otra). Esto se ha confirmado tanto a través del desmontaje como de una verificación de tiempo de ejecución.

void setup() {
  Serial.begin(115200);
  timeFnc(calc);
  timeFnc(calc2);
}

void timeFnc(int (*functionPtr)(void)) {
  unsigned long time1 = micros();

  int res = (*functionPtr)();

  unsigned long time2 = micros();
  Serial.print("Address: ");
  Serial.print((unsigned int)functionPtr);
  Serial.print(" Res: ");
  Serial.print(res);
  Serial.print(": ");
  Serial.print(time2-time1);
  Serial.println("us");

}

int calc() {
   asm volatile(
      "movs r1, #33 \n\t"
      "push {r1,r4,r5,lr} \n\t"
      "bl .in \n\t"
      "pop {r1,r4,r5,lr} \n\t"
      "bx lr \n\t"

      ".in: \n\t"
      "movs r5,#1 \n\t"
      "subs r1, r1, #1 \n\t"
      "cmp r1, #2 \n\t"
      "blo .lblb \n\t"
      "movs r5,#1 \n\t"

      ".lbla: \n\t"
      "push {r1, r5, lr} \n\t"
      "bl .in \n\t"
      "pop {r1, r5, lr} \n\t"
      "adds r5,r0 \n\t"
      "subs r1,#2 \n\t"
      "cmp r1,#1 \n\t"
      "bhi .lbla \n\t"
      ".lblb: \n\t"
      "movs r0,r5 \n\t"
      "bx lr \n\t"
      ::
   ); //redundant auto generated bx lr, aware of that
}

int calc2() {
  asm volatile(
    ".word  0xB5322121 \n\t"
    ".word  0xF803F000 \n\t"
    ".word  0x4032E8BD \n\t"
    ".word  0x25014770 \n\t"

    ".word  0x29023901 \n\t"
    ".word  0x800BF0C0 \n\t"
    ".word  0xB5222501 \n\t"
    ".word  0xFFF7F7FF \n\t"
    ".word  0x4022E8BD \n\t"
    ".word  0x3902182D \n\t"
    ".word  0xF63F2901 \n\t"
    ".word  0x0028AFF6 \n\t"
    ".word  0x47704770 \n\t"
  );
}

void loop() {

}

La salida del programa anterior en el objetivo de Arduino Due es:

Address: 524617 Res: 3524578: 1338254us
Address: 524669 Res: 3524578: 2058819us

Por lo tanto, confirmamos que los resultados sean iguales y que la dirección durante el tiempo de ejecución sea la esperada. La ejecución de la función de código de máquina ingresada manualmente es 50% más lenta.

El desmontaje con arm-none-eabi-objdump confirma aún más las direcciones respectivas, la residencia de la memoria flash y la igualdad del código de la máquina (¡tenga en cuenta la endianness y la agrupación de bytes!):

00080148 <_Z4calcv>:
   80148:   2121        movs    r1, #33 ; 0x21
   8014a:   b532        push    {r1, r4, r5, lr}
   8014c:   f000 f803   bl  80156 <.in>
   80150:   e8bd 4032   ldmia.w sp!, {r1, r4, r5, lr}
   80154:   4770        bx  lr

00080156 <.in>:
   80156:   2501        movs    r5, #1
   80158:   3901        subs    r1, #1
   8015a:   2902        cmp r1, #2
   8015c:   f0c0 800b   bcc.w   80176 <.lblb>
   80160:   2501        movs    r5, #1

00080162 <.lbla>:
   80162:   b522        push    {r1, r5, lr}
   80164:   f7ff fff7   bl  80156 <.in>
   80168:   e8bd 4022   ldmia.w sp!, {r1, r5, lr}
   8016c:   182d        adds    r5, r5, r0
   8016e:   3902        subs    r1, #2
   80170:   2901        cmp r1, #1
   80172:   f63f aff6   bhi.w   80162 <.lbla>

00080176 <.lblb>:
   80176:   0028        movs    r0, r5
   80178:   4770        bx  lr
}
   8017a:   4770        bx  lr

0008017c <_Z5calc2v>:
   8017c:   b5322121    .word   0xb5322121
   80180:   f803f000    .word   0xf803f000
   80184:   4032e8bd    .word   0x4032e8bd
   80188:   25014770    .word   0x25014770
   8018c:   29023901    .word   0x29023901
   80190:   800bf0c0    .word   0x800bf0c0
   80194:   b5222501    .word   0xb5222501
   80198:   fff7f7ff    .word   0xfff7f7ff
   8019c:   4022e8bd    .word   0x4022e8bd
   801a0:   3902182d    .word   0x3902182d
   801a4:   f63f2901    .word   0xf63f2901
   801a8:   0028aff6    .word   0x0028aff6
   801ac:   47704770    .word   0x47704770
}
   801b0:   4770        bx  lr
    ...

Podemos confirmar aún más la convención de llamada utilizada de forma análoga:

00080234 <setup>:
void setup() {
   80234:   b508        push    {r3, lr}
  Serial.begin(115200);
   80236:   4806        ldr r0, [pc, #24]   ; (80250 <setup+0x1c>)
   80238:   f44f 31e1   mov.w   r1, #115200 ; 0x1c200
   8023c:   f000 fcb4   bl  80ba8 <_ZN9UARTClass5beginEm>
  timeFnc(calc);
   80240:   4804        ldr r0, [pc, #16]   ; (80254 <setup+0x20>)
   80242:   f7ff ffb7   bl  801b4 <_Z7timeFncPFivE>
}
   80246:   e8bd 4008   ldmia.w sp!, {r3, lr}
  timeFnc(calc2);
   8024a:   4803        ldr r0, [pc, #12]   ; (80258 <setup+0x24>)
   8024c:   f7ff bfb2   b.w 801b4 <_Z7timeFncPFivE>
   80250:   200705cc    .word   0x200705cc
   80254:   00080149    .word   0x00080149
   80258:   0008017d    .word   0x0008017d

Puedo descartar que esto se deba a algún tipo de búsqueda especulativa (que aparentemente tiene el Cortex-M3) o interrupciones. (EDITAR: NO, no puedo. Probablemente algún tipo de captación previa) Cambiar el orden de ejecución o agregar llamadas a funciones intermedias no cambia el resultado. ¿Cuál podría ser el culpable aquí?


EDITAR: después de cambiar la alineación de la función de código de máquina (inserte nops como prólogo) obtengo los siguientes resultados:

+ 16 bits para calc2:

Address: 524617 Res: 3524578: 1102257us
Address: 524669 Res: 3524578: 1846968us

+ 32 bits para calc2:

Address: 524617 Res: 3524578: 1102257us
Address: 524669 Res: 3524578: 1535424us

+ 48 bits para calc2:

Address: 524617 Res: 3524578: 1102155us
Address: 524669 Res: 3524578: 1413180us

+ 64bit para calc2:

Address: 524617 Res: 3524578: 1102155us
Address: 524669 Res: 3524578: 1346606us

+ 80bit para calc2:

Address: 524617 Res: 3524578: 1102145us
Address: 524669 Res: 3524578: 1180105us

EDIT2: solo ejecuta calc:

Address: 524617 Res: 3524578: 1102155us

Solo ejecutando calc2:

Address: 524617 Res: 3524578: 1102257us

Cambiar el orden:

Address: 524669 Res: 3524578: 1554160us
Address: 524617 Res: 3524578: 1102211us

EDITAR3: Agregar .p2align 4antes de la etiqueta .insolo para calc, ejecución separada:

Address: 524625 Res: 3524578: 1413185us

Tanto como en el punto de referencia original:

Address: 524625 Res: 3524578: 1413185us
Address: 524689 Res: 3524578: 1535424us

EDITAR4: invertir la posición en flash cambia completamente el resultado. -> Captación previa lineal?

fscheidl
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Samuel Liew

Respuestas:

4

La velocidad de ejecución del código desde flash depende del número de ciclos de espera y la alineación del código para cada objetivo de rama. En este y otros procesadores similares, como STM32F103, el flash necesita 3 ciclos de espera cuando el núcleo funciona a la frecuencia más alta. Esto significa que cada rama tomada puede tomar entre 2 y 5 ciclos, lo que puede afectar el tiempo de ejecución total.

Para compensar la lentitud de FLASH, estos procesadores tienen un bus FLASH ancho y un buffer de recuperación. SAM3X tiene un par de memorias intermedias de instrucciones de 128 bits, que parecen estar pobladas en un patrón de captación previa [1].

Para optimizar un bucle cerrado, intente encajar en un bloque de código de 32 bytes y alinearlo en el límite de 16 bytes (o mejor 32, por si acaso). Además, podría ser una buena idea verificar si los parámetros FLASH están configurados correctamente, es decir, la captación previa está habilitada y el ancho del bus está configurado en 128 bits, en esta MCU. Copiar código a la RAM puede ser una opción, pero es una molestia y en realidad puede ralentizar las cosas, en comparación con los buffers de recuperación que funcionan correctamente.

[1] http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-11057-32-bit-Cortex-M3-Microcontroller-SAM3X-SAM3A_Datasheet.pdf , página 294, Figuras 18-2, 18-3 .

Alaska
fuente