Free Pascal Compiler presta?

Recentemente topei com um projeto que pode ser muito interessante. Uma tentativa de criar um sistema operacional “de brinquedo” (nas palavras do próprio autor, um “toyOS”) usando um compilador PASCAL e o NASM. Trata-se desse projeto LuckyOS (eis o blog do autor, Luciano Gonçalez, neste link).

Meu HeinsenbergOS também pretendia ser um toyOS… isso ai em cima não é crítica, é só uma declaração de um fato!

O “sortudo” usa o Free Pascal Compiler (fpc) como compilador base… Acho uma boa idéia, do ponto de vista educacional, já que PASCAL é mais “didático” do que C, por exemplo… Mas, num ambiente de produção o fpc é uma péssima idéia, eis o porquê:

O fpc não é um “compilador”:

Embora o fpc “compile” o código pascal, ele é um tradutor… Ele converte seu código para assembly e o compila com o GAS ou outro assembler (embora eu não tenha conseguido fazer o NASM funcionar com ele)… A tradução em si não é o principal problema. Acontece que em qualquer processo de tradução existem perdas e o fpc não faz um bom trabalho nesse aspecto:

O fpc não cria código confiável!

Eis um exemplo do mesmo código gerado pelo fpc 2.6.4 e pelo GCC 4.9.2, ambos usando a otimização máxima (-O3):

/* test_c.c */
int f(int a, int b) { return a + b; }

E o código em Pascal:

unit test_pas;

interface

function f(a, b : Integer): Integer;

implementation

function f(a, b : Integer): Integer;
begin
  f := a + b;
end;

end.

As duas rotinas foram compiladas com:

$ gcc -O3 -S test_c.c
$ fpc -a -s -O3 test_pas.pas

Eis o resultado (editado e convertido para notação Intel):

;--- test_pas.pas
f:
  mov  ax,di
  movsx rax,ax
  movsx rsi,si
  add rax,rsi
  movsx eax,ax
  ret

;--- test_c.c
f:
  lea eax,[rdi+rsi]
  ret

De cara topamos com um troço esquisito: O fpc está, de fato, usando a convenção de chamada x86-64 definida no padrão POSIX, mas note que os valores Integer têm 16 bits de tamanho (DI e SI são estendidos para 32 bits via MOVSX) e o valor de retorno também (AX é estendido em sinal para EAX)… Isso, é claro, pode ser uma limitação do tipo Integer, mas porque diabos o compilador faz a adição em 64 bits com o sinal estendido? Além das instruções que têm como alvo registradores Rxx serem maiores (o prefixo REX precisa constar da instrução), a extensão para 64 bits é claramente desnecessária.

No código gerado pelo GCC o alvo é EAX. RDI e RSI, usados na indireção, na instrução LEA, não cria prefixos na instrução e o uso de EAX como alvo também não… Assim, a adição é correta e gera a menor instrução possível – e a mais performática!

Alguns de vocês podem questionar o resultado devido ao uso da opção -O3. Em casos raros essa opção pode gerar código maluco. Acontece que, mesmo que usemos a opção mais confiável, -O2, o código gerado pelos compiladores será o mesmo (faça o teste).

Ainda, se substituirmos o tipo int pelo short, na rotina em C, para compatibilizarmos com o tipo Integer de 16 bits, obteremos a mesmíssima rotina em assembly, de test_c.c, acima! O sinal de um short consta no bit 15 e o valor tem 16 bits de tamanho… os 16 bits superiores podem ser ignorados, facilmente!

Eis uma comparação de tamanho das duas funções:

;---- test_pas.pas
00000000 66 89 F8     mov ax,di
00000003 48 0F BF C0  movsx rax,ax
00000007 48 0F BF F6  movsx rsi,si
0000000B 48 01 F0     add rax,rsi
0000000E 0F BF C0     movsx eax,ax
00000011 C3           ret

;----- test_c.c
00000000 8D 04 37     lea eax,[rdi+rsi]
00000003 C3           ret

Note a profusão de prefixos 0x66, 0x48 (REX) e 0x0F na primeira rotina… 40% do tamanho do código é composto apenas de prefixos… Esses prefixos não se encontram na segunda! E, mesmo com o paralelismo da pipeline do processador, a primeira rotina tem o potencial de ser 5 vezes mais lenta que a segunda!

Tá certo que o prefixo 0x0f não pode ser retirado de MOVSX, mas repare que o fpc não se preocupa sequer em remover o prefixo REX (0x48) dos dois primeiros MOVSX… estender AX e SI para EAX e ESI seria suficiente para obter o efeito desejado (bem como usar EAX e ESI na adição, eliminando o último MOVSX!)…

Strings são legais, mas é necessário cuidado!

Uma das vantagens do Pascal é que as rotinas de manipulação de string são incorporadas na linguagem. Basta usar um “+” para concatenar duas strings, por exemplo… Diferente de C, o tipo “string” existe e é um tipo “primitivo”. Como tal, ele pode ser passado por valor ou por referência e o fpc não consegue encarar uma string como um ponteiro muito bem… As duas funções, abaixo, geram códigos bem diferentes:

...
function Append1(s1, s2 : String): String;
begin
  Append1 := s1 + s2;
end;

function Append2(var s1, s2 : String): String;
begin
  Append2 := s1 + s2;
end;
...

Em primeiro lugar, o tipo “string” é limitado em 255 bytes. A primeira rotina aloca 510 bytes na pilha para armazenamento local das strings s1 e s2, faz a cópia de ambas para os respectivos endereços assumindo que ambas têm 255 bytes de tamanho (porque o compilador não tem como saber o tamanho de antemão), as concatena (em até 255 bytes) e retorna. Obviamente que o passo de criar as cópias locais é completamente desnecessário, mas o FPC não é lá muito esperto!

Outro problema é se s1 tiver, por exemplo, 200 caracteres e s2 tiver 100… A string final terá, necessáriamnte, apenas os primeiros 55 bytes de s2 concatenados…

Append1:
  ; Reserva espaço na pilha e guarda RBX.
  sub  rsp,536
  mov  [rsp+528],rbx

  mov  rbx,rdi               ; RDI contém @Result.
  mov  [rsp],rsi             ; RSI = @s1
  mov  [rsp+8],rdx           ; RDX = @s2

  ; Copia string s1 para pilha.
  mov  rsi,[rsp]
  lea  rdx,[rsp+16]
  mov  rdi,255               ; Tenta copiar 255 bytes.
  call FPC_SHORTSTR_ASSIGN

  ; Copia string s2 para pilha.
  mov  rsi,[rsp+8]
  lea  rdx,[rsp+272]
  mov  rdi,255               ; Tenta copiar 255 bytes.
  call FPC_SHORTSTR_ASSIGN

  mov  rdi,rbx              ; @Result
  mov  rsi,255              ; Tenta contactenar, no máximo 255 bytes.
  lea  rdx,[rsp+16]         ; @a (local)
  lea  rcx,[rsp+272]        ; @b (local)
  call fpc_shortstr_concat

  ; Limpa a pilha.
  mov  rbx,[rsp+528]
  add  rsp,536
  ret

A alocação de 536 bytes na pilha é porque o compilador decidiu criar uma variável temporária, local, para armazenar RBX e restaurá-lo depois, além das duas strings de 255 bytes, somado às estruturas do tipo “string” (que contém, além dos caracteres, o tamanho do array e, possívelmente, um contador de referências). Pelo menos essa era a implementação da Borland… De qualquer maneira, o tamanho da string é armazenado junto com a mesma, já que as strings tendem a não serem terminadas com “zero”…

Temos ainda uma alocação “automatica”, como mostrada abaixo, que deveria ser desnecessária!. É possível que alguns bytes sejam subtraidos de RSP, na rotina acima, para manter o alinhamento em qword

A segunda rotina faz o que a primeira deveria fazer…

Assign2:
  sub  rsp,8               ; Pra quê isso, FPC?!
  ; RDI já contém @Result...
  mov  rsi,255
  mov  rdx,rsi
  mov  rcx,rdx
  call fpc_shortstr_concat
  add  rsp,8              ; Pra quê isso, FPC?!
  ret

Note que, em ambas as rotinas, o endereço de retorno da string é informado para a rotina num primeiro parâmetro “escondido” (RDI contém o ponteiro para a string de destino)… No pascal da “Borland” este ponteiro poderia ser referenciado através de uma pseudo variável local chamada Result (que parece não funcionar no dialeto do fpc).

O código gerado é GIGANTE!

Eis um teste simples e, acho, mais simples do que isso é impossível:

/* test_c.c */
#include <stdio.h>
void main(void) { puts("Hello, world!"); }

E o equivalente em pascal:

{ test_pas.pas }
begin
  WriteLn('Hello, world!');
end.

Ao compilar ambos e extirpar as informações de debugging, temos:

$ fpc -O3 test_pas.pas
$ gcc -O3 test_c.c -o test_c
$ strip -s test_pas test_c
$ ls -go
-rwxr-xr-x 1   4416 Jan 11 11:44 test_c
-rw-r--r-- 1     63 Jan 11 11:38 test_c.c
-rwxr-xr-x 1 154584 Jan 11 11:44 test_pas
-rw-r--r-- 1     39 Jan 11 11:38 test_pas.pas

154 kB para um simples “Hello, world!”?! Bem… o detalhe aqui é que fpc não usa, por default, a libc… O GCC usa! Mas a libc é carregada por default pelo próprio kernel, senão por drivers ou utilitários periféricos… Mesmo assim, basta dar uma olhada na imagem binária test_pas pra vermos que um caminhão de código inútil é colocado no executável final…

Outra tentative é compilar o código em pascal usando a opção -XD para linká-lo contra bibliotecas dinâmicas (shared objects), mas o tamanho permanece o mesmo, indicando que as units com um batalhão de código inútil são copiadas no código final.

Name mangling

O fpc também não cria símbolos simples, mesmo quando orientação a objetos não é usada… A função f, acima é nomeada TEST_PAS_F$SMALLINT$SMALLINT$$SMALLINT. Aparentemente TEST_PAS_ é o nome da unit, F o da função seguida dos tipos dos parâmetros (SMALLINT) e, o último, precedido de $$, o tipo de retorno. Não há diferenciação se os parâmetros são passados por referência ou valor, o nome continua o mesmo. Também não há diferença se temos uma function ou uma procedure. Só o último $$ muda para um $ simples, indicando que não há retorno.

Claro que isso é melhor que a loucura dos nomes criados pelo compilador C++, mas a necessidade de “amalucar” o nome, numa linguagem essencialmente estruturada, me parece exagero…

 

Anúncios