Adicionando uma imagem SVG no LaTeX

Últimamente tenho mexido mais com \LaTeX do que com ferramentas como o LibreOffice para editar textos. Uma das coisas que sentia falta, era a habilidade de inserir uma imagem no texto, de uma forma simples. Well… não tema, tem um jeito muito simples:

Em primeiro lugar, além de instalar o pacote texlive-latex-base, é interessante instalar também o texlive-latex-extra. O primeiro instala uma série de pacotes (latex, pspicture, babel, graphics…) e o segundo, instala outros pacotes interessantes para o \LaTeX, entre eles, o svg. Esse último usa o inkscape para obter um arquivo PDF, compatível com o pdflatex, para inserir a figura no seu texto. Tudo o que você precisa fazer, antes de declarar o início do documento, é usar o pacote. Por exemplo:

% report.tex
\documentclass[10pt,a4paper,twoside]{report}
\usepackage[utf8]{inputenc}
\usepackage[brazil]{babel}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{graphicx} % Usado para outros tipos de imagens
\usepackage{float} % Usado para posicionamento de imagens
\usepackage{svg}  % Eis o pacote que queremos.

\author{Frederico Lamberti Pissarra}
\title{Relatório de coisa alguma}

\begin{document}
  \maketitle
  \tableofcontents

  \include{prefacio.tex}
  \include{capítulo1.tex}
  % ... inclui outros textos aqui.

\end{document}

Como você pode perceber neste exemplo, trata-se de um relatório com tamanho A4 com páginas de frente-e-verso. Também habilito o uso de utf-8 no texto e uso textos automatizados (“Capítulo”, “Sumário” etc – que são colocados por comandos) em PT-BR. Os pacotes amsmath, amsfonts e amssymb estão ai para uso de fórmulas e, finalmente, uso o pacote svg.

Num dos arquivos que são incluidos, onde quero colocar a imagem, faço:

\begin{figure}[H]
  \centering   % Para colocar a imagem no meio do "parágrafo"...
  \includesvg{image}  % Coloca a imagem 'image.svg' aqui.
  \caption{Figura de exemplo}  % Poe um texto na parte de baixo da figura.
  \label{fig:fig1}  % Coloca uma marca de referência nessa figura.
\end{figure}

Existem outras customizações que podem ser feitas ai, mas colocar uma imagem é apenas incluir o arquivo SVG usando o comando \includesvg, dentro de um bloco de figura. E é só isso… quase… Note a opção “[H]” no environment da figura. Isso ai funciona graças ao pacote float. Sem isso a imagem vai “flutuar” em posições que não correspondem ao ponto exato onde você quis colocar a figura. Isso é especialmente válido para imagens que não sejam SVG. Outro ponto que deixei fora, na listagem acima, é que \includesvg, assim como \includegraphic permite uma lista de argumentos opcionais. A inclusão do SVG, por exemplo, poderia ter as seguintes opções:

\includesvg[width=0.8\linewidth]{image}

Aqui, a largura da imagem foi redimensionada para 80% do tamanho de uma linha.

O “quase”, dito acima, também se refere à configurações do interpretador do arquivo \LaTeX. Por exempplo, o utilitário pdflatex vai chamar o inkscape para converter o arquivo SVG para PDF, precisamos dizer ao comando que ele pode instanciar processos filhos. Isso é feito assim:

$ pdflatex -shell-escape -interaction=nonstopmode report.tex

O comando acima criará o arquivo report.pdf, mas também criará image.pdf e image.pdf_tex. Esses dois últimos são a conversão do SVG para PDF (depois do PDF final, criado, podem ser apagados). Alguns programas, como o TeXStudio, adicionam a opção -synctex=1 ao pdflatex. Não achei referências a ela.

Uma demonstração da técnica que mostrei antes…

Finalmente botei as mãos em “Avengers Endgame” pra assistir (meio “torrado”, se é que me entendem). Mas, o arquivo tem 5.8 GiB de tamanho com um bitrate de quase 4 Mb/s (para um vídeo em 720p) e ainda áudio Dolby (5.1 canais) com bitrate de 384 kb/s:

$ ls -goh *.avi
-rw-rw-r-- 1 5,8G jul 29 20:59 Avengers.Endgame.2019.720p.WEB-DL.XviD.AC3-FGT.avi

$ VNAME="Avengers.Endgame.2019.720p.WEB-DL.XviD.AC3-FGT"

$ for i in Width Height FrameRate BitRate; do \
  echo -n "${i}: "; \
  mediainfo --inform="Video;%${i}%" "${VNAME}.avi"; \
done

Width: 1280
Height: 536
FrameRate: 23.976
BitRate: 3985735

$ mediainfo --inform="Audio;%BitRate%" "${VNAME}.avi" 
384000

Podemos fazer melhor, huh? Calculando o bitrate do vídeo, de acordo com minha formuletinha:

\displaystyle V_{bitrate}=\frac{1280 \cdot 536 \cdot 23.976}{13.824}=1189920\;b/s

Ou seja, cerca de 1.2 Mb/s… Reduzindo o bitrate do áudio para 128 kb/s e somente com 2 canais (stereo) porque não tenho equipamento para “tocar” som surround, temos:

$ ffmpeg -v quiet -stats -hwaccel cuvid -i "${VNAME}.avi" \
  -c:v h264_nvenc -maxrate 1189920 -bufsize 1189920 -b:v 1189920 \
  -c:a ac3 -b:a 128k -ac 2 \
  "${VNAME}.mp4"

