La copie exacte du code machine s'exécute 50% plus lentement que la fonction d'origine

11

J'ai expérimenté un peu avec l'exécution à partir de RAM et de mémoire flash sur des systèmes embarqués. Pour le prototypage et les tests rapides, j'utilise actuellement un Arduino Due (SAM3X8E ARM Cortex-M3). Pour autant que je puisse voir, le runtime Arduino et le chargeur de démarrage ne devraient pas faire de différence ici.

Voici le problème: j'ai une fonction ( calc ) qui est écrite dans ARM Thumb Assembly. calc calcule un nombre et le renvoie. (> 1 s d'exécution pour l'entrée donnée) Maintenant, j'ai extrait manuellement le code machine assemblé de cette fonction et l'ai mis sous forme d'octets bruts dans une autre fonction. Il est confirmé que les deux fonctions résident dans la mémoire flash (adresses 0x80149 et 0x8017D, l'une à côté de l'autre). Cela a été confirmé à la fois via le démontage et une vérification de l'exécution.

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 sortie du programme ci-dessus sur la cible Arduino Due est:

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

Nous confirmons donc que les résultats sont égaux et que l'adresse pendant l'exécution est comme prévu. L'exécution de la fonction de code machine saisie manuellement est 50% plus lente.

Le démontage avec arm-none-eabi-objdump confirme en outre les adresses respectives, la résidence de la mémoire flash et l'égalité du code machine (notez l'endianité et le regroupement d'octets!):

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
    ...

Nous pouvons en outre confirmer la convention d'appel utilisée de façon analogue:

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

Je peux exclure que cela soit dû à une sorte de fetch spéculatif (que le Cortex-M3 a apparemment!) Ou à des interruptions. (EDIT: NOPE, je ne peux pas. Probablement une sorte de prélecture) Changer l'ordre d'exécution ou ajouter des appels de fonction entre les deux ne change pas le résultat. Quel pourrait être le coupable ici?


EDIT: Après avoir modifié l'alignement de la fonction de code machine (insérer des nops comme prologue), j'obtiens les résultats suivants:

+ 16bit pour calc2:

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

+ 32bit pour calc2:

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

+ 48bit pour calc2:

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

+ 64bit pour calc2:

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

+ 80bit pour calc2:

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

EDIT2: Exécution de calc uniquement:

Address: 524617 Res: 3524578: 1102155us

Exécution de calc2 uniquement:

Address: 524617 Res: 3524578: 1102257us

Modification de la commande:

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

EDIT3: Ajout d'une .p2align 4étiquette avant .inpour calc uniquement, exécution séparée:

Address: 524625 Res: 3524578: 1413185us

Les deux comme dans la référence d'origine:

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

EDIT4: Inverser la position en flash change complètement le résultat. -> Prélecture linéaire?

fscheidl
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew

Réponses:

4

La vitesse d'exécution du code à partir du flash dépend du nombre de cycles d'attente et de l'alignement du code pour chaque branche cible. Dans ce processeur et dans des processeurs similaires, comme le STM32F103, le flash a besoin de 3 cycles d'attente lorsque le cœur fonctionne à la fréquence la plus élevée. Cela signifie que chaque branche prise peut prendre entre 2 et 5 cycles, ce qui peut affecter le temps d'exécution total.

Pour compenser la lenteur FLASH, ces processeurs disposent d'un bus FLASH large et d'un tampon d'extraction. SAM3X possède une paire de tampons d'instructions de 128 bits, qui semblent être remplis dans un modèle de prélecture [1].

Pour optimiser une boucle serrée, essayez de vous adapter à un bloc de code de 32 octets et alignez-le à la limite de 16 octets (ou mieux 32, juste au cas où). En outre, il pourrait être judicieux de vérifier si les paramètres FLASH sont correctement configurés, c'est-à-dire que la prélecture est activée et que la largeur du bus est définie sur 128 bits, dans ce MCU. La copie de code dans la RAM peut être une option, mais c'est pénible et peut en fait ralentir les choses, par rapport à des tampons d'extraction fonctionnant correctement.

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

AK
la source