Um array é um conjunto de valores posicionados na memória de maneira sequencial. Percorrer os elementos de um array implica, pura e simplesmente, em acessar essas posições memória.
Esse conceito, aliás, é bem explícito em C++.
#include <iostream> using namespace std; extern "C" int compute_sum_asm(const int* values, const int count); int compute_sum(const int* values, const int count) { auto result = 0; for (auto i = 0; i < count; i++) { result += *values; values++; } return result; } int main() { const int values[] { 4, 7, 9, 12, 23, 18, 45 }; const int count = sizeof(values) / sizeof(int); cout << compute_sum(values, count) << endl; cout << compute_sum_asm(values, count) << endl; return 0; }
No exemplo, escrito de maneira bem primitiva, vemos que a função recebe dois parâmetros. O primeiro, é um ponteiro para o primeiro elemento no array. O segundo, é um contador indicando quantos elementos o array possui.
A implementação, em Assembly para x86 não se afasta muito do que já fizemos até aqui.
.model flat, c .code compute_sum_asm proc push ebp mov ebp, esp push ebx xor eax, eax mov ebx, [ebp + 8] mov edx, [ebp + 12] cmp edx, 0 jle done lp: add eax, [ebx] add ebx, 4 dec edx jnz lp done: pop ebx pop ebp ret compute_sum_asm endp end
Para X64, entretanto, essa implementação precisaria ser um pouco diferente. Com o número ampliado de registradores (todos que começam com “r” são versões 64 bits), a convenção os utiliza sempre que possível reduzindo a utilização da Stack. Como registradores são os elementos de memória mais velozes disponíveis, o resultado é um incremento de performance.
Convenções para chamada de funções em C++ 64 bits
A convenção para chamada de funções em C/C++ X64 se beneficia do número maior de registradores disponível nessa arquitetura. Assim:
- Os primeiros quatro parâmetros inteiros ou ponteiros são passados através dos registradores rcx, rdx, r8, e r9.
- Os primeiros quatro argumentos com ponto-flutuante são passados nos registradores SSE xmm0–xmm3.
- A função que está “chamando” é responsável por reservar espaço na stack para os argumentos passados como parâmetros. Assim, a função “chamada” pode usar este espaço para “copiar” os valores dos registradores na stack.
- Parâmetros adicionais são passados via stack.
- O retorno deve acontecer em rax (lembrando que eax corresponde aos bits menos significativos desse registrador). Retornos com ponto flutuante devem ser retornados em xmm0.
- rax, rcx, rdx, r8–r11 são voláteis (não é necessário garantir que seus valores sejam restaurados quando a função retorna).
- rbx, rbp, rdi, rsi, r12–r15 não são voláteis..
Fonte: Microsoft
Essas convenções, por interoperabilidade, também são respeitadas pelo .NET
No exemplo desse post, como estamos passando um ponteiro e um inteiro, respectivamente, os registradores utilizados nas passagens de parâmetros são rcx e rdx.
.code compute_sum_asm proc xor eax, eax cmp edx, 0 jle done lp: add eax, [rcx] add rcx, 4 dec edx jnz lp done: ret compute_sum_asm endp end
Os cuidados no projeto dos compiladores em aproveitar as características da arquitetura de execução são expressão de “design caprichoso”.