$ rename 's/XviD/H264/' *.mp4

Depois de 18 minutos de codificação (note que estou usando CUDA e o encoder da nVidia para h264, mas minha placa gráfica é uma GT630 e consigo uma taxa de velocidade de conversão de 10x – Em testes, feitos na máquina de meu irmão, com uma GTX 1070, obtivemos 70x! Ou seja a conversão demoraria pouco mais de 2 minutos!):

$ ls -goh *.{avi,mp4}
-rw-r--r-- 1 1,7G jul 29 21:36 Avengers.Endgame.2019.720p.WEB-DL.H264.AC3-FGT.mp4
-rw-rw-r-- 1 5,8G jul 29 21:18 Avengers.Endgame.2019.720p.WEB-DL.XviD.AC3-FGT.avi

Nada mau, huh? Um arquivão de 5.8 GiB ficou com 1.7 GiB (cabe num pendrive com FAT32 tranquilo). E sem perder qualidade (que eu possa perceber!)… A imagem comparativa, abaixo, não faz justiça (não é o mesmo quadro – embora alguma perda na faixa dinâmica pareça estar acontecendo, não está!), mas dá uma ideia:

Quebrando a cabeça com SSE…

… tentando ajudar um sujeito num forum gringo, que queria separar a parte inteira e “fracionária” de um float, em assembly, ofereci o seguinte fragmento de código como possibilidade:

; cvt.asm (NASM).
bits 64

section .text

; void f( float x, float *i, float *fr );
; xmm0 = x
; rdi = i (pointer)
; rsi = fr (pointer)
global f
f:
  cvtss2si eax,xmm0
  cvtsi2ss xmm1,eax  ; xmm1 = parte inteira.
  subss xmm0,xmm1    ; xmm0 = parte fracionária.
  movss dword [rdi],xmm1
  movss dword [rsi],xmm0
  ret

O troço parecia funcionar em uns poucos testes que fiz com o seguinte programa:

/* test.c */
#include <stdio.h>

extern void f( float, float *, float * );

int main( void )
{
  float x = 2.25;
  float i, fr;

  f( x, &i, &fr );
  printf( "%g -> Integer: %g; Fraction: %g\n", x, i, fr );
}

Compilando e executanto:

$ cc -O2 -c -o test.o test.c
$ nasm -f elf64 -o cvt.o cvt.asm
$ cc -o test test.o cvt.o
$ ./test
2.2 -> Integer: 2; Fraction: 0.25

Mas, altere x para 2.94 e você verá que o resultado será esquisito:

$ ./test
2.94 -> Integer: 3; Fraction: -0.059999

O interessante é que a mesma rotina, escrita em C, funciona:

void f( float x, float *i, float *fr )
{
  int t = x;
  *i = t;
  *fr = x - t;
}

Qual é o problema?! Eis a parte de “quebrar a cabeça”!! O código acima cria algo como:

f:
  cvttss2si eax,xmm0
  pxor xmm1,xmm1     ; O GCC poe isso aqui (à toa!).
  cvtsi2ss xmm1,eax  ; xmm1 = parte inteira.
  subss xmm0,xmm1    ; xmm0 = parte fracionária.
  movss dword [rdi],xmm1
  movss dword [rsi],xmm0
  ret

Notou a diferença além do pxor que poderia não estar lá!? Não? Deixa eu ressaltá-la pra você:

  cvttss2si eax,xmm0

Viu o ‘t’ adicional na instrução? É que cvtss2si (sem o ‘t’ adicional) arredonda o valor. Como a conversão é para inteiro, o arredondamento é feito na primeira “casa decimal”. Qualquer coisa maior que 2.5 se tornará 3… A instrução cvttss2si (com o ‘t’ adicional) realiza a conversão truncando o valor. Que é justamente o que queremos.

Uma porcariazinha de um caracter me deixou horas quebrando a cabeça depurando essa merdinha de código… puts!

Decodificando e codificando h264 via hardware (nVidia)

Yep… o ffmpeg suporta!

