A cópia exata do código da máquina é 50% mais lenta que a função original

11

Venho experimentando um pouco a execução da RAM e da memória flash em sistemas embarcados. Para prototipagem e teste rápidos, atualmente estou usando um Arduino Due (SAM3X8E ARM Cortex-M3). Tanto quanto posso ver, o tempo de execução e o gerenciador de inicialização do Arduino não devem fazer diferença aqui.

Aqui está o problema: Eu tenho uma função ( calc ) escrita em ARM Thumb Assembly. calc calcula um número e o retorna. (> 1s de tempo de execução para a entrada fornecida) Agora extraí manualmente o código de máquina montado dessa função e o coloquei como bytes brutos em outra função. Confirma-se que ambas as funções residem na memória flash (endereço 0x80149 e 0x8017D, um ao lado do outro). Isso foi confirmado via desmontagem e verificação de tempo de execução.

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

}

A saída do programa acima no destino Arduino Due é:

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

Portanto, confirmamos que os resultados são iguais e o endereço durante o tempo de execução é o esperado. A execução da função de código de máquina inserida manualmente é 50% mais lenta.

A desmontagem com arm-none-eabi-objdump confirma ainda os respectivos endereços, residência na memória flash e igualdade do código da máquina (observe endianness e agrupamento 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 ainda a convenção de chamada usada 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

Posso descartar isso devido a algum tipo de busca especulativa (que o Cortex-M3 aparentemente tem!) Ou interrompe. (EDIT: NOPE, não posso. Provavelmente algum tipo de pré-busca) Alterar a ordem de execução ou adicionar chamadas de função entre elas não altera o resultado. Qual poderia ser o culpado aqui?


EDIT: Depois de alterar o alinhamento da função de código da máquina (inserir nops como prólogo), obtenho os seguintes 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

+ 64 bits para o calc2:

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

+ 80 bits para calc2:

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

EDIT2: Apenas executando calc:

Address: 524617 Res: 3524578: 1102155us

Apenas executando o calc2:

Address: 524617 Res: 3524578: 1102257us

Alterando a ordem:

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

EDIT3: Adicionando .p2align 4rótulo anterior .inapenas para calc, execução separada:

Address: 524625 Res: 3524578: 1413185us

Tanto como na referência original:

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

EDIT4: A inversão da posição no flash altera completamente o resultado. -> Pré-busca linear?

fscheidl
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Samuel Liew

Respostas:

4

A velocidade da execução do código a partir do flash depende do número de ciclos de espera e do alinhamento do código para cada destino da ramificação. Neste e em processadores similares, como o STM32F103, o flash precisa de 3 ciclos de espera quando o núcleo é executado na frequência mais alta. Isso significa que cada ramificação tomada pode demorar entre 2 e 5 ciclos, o que pode afetar o tempo total de execução.

Para compensar a lentidão do FLASH, esses processadores possuem um amplo barramento FLASH e um buffer de busca. O SAM3X possui um par de buffers de instruções de 128 bits, que parecem estar preenchidos em um padrão de pré-busca [1].

Para otimizar um loop restrito, tente encaixar-se em um bloco de código de 32 bytes e alinhe-o no limite de 16 bytes (ou melhor, 32, apenas no caso). Além disso, pode ser uma boa ideia verificar se os parâmetros FLASH estão configurados corretamente, ou seja, a pré-busca está ativada e a largura do barramento está definida para 128 bits neste MCU. Copiar o código para a RAM pode ser uma opção, mas é muito trabalhoso e pode realmente atrasar as coisas, em comparação com os buffers de busca que funcionam corretamente.

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

AK
fonte