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 .in
pour 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?
Réponses:
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 .
la source