Para decodificar vídeos usando CUDA, adicione a opção -hwaccel cuvid antes de informar o(s) arquivo(s) de entrada (com a opção -i e, no codec de vídeo, use h264_nvenc. Repare que no processo de codificação aparece um status speed= nx. Essa é a velocidade média de conversão. Com o codec libx264 e a decodificação puramrnte em software, consigo aqui cerca de 0.9x. Com as modificações acima, quase 20x. Ou seja, um vídeo de 1 hora, é convertido em 3 minutos!

No meu caso, minha placa-de-vídeo é bem velha: Uma GT630 (chip Tegra). Em placas mais recentes, com drivers mais recentes (GTX série 1000 ou RTX série 2000), a velocidade pode aumentar um bocado – e essas placas conseguem codificar, por hardware, inclusive, h265!

PS: Infelizmente isso ai em cima só funciona com nVidia. Existe o codec h264_vaapi, que usa recursos de hardware para quem tem vídeo on-board Intel (GPU) e opções audo, vaapi e qsv para usar paralelismo em processadores/GPUs Intel, mas isso depende de instalação de drivers (que, dependendo da distro, podem não suportar)… Pelo menos, não consegui fazer funcionar numa de minhas máquinas…

Suponha que você queira converter um vídeo de 2h, com resolução de 720p @ 30Hz:

$ ffmpeg -hwaccel cuvid -i videoin.mp4 \
   -c:v h264_nvenc -b:v 2M -maxrate 2M -bufsize 2M \
   -c:a ac3 -b:a 128k -ac 2 \
   videoout.mp4

Ahhh… os fatores multiplicativos, nas opções -b:x, -maxrate e -bufsize são sempre maiúsculos (exceto ‘k’, que pode ser minúsculo!). Assim, 2 Mb/s é escrito como 2M (nunca ‘2m’) e eles aceitam valores “quebrados” ou expressões: 980k pode ser escrito como 0.98M, sem problemas…

Cálculo empírico do melhor valor de bitrate para compressão de vídeos

Na minha obsessão compulsiva por minimizar as coisas, de tempos em tempos tento aumentar, ao máximo, o espaço livre em minhas unidades de armazenamento. Um dos tipos de arquivo que consomem um espaço considerável em disco são os vídeos e, para diminuir esses tamanhos existem algumas coisas que você pode fazer:

  1. Usar codecs mais eficientes;
  2. Diminuir a resolução gráfica;
  3. Diminuir o framerate;
  4. Diminuir o bitrate.

No primeiro caso, o codec de vídeo mais eficiente (que cria arquivos mais “compactados”), atualmente, é o H.265 (também chamado de HEVC [High Efficiency Video Codec]), mas há um problema: Nem todo dispositivo suporta esse codec e, quando suporta, nem todos suportam com aceleração de hardware. Assim, o segundo codec mais eficiente, atualmente, é o H.264 (também chamado de AVC [Advanced Video Codec]). Então, não considero a primeira possibilidade muito interessante (a não ser que você só assista seus vídeos no computador e tenha uma placa de vídeo mais “moderna”, como a nVidia GTX1070 em diante, por exemplo.

O segundo caso, diminuir a resolução, não é realmente desejável… O terceiro pode implicar em problemas com sincronismo de fontes externas ao vídeo, como legendas, por exemplo, mas é uma boa ideia limitar o framerate em até 30 fps, já que a maioria das TVs não são capazes de taxas maiores que essa.

O quarto case é a taxa de transferência entre o stream de vídeo e o dispositivo. Em teoria, quanto maior a taxa, melhor a qualidade. Se for muito pequeno o dispositivo não terá alternativa senão fazer um “downsampling”, diminuindo a qualidade do vídeo. Se for muito alta, ele fará um “oversampling”, tentando aumentar a qualidade (o que muitas vezes só pode ser feito por interpolação – que, possivelmente, adicionará “artefatos” nos frames, piorando a qualidade!). Então, temos que achar um valor ideal, mínimo, para garantir a qualidade de cada frame e ter uma taxa de transferência boa. Infelizmente, a fórmula para o cálculo desse bitrate é empírica e pode depender do codec em uso. Eu achei uma fórmula ideal para as minhas necessidades:

\displaystyle vbr=\frac{width\cdot height\cdot framerate}{13824}\quad\texttt{[in kb/s]}

Eu poderia te dizer que esse valor 13824 é um valor obtido a partir de alguma relação esquisita entre o tamanho de um pixel (em bits), o aspect ratio e mais um monte de propriedades do vídeo, mas eu estaria mentindo… O fato é que eu acho que um bitrate de 2 Mb/s (2000 kb/s) para uma resolução de 720p, num framerate de 30 Hz, funciona muito bem e extrapolei a relação para qualquer outra resolução.

Assim, um pequenino script para calcular o bitrate de um vídeo:

#!/bin/bash
# ovbr.sh

if [ -z "$1" ]; then
  echo -e "\e[33;1mUsage\e[m: `basename $0` <video>\n\
\n
  Calculates the optimal bitrate, based on video resolution."
  exit 1
fi

W=`mediainfo --Inform="Video;%Width%" "$1"`
H=`mediainfo --Inform="Video;%Height%" "$1"`
F=`mediainfo --Inform="Video;%FrameRate%" "$1"`

# 13824 - empirical value (1280x720@30Hz -> 2000k bitrate)
BR="`bc <<< "scale=0; ($W * $H * $F ) / 13824"`"

echo "${BR}k"
exit 0

Quanto ao áudio, eu prefiro manter padronizado em cerca de 64 kb/s por canal. Assim, supondo que um vídeo tenha áudio e esse não seja monofônico, posso convertê-lo para um tamanho razoável usando:

$ BR=$(ovbr.sh video.mp4)
$ ffmpeg -i video.mp4 \
  -c:v libx264 -maxrate $BR -bufsize $BR -b:v $BR \
  -c:a ac3 -b:a 128k -ac 2 video_out.mp4

Você pode adicionar a opção -async 1 se quiser sincronizar o áudio, mas, como não estamos mudando nada a não ser o bitrate, isso não é necessário. Se o vídeo tiver um framerate maior que 30, ai sim é prudente a opção junto -r 30 para mudar o taxa de frames.

O problema do tratamento de exceções em ponto flutuante

Recentemente topei com uma pegunta interessante… O padrão ISO 9989:1999 (e o de 2001) incorporaram funções para lidar com o “ambiente” de ponto flutuante via protótipos de funções contidas no header fenv.h. Isso, literalmente, torna obsoleto o mecanismo que usava a função matherr() — isso é particularmente verdadeiro para a glibc 2.27 em diante.

No mecanismo antigo, se a função matherr() estiver definida em seu programa, todas as exceções ocorridas em ponto flutuante são repassadas para ela, como se fosse um tratador de sinal. Isso não funciona mais assim. Agora, temos que usar fenv.h e suas funções… Eis um exemplo:

/* Define _GNU_SOURCE para podermos usar
   as extensões feenableexcept() e fedisableexcept() */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <fenv.h>

/* declara o tratador de SIGFPE */
static void myFPEhandler(int);

int main(void)
{
  double a, b, c;

  // Lê os valores de stdin.
  scanf( "%lg %lg", &a, &b );

  signal( SIGFPE, myFPEhandler );

  // Zera os flags indicadores de exceção.
  feclearexcept( FE_ALL_EXCEPT );

  // Tira a máscara da exceção de divisão por zero.
  feenableexcept( FE_DIVBYZERO );

  c = a / b;

  printf( "%g / %g = %g\n", a, b, c );
}

void myFPEhandler(int sig)
{
  fputs( "Math exception!\n", stderr );
  exit( sig + 128 );
}

Se, depois de compilarmos, tentarmos fazer:

$ ./test <<< '1 0'
Math exception!

E o troço parece funcionar perfeitamente! Mas, temos um problema: Não é possível testar qual exceção ocorreu!

No exemplo acima, retiramos a máscara da exceção de divisão por zero, mas poderíamos “desmascarar” todas as exceções, com feenableexcept(FE_ALL_EXCEPT);. Acontece que a interrupção tratada SIGFPE colocará todas as máscaras no lugar (“re-setadas” ou “setadas de novo”) e limpará os flags indicadores de exceção!!! Se tentarmos criar um tratador como:

void myFPEhandler(int sig)
{
  int except = fetestexcept( FE_ALL_EXCEPT );
  fprintf( stderr, "Exception %#x ", except );
  if ( except & FE_DIVBYZERO )
    fputs( "Division by zero!\n", stderr );
  else
    fputs( "Other execption!\n", stderr );
  exit( sig + 128 );
}

Obteremos:

$./test <<< '1 0'
Exception 0x0 Other exception!

O fato de fetestexcept() retornar 0 mostra o que eu disse antes. Você pode achar que isso é um problema da glibc, mas não é… Tente com o código abaixo:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <x86intrin.h>

/* declara o tratador de SIGFPE */
static void myFPEhandler(int);

int main(void)
{
  double a, b, c;

  // Lê os valores de stdin.
  scanf( "%lg %lg", &a, &b );

  signal( SIGFPE, myFPEhandler );

  // Tira a máscara da exceção de Divisão por zero.
  _mm_setcsr( _mm_getcsr() & ~(1 << 9) );

  c = a / b;

  printf( "%g / %g = %g\n", a, b, c );
}

void myFPEhandler(int sig)
{
  int except = _mm_getcsr();

  fprintf( stderr, "%#x: ", except );
  if ( except & 4 )
    fputs( "Division by zero!\n", stderr );
  fputc( '\n', stderr );
  exit( sig + 128 );
}

Aqui você obterá:

./test <<< '1 0'
0x1f80: 

Note que o bit 9 de MXCSR está setado (a máscara de divisão por zero) e os 5 bits inferiores estão zerados (os flags indicadores de exceção)… Aqui estou lidando diretamente com SSE, usando as funções intrínsecas _mm_getcsr() e _mm_setcsr(), que o compilador substituirá pelas instruções STMXCSR e LDMXCSR, respectivamente (sem “chamadas” para a glibc), e a mesmíssima coisa acontece (a palavra de controle MXCSR é reajustada pelo sistema operacional ao chamar o tratador de sinal do processo).

Como o kernel “re-seta” as máscaras, elas devem ser desabilitadas de novo dentro do tratador, se você quiser continar tratando as exceções. Isso é simples… mas, o detalhe é que os flags que indicam qual exceção ocorreu também são zerados, antes do tratador ter a chance de obtê-los, e esse problema parece ser incontornável… É possível que exista alguma configuração lá em sys.conf para mudar isso, mas não é prudente fazê-lo. O kernel se comporta dessa maneira para manter o estado do co-processador matemático (80×87) ou SIMD em um padrão coerente (isso está na especificação SysV).

Dicas sobre criação de funções

Uma função, em C ou em qualquer outra linguagem, é o bloco primordial de qualquer programa. A ideia é que cada pedacinho do seu programa seja dividido em blocos menores, com comportamentos bem definidos, que possam ser reusados. Mas, é claro, existem regras para criá-las… Sua imaginação pode criar algumas funções bem doidas (e, de tempos em tempos, bem interessantes do ponto de vista da análise) e esse artigo não pretende limitar a forma delas, mas mostrar algumas técnicas que podem melhorar um bocado a confecção e performance.

Conheça como seu compilador/sistema operacional “passa” argumentos para funções

Para poder gerar código eficiente, o compilador e/ou o sistema operacional impõem algumas regras simples (bem… às vezes nem tão simples assim) para passar argumentos para uma função. Isso é chamado de convenção de chamada e existem várias. Aqui vou exemplificar apenas as convenções usadas na família x86, mas saiba que cada processador usa a sua:

  • Sistemas operacionais de 32 bits (modo i386)Passagem de argumentos usando a pilha é, de longe, o mais usado nesse modo. A ideia é empilhar os argumentos e pular para o ponto de entrada da função. No caso de C, a convenção cdecl empilha os argumentos do último para o primeiro:
    ;int f( int x, int y ) { return x / y; }
    f:
      xor  edx,edx
      mov  eax,[esp+4]  ; Lê x
      mov  ecx,[esp+8]  ; Lê y
      idiv ecx          ; EAX = EDX:EAX / ECX
      ret

    Como a pilha “cresce” para baixo e a instrução CALL colocará o endereço de retorno na pilha, ESP aponta para a posição da pilha onde esse endereço de retorno está. A posição ESP+4 é a posição onde o último argumento empilhado está (x), logo ESP+8 é onde o argumento à direita (o “último”) está.

    Outro detalhe da convenção cdecl é que a responsabilidade da “limpeza” da pilha, uma vez que função chamada retorne, é da própria função chamadora.

    Esse sistema não é dos mais eficientes porque a rotina chamadora da função f() tem que gravar, na pilha, os argumentos, pular para a função e esta, depois tem que ler, de volta, os argumentos para o interior de registradores. Um exemplo de chamada seria esse:

    ; y = f(10,2);
      sub  esp,8   ; Reserva espaço para 2 argumentos na pilha.
      mov  dword [esp+4],2  ; empilha 2
      mov  dword [esp],10   ; empilha 10
      call f
      add  esp,8   ; Dealoca o espaço usado pelos argumentos na pilha.
                   ; Note que a limpeza é feita DEPOIS que f() retorna.
      ; y, no nosso caso, é o próprio EAX.

    Aqui você pode se perguntar porque diabos não usei as instruções push 2 e push 10, na sequência. Acontece que cada push decrementará ESP e depois escreverá na nova posição o valor (sempre 4 bytes). O esquema acima, subtrai 8 de ESP (fazendo, de uma só vez, o que ambos os push fariam) e escreve diretamente na pilha. É mais eficiente.

    Por que cdecl usa o empilhamento do último para o primeiro argumento? Para permitir número variável de argumentos:

    int f(int x, ... );

    Se chamarmos a função acima assim:

    y = f( 10, 1, 2, 3, 4, 5 );

    O primeiro argumento empilhado será o 5, depois o 4 e assim por diante… O argumento 10 estará, necessariamente, no topo da pilha.

    Uma convenção semelhante é a pascal, que tem duas características diferentes: Ela empilha da esquerda para a direita e a responsabilidade da limpeza da pilha é da função chamada, não da chamadora. A mesma função f() que definimos antes, ficaria assim:

    ; int pascal f( int x, int y ) { return x / y; }
    f:
      mov eax,[rsp+8]  ; pega x
      mov ecx,[rsp+4]  ; pega y
      xor edx,edx
      idiv ecx
      ret 8            ; Retorna, mas faz ESP = ESP+8,
                       ; depois de desempilhar o endereço de
                       ; retorno.

    A desvantagem da convenção pascal é que número variável de argumentos é mais difícil de implementar. Interessnte notar que essa convenção não está presente nos compiladores modernos e era bem comum na época em que o Turbo Pascal era rei… Os compiladores C e C++ da Borland o suportavam, por motivos óbvios. A Microsoft, para manter compatibilidade com as versões iniciais do Windows, usa uma convenção similar, chamada stdcall. Ela é, essencialmente, a convenção pascal onde a responsabilidade pela limpeza da pilha é da função chamadora, assim como em cdecl.

    Felizmente, para o modo i386, existem outros meios de passar argumentos. Uma convenção que existe desde os antigos Microsoft C 6.0 e Borland C++ 3.1 with Application Frameworks é a fastcall. Nessa convenção os registradores ECX e EDX são usados para passagem dos 2 primeiros argumentos e o restante, se existirem, são passados pela pilha na ordem da convenção cdecl. A mesma rotina f(), acima, ficaria assim:

    ; int fastcall f( int x, int y ) { return x / y; }
      mov  eax,ecx
      mov  ecx,edx
      xor  edx,edx
      idiv ecx
      ret

    Repare que, além das instruções CALL e RET não há qualquer manipulação da pilha, acelerando um bocado a rotina. E a chamada fica bem simples:

    ; y = f(10,2);
      mov  ecx,10
      mov  edx,2
      call f
      ; y, no nosso caso, é o próprio EAX.

    De novo, sem o uso da pilha além do CALL.

    Mas, e se quiséssemos usar mais um argumento? A convenção fastcall só permite o uso de dois registradores como argumentos. No caso de compiladores como o GCC existe uma extensão, um atributo, para funções que permite o uso de até 3 argumentos alocados em registradores:

    ; __attribute__((regparm(3)))
    ; int f( int x, int y, int z ) { return x * y + z; }
    f:
      imul eax,edx  ; EAX = EAX * EDX
      add  ecx  ; EAX = EAX + ECX
      ret

    Aqui, EAX conterá o valor em x, EDX o valor em y e ECX o valor em z.

  • Sistemas operacionais de 64 bits (modo x86-64)Com a criação da extensão amd64 (que foi criada pela AMD, não pela Intel!) o número de registradores de uso geral aumentou para 16 e seus tamanhos foram estendidos para 64 bits. Com isso o modo x86-64 (outro nome para amd64) usa uma convenção de chamada padronizada. Bem… duas, cada uma depende do sistema operacional. No caso de sistemas Unix, o padrão SysV ABI para x86-64 nos diz que os primeiros 6 argumentos integrais devem ser passados nos registradores RDI, RSI, RDX, RCX, R8 e R9, nessa ordem. Se tivermos mais argumentos integrais, esses serão passados na pilha… Ainda, até 8 valores em ponto flutuante devem ser passados via registradores XMM0 até XMM7 (ou YMM0 até YMM7, se tiverem mais que 128 bits).A nossa função f() original ficaria assim:
    ; int f( int x, int y ) { return x / y; }
    f:
      mov eax,edi
      xor edx,edx
      idiv esi
      ret

    Aqui, o valor de x estará em EDI; e o valor de y em ESI.

Uso de registradores em funções

No modo i386 os registradores EBX, EBP e ESP devem ser preservados entre chamadas. Isso significa que esses registradores, se modificados pela função, devem ser salvos e restaurados aos seus valores originais. No modo x86-64 os registradores RBX, RBP, RSP, R12, R13, R14 e R15 devem ser presevados.

Todos os outros registradores podem ser modificados à vontade pela função sem a preocupação de retornarem com valores preservados.

O motivo pelo uso de RBX é somente para termos um registrador que não se altere entre chamadas, que possa ser usado como “porto seguro” para algum outro registrador que queiramos manter, sem termos que salvá-lo na pilha ou numa posição de memória quaisquer. Já RBP é preservado porque ele é usado como “base” para a pilha, algumas vezes. Felizmente os compiladores mais recentes tendem a não usar um “stack frame pointer”, ou seja, o uso de prólogo e epílogo é, mais ou menos coisa do passado… Antes era comum vermos funções como:

f:
  push rbp      ;
  mov  rbp,esp  ; prólogo
  ...
  pop  rbp      ; epílogo.
  ret

Ao invés de lidar com RSP diretamente, era comum lidarmos com RBP. No entanto, tanto RBP quanto RSP podem ser usados como registradores contendo o endereço do topo da pilha. Manipular RSP, neste caso, significa que essas 3 instruções podem não ser usadas numa função!

O motivo para a preservação de RSP deve ser óbvio, especialmente numa convenção de chamada cdecl, onde a responsabilidade da limpeza é da função chamadora, que precisará saber, exatamente, para onde RSP aponta.

O padrão SysV ABI define que os regisradores de R12 até R15 também devem ser preservados e o motivo é o mesmo da preservação de RBX. Temos aqui mais 4 registradores que podem ser usados como “porto seguro”… No total, no modo x86-64, 7 registradores de uso geral devem ser preservados (RBX, RBP, RSP, R12, R13, R14 e R15) entre chamadas e 9 podem ser livremente modificados (RAX, RCX, RDX, RSI, RDI, R8, R9, R10 e R11).

Como exemplo, suponha que tenhamos o seguinte fragmento de código:

strcpy( s1, s2 );
if ( strcmp( s1, "fred" ) == 0)
  doSomething();

Claramente o compilador precisa manter o ponteiro s1 intacto depois da chamada a strcpy() para poder reusá-lo na chamada a strcmp. Assim, ele pode decidir fazer algo assim:

...
  mov rdi,[s1]
  mov rsi,[s2]
  mov rbx,rdi    ; salva RDI em RBX
  call strcpy

  mov rdi,rbx    ; recupera RDI de RBX
  lea rsi,[tmp]  ; tmp aponta para array "fred\0".
  call strcmp

  test eax,eax
  jnz  .skip
  call doSomething
.skip:
...

O compilador usou RBX como salvaguarda porque ele tem que ser preservado entre chamadas, de acordo com a convenção de chamada. É claro, a função chamadora também terá que preservá-lo, por exemplo, usando um push rbx na entrada e um pop rbx antes do retorno.

A ordem dos argumentos

Você já deve ter percebido que a biblioteca padrão da linguagem C usa uma ordem bem definida para a maioria das funções. Por exemplo, na função memset:

void *memset( void *ptr, int c, size_t count );

O argumento ptr é o ponteiro do buffer onde count caracteres c serão escritos. Quer dizer, o alvo da função é o buffer apontado por ptr, que também é retornado pela função, por conveniência… De forma geral, o primeiro argumento é a “saída” da função e os demais, entradas. O mesmo acontece com strcpy:

char *strcpy( char *destptr, char *srcptr );

Claro, existem muitas funções onde a saída é o valor de retorno, mas, mesmo esse está localizado, sintaticamente, mais a esquerda. Esse tipo de padrão é conveniente para manter alguma coerência em todo o seu código fonte, porém, isso não significa que tem que ser sempre assim. Por exemplo, no código fonte do antigo Quake II, podemos ver rotinas como a do cálculo do produto vetorial entre dois vetores:

void vec3_cross( float *v1, float *v2, float *vout )
{
  vout[0] = ( v1[1] * v2[2] ) - ( v1[2] * v2[1] );
  vout[1] = ( v1[2] * v2[0] ) - ( v1[0] * v2[2] );
  vout[3] = ( v1[0] * v2[1] ) - ( v1[1] * v2[0] );
}

Aqui, vout aponta para o vetor de saída, o resultado. e os dois vetores de entrada, na ordem, v1 e v2, aparecem antes dele… Para mim, isso é contraintuitivo, mas, é claro, possível.

O programador esperto pode criar “lembretes” para quem é entrada e quem é saída com simples macros vazias. Usando o mesmo exemplo da função acima, poderíamos ter:

#define IN
#define OUT

void vec3_cross( IN float *v1, IN float *v2, OUT float *vout );

Vale lembrar da limitação do modo x86-64 aqui: Sempre limite seus argumentos a, no máximo 6 inteiros, incluindo ponteiros, e se houver algum em ponto flutuante, a 8 deles. Assim você evita escritas e leituras adicionais na pilha.

Quando retornar valores?

Algumas funções da biblioteca padrão, como vimos, retornam o mesmíssimo conteúdo do primeiro argumento (strcpy, strcat e memcpy, por exemplo, fazem isso). Isso existe por conveniência, para que você possa ter um atalho para obter o valor original do argumento depois de modificá-lo, por exemplo:

char buffer[1024];
char *p, *q;

q = buffer;
p = memset( q++, 0, sizeof buffer );

Aqui, p continuará apontando para o início do buffer e q será incrementado após a chamada a memset. Isso poderia muito bem ser feito de uma forma mais evidente, mas esse tipo de atalho existe. Costumo evitá-los.

Acredito que os valores de retorno válidos são aqueles que dizem alguma coisa sobre a função. Além dos casos puramente matemáticos, onde o retorno é uma transformação da entrada, existem aqueles casos onde o retorno pode ser um status da operação da função. Por exemplo, a função realloc:

void *realloc( void *ptr, size_t size );

Tenta mudar o tamanho do buffer apontado por ptr para o tamanho size e retorna o novo ponteiro. Mas, essa função pode falhar e, nesse caso, retornará NULL (zero!) sem alterar o tamanho do buffer original. Por isso, fazer algo assim:

p = realloc( p, 1024 );

É um erro porque, em caso de erro, o ponteiro p será assinalado com NULL e perderemos a referência ao buffer original. Eis uma função que pode fazer algo assim, com mais segurança:

int reallocate( void **pp, size_t size )
{
  void *p;
  int status;

  p = realloc( *pp, size );
  status = ( p != NULL );

  // Só modifica o ponteiro se bem sucedido!
  if ( status )
    *pp = p;

  return status;
}

A função tem duas saídas, por assim dizer, o buffer que será realocado e um resultado de status da função… Se realloc não conseguir realocar o buffer, nada é mudado e 0 (erro ou falso) é retornado, caso contrário, o ponteiro “apontado” por pp é modificao e retornamos ok (ou true). Claro, a chamada fica um pouco diferente:

// Nosso ponteiro para o buffer.
char *p = NULL;
...
if ( ! reallocate( &p, 1024 ) )
{ ... trata erro aqui ... }
...

Repare que estamos passando o endereço (o ponteiro) para a variável p que, em si, é um ponteiro também… Aliás, existe um efeito colateral para a função reallocate, acima, interessante: Se o ponteiro p for inicializado inicialmente com NULL, a função realloc funciona exatamente como malloc.

Evite passar e retornar tipos compostos (estruturas) por valor:

O problema com estruturas é que elas podem ser tão grandes que não caibam dentro de um único registrador. Por exemplo:

struct vertex_s {
  double x, y, z;    // vértice
  double nx, ny, nz; // vetor normal.
  double r, g, b, a; // cor do vértice.
  double u, v;       // coordenada de textura.

  double bx, by, bz; // Tangentes ao vetor normal,
  double tx, ty, tz; // usados para bump mapping.
};

A estrutura acima tem 144 bytes de tamanho. Ao criar uma função como:

struct vertex_s f( struct vertex_s v );

O compilador não tem alternativa senão alocar espaço para duas estruturas: Uma para passar o argumento (que é local à função) e outra para receber a resposta. Para entender como isso funciona precisamos recorrer às funções que usam tipos mais simples. Considere a função:

int f( int x ) { return x + x; }

A função que chama essa função f() é responsável por alocar o espaço para o argumento x e copiar o dado que será passado para f(). A chamadora também é responsável por alocar o espaço para o retorno da função. No entanto, para tipos simples, a convenção de chamada geralmente retorna valores diretamente em registradores, ao invés de memória, passando ao largo dessa alocação de retorno.

Isso não pode acontecer com estruturas grandes. Supondo que você já tenha uma estrutura alocada e vai passá-la para a função f() que tenha o protótipo usando as estruturas, como acima. Ambos, o argumento e o retorno, serão alocados pela função chamadora. Por que o argumento precisa ser criado? Ele é interpretado como local à função f() e é uma cópia da estrutura original. Só ai temos 3 blocos de 144 bytes da estrutura vertex_s na memória: A original, a cópia que será passada para o argumento e a estrutura de retorno. E todas as três estarão alocadas na pilha.

Se a função chamadora faz algo como:

struct vertex_s r, v = { 0 };

r = f(v);

O compilador, sem otimizações, provavelmente fará algo assim:

  sub rsp,576  ; espaço para r, v, argumento e retorno de f().

  ; zera todo v:
  ; PS: Usei 'rep stosq' e 'rep movsq', abaixo, para
  ;     escrever menos. O compilador pode preferir criar
  ;     18 instruções mov aqui e 36, no caso de cópias...
  mov  ecx,18
  lea  rdi,[rsp+288]
  xor  eax,eax
  rep  stosq

  ; copia v para o argumento.
  mov  ecx,18
  mov  rdi,rsp
  lea  rsi,[rsp+288]
  rep  movsq

  ; chama f
  call f

  ; copia o retorno para r
  mov  ecx,18
  lea  rsi,[rsp+144]
  lea  rdi,[rsp+432]
  rep  movsq

  ; libera espaço do argumento e do retorno
  add  rsp,288

  ...

  ; antes de sair, livra-se as variáveis locais...
  add  rsp,288
  ret

Repare a ordem com que as variáveis são alocadas… Quanto a r e v, o compilador decide onde as colocará, mas quanto aos argumentos e retorno da função, as estrturas são, necessariamente, colocadas no topo da pilha: retorno primeiro, argumento depois (lembre-se que a pilha “cresce” para baixo!). Assim, RSP+8, dentro de f() aponta para o argumento e RSP+152 apontará para o resultado.

Se você, ao invés de usar argumentos e retornos de tipos compostos, passar a usar ponteiros, os argumentos podem ser passados por registradores (no modo x86-64 e nos modos fastcall ou regparam do GCC):

void f( struct vertex_s *vout, struct vertex_s *vin );

Aqui, numa chamada do tipo f( &r, &v );, os ponteiros de r e v são passados para a função que não precisará alocar espaço temporário, inundando a pilha…

Então, só para lembrar: Estruturas são, necessariamente, passadas pela pilha (em qualquer convenção de chamada). Evite passagens por valor de tipos não primitivos!

Evite criar arrays e estruturas grandes como variáveis locais:

Dito o que eu disse acima, é prudente evitar a criação de arrays e tipos complexos (como estruturas) na pilha… Seu sistema operacional permite que a pilha cresça até um certo ponto e esse limite costuma ser bem grande (de 8 a 10 MiB, dependendo da distribuição do Linux; algo nesses mesmos limites no Windows), mas o espaço inicial de alocação da pilha é de cerca de 16 KiB para cada thread em execução.

Quando o limite da pilha passa dos 16 KiB ocorre uma “falta de página” e o seu processo é interrompido para que o sistema operacional atualize o tamanho da pilha, adicionando o tamanho requisitado, com granulidade de, pelo menos 4 KiB por vez. Então, quando você faz, no interior de uma função:

char buffer[8192] = { 0 };

O compilador não tem alternativa senão alocar o buffer de 8 KiB na pilha. Supondo que seu código já esteja usando uns 10 KiB da pilha, ao subtrair 8192 de RSP e usar o ponteiro para preencher a região de 8 KiB com zeros, o processador pode não encontrar uma região abaixo dos 16 KiB já mapeados para a pilha e uma falta de página ocorre, parando tudo, alocando mais 4 KiB e retornando da falta com uma nova pilha de 20 KiB. Só então o processamento continua…

Page Faults costumam ser muito lerdas, gastando centenas de milhares de ciclos de clock para fazer o trabalho. Se sua rotina for chamada, por exemplo, recursivamente, cada 2 iterações podem causar page faults e o que levaria alguns segundos para executar pode levar alguns minutos…

Prefira trabalhar com alocação dinâmica. É mais rápido.

Um outro detalhe importante é que variáveis de escopo local, definidas em blocos diferentes, não são liberadas da pilha (quando alocadas lá) automaticamente logo após a perda do escopo do bloco interno… O compilador tenta otimizar o uso da pilha, mas variáveis locais de escopo interno são todas alocadas na pilha e ficam lá até o retorno da função… Por exemplo:

int f( void )
{
  char buff[1024];

  {
    char buff2[1024];
    ...
  }

  {
    char buff3[1024];
    ...
  }
  ...
}

Aqui teremos 3 KiB alocados na pilha e eles ficarão lá até a função retornar. Em C, O fato de buff2 e buff3 terem visibilidade interna a seua blocos, não significa que no encerramento desses blocos a pilha será “limpa”… Em C++ isso é um pouco diferente: A criação e limpeza da pilha ocorrem nos pontos de sequência onde a declaração aparece e some ao perder o escopo.

Outro motivo para evitar grandes blocos de dados são os efeitos dos caches. Seu processador sempre lida com o cache L1 para a maioria das regiões da memória disponíveis para o seu processo. Os caches são divididos em linhas de 64 bytes e têm um número limitado de linhas. O cache L1, por exemplo, é dividido em dois pedaços de 32 KiB, onde cada pedaço suporta apenas 512 linhas de 64 bytes. Sempre que um pedaço da memória não esteja presente no cache L1 o processador pára o que está fazendo e tenta carregar uma nova linha, do cache L2. Se essa linha também não estiver presente no cache L2 (que tem uns 256 KiB, ou 4096 linhas), ele tenta carregá-la do cache L3 (que têm uns 6 MiB e também é dividido em linhas)… Se a linha não estiver em nenhum dos caches ela é carregada para memória, para o cache L3, depois o L2 e depois o L1… Mas, espere! E se todas as linhas dos caches estiverem em uso o processador tem que selecionar uma linha “menos usada”, despejá-la na memória física e só então fazer a cópia… Isso pode tomar dezenas de milhares de ciclos de clock. Uma simples instrução mov eax,[var], ao invés de gastar 2 ciclos de clock, pode gastar uns 30000 (ou mais!).

Se você conseguir limitar o total de dados na ordem de 32 KiB, a maioria de seus dados permanecerá no cache L1 ou, no máximo, no cache L2, diminuindo muito o tempo de acesso… Então, evite criar arrays e estruturas muito grandes!