Dokładna kopia kodu maszynowego działa o 50% wolniej niż funkcja oryginalna

11

Trochę eksperymentowałem z wykonywaniem z pamięci RAM i pamięci flash w systemach wbudowanych. Do szybkiego prototypowania i testowania używam obecnie Arduino Due (SAM3X8E ARM Cortex-M3). Z tego, co widzę, środowisko uruchomieniowe Arduino i bootloader nie powinny mieć tutaj znaczenia.

Oto problem: Mam funkcję ( calc ), która jest napisana w ARM Thumb Assembly. calc oblicza liczbę i zwraca ją. (Środowisko wykonawcze> 1 s dla podanego wejścia) Teraz ręcznie wyodrębniłem zmontowany kod maszynowy tej funkcji i umieściłem go jako nieprzetworzone bajty w innej funkcji. Potwierdzono, że obie funkcje znajdują się w pamięci flash (adres 0x80149 i 0x8017D, tuż obok siebie). Zostało to potwierdzone zarówno poprzez demontaż, jak i kontrolę czasu wykonywania.

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() {

}

Wyjście powyższego programu w celu Arduino Due to:

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

Potwierdzamy więc, że wyniki są równe, a adres podczas działania jest zgodny z oczekiwaniami. Wykonanie ręcznie wprowadzonej funkcji kodu maszynowego jest o 50% wolniejsze.

Demontaż za pomocą arm-none-eabi-objdump dodatkowo potwierdza odpowiednie adresy, rezydencję pamięci flash i równość kodu maszynowego (zwróć uwagę na endianowość i grupowanie bajtów!):

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

Możemy dodatkowo potwierdzić analogicznie przyjętą konwencję połączeń:

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

Mogę wykluczyć to z powodu jakiegoś rodzaju spekulacyjnego pobrania (które najwyraźniej ma Cortex-M3!) Lub przerywa. (EDYCJA: NOPE, nie mogę. Prawdopodobnie jakiś rodzaj pobierania wstępnego) Zmiana kolejności wykonywania lub dodawanie wywołań funkcji pomiędzy nimi nie zmienia wyniku. Co może być tutaj winowajcą?


EDYCJA: Po zmianie wyrównania funkcji kodu maszynowego (wstaw nops jako prolog) otrzymuję następujące wyniki:

+ 16 bitów dla calc2:

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

+ 32bit dla calc2:

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

+ 48 bitów dla calc2:

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

+ 64bit dla calc2:

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

+ 80bit dla calc2:

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

EDYCJA 2: Uruchamia się tylko:

Address: 524617 Res: 3524578: 1102155us

Uruchamiane tylko calc2:

Address: 524617 Res: 3524578: 1102257us

Zmiana kolejności:

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

EDYCJA 3: Dodanie .p2align 4przed etykietą .intylko dla obliczeń , osobne wykonanie:

Address: 524625 Res: 3524578: 1413185us

Zarówno jak w oryginalnym teście:

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

EDYCJA 4: Odwrócenie pozycji we flashu całkowicie zmienia wynik. -> Liniowe pobieranie wstępne?

fscheidl
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
Samuel Liew

Odpowiedzi:

4

Szybkość wykonywania kodu z pamięci flash zależy od liczby cykli oczekiwania i wyrównania kodu dla każdego elementu docelowego gałęzi. W tym i podobnych procesorach, takich jak STM32F103, flash potrzebuje 3 cykli oczekiwania, gdy rdzeń działa z najwyższą częstotliwością. Oznacza to, że każda brana gałąź może zająć od 2 do 5 cykli, co może wpłynąć na całkowity czas pracy.

Aby zrekompensować spowolnienie FLASH, procesory te mają szeroką szynę FLASH i bufor pobierania. SAM3X ma parę 128-bitowych buforów instrukcji, które wydają się być zapełnione wzorcem pobierania wstępnego [1].

Aby zoptymalizować ciasną pętlę, spróbuj dopasować ją do 32-bajtowego bloku kodu i wyrównać go do 16-bajtowej granicy (lub lepiej 32, na wszelki wypadek). Dobrym pomysłem może być również sprawdzenie, czy parametry FLASH są ustawione poprawnie, tzn. Włączone jest pobieranie wstępne, a szerokość magistrali jest ustawiona na 128 bitów w tym MCU. Kopiowanie kodu do pamięci RAM może być opcją, ale jest to uciążliwe i może faktycznie spowolnić, w porównaniu do prawidłowo działających buforów pobierania.

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

AK
źródło