Localização em C

Alguém me perguntou sobre locale em C. Pra você que não sabe o que locale significa, é somente a maneira como algumas strings relacionadas a formatação se apresentam de acordo com a localidade onde o usuário se encontra. Por exemplo, no Brasil usamos ‘,’ (vírgula) ao invés de ‘.’ (ponto) decimal, quando queremos expressar valores fracionários, como em 3,141593. Mas, parece, que as rotinas das bibliotecas lidam apenas com o formato americano, usando o ‘.’. Bem… não é bem assim.

O padrão ISO 9899, que rege a linguagem C e a biblioteca padrão, nos fornece o header locale.h com estruturas e funções para lidarmos com localização. A localização default é chamada, simplesmente, de “C” e segue o padrão americano. Mas, podemos ajustar a localidade para a nossa língua, nosso país e o charset que usamos, desde que o sistema operacional o tenha acessível. Para isso usamos a função setlocale(), que tem o protótipo:

char *setlocale(int category, const char *locale);

A “categoria” é uma das constantes LC_ALL, LC_ADDRESS, LC_COLLATE, LC_CTYPE, LC_IDENTIFICATION, LC_MEASUREMENT, LC_MESSAGES, LC_MONETARY, LC_NAME, LC_NUMERIC, LC_PAPER, LC_TELEPHONE ou LC_TIME. Cada uma dessas características endereça um aspecto da sua localidade… LC_NUMERIC, por exemplo, é usada para ajustar como o “ponto decimal” e o “separador de milhar” serão usados. LC_MONETARY, a mesma coisa, mas também onde a string da moeda será colocada e se como valores negativos são expressos… A categoria LC_CTYPE tem a ver com as macros contidas no header ctype.h. E por ai vai…

O argumento “locale” é uma string com um formato específico. Pro Brasil, por exemplo, pode-se usar “pt_BR” ou “pt_BR.utf8” (no caso do Linux/BSD/MacOS). O formato segue o padrão “língua[_país][.charset]”, onde “língua” segue a abreviação especificada na ISO 639, enquanto “país” segue a especificação ISO 3166… Mas, é importante notar que apenas os locales disponíveis no sistema operacional são aceitáveis e eles podem ser obtidos assim:

$ locale -a
C
C.UTF-8
en_AG
en_AG.utf8
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_IN.utf8
en_NG
en_NG.utf8
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZM
en_ZM.utf8
en_ZW.utf8
POSIX
pt_BR.utf8

Note que, no meu caso, “pt_BR” não está disponível, apenas “pt_BR.utf8”.

O programinha abaixo mostra locale em funcionamento:

#include <stdio.h>
#include <locale.h>

void main(void)
{
  float f;

  setlocale(LC_ALL, "pt_BR.utf8");
  fputs("Entre com um valor float (usando ',' como separador decimal): ", stdout);
  fflush(stdout);
  scanf("%f", &f);
  printf("Valor: %f\n", f);
}

Compilando e executando:

$ cc -o test test.c
$ ./test
Entre com um valor float (usando ',' como separador decimal): 3,14
Valor: 3,140000

Tanto o scanf() quanto o printf() obedeceram a localização ajustada por setlocale()! Mas, note, não é que essas funções respeitem a localização, mas as funções que elas usam o fazem (atof(), por exemplo). Outro ponto interessante é sobre os “separadores de milhar”. Eles não são respeitados nessas funções! Faça float f = atof("32.768,9"); com o locale pt_BR.utf8 ativo e você, provavelmente, obterá 0.0f como resposta, indicando falha da conversão. Não espere que scanf() e printf() vão entender valores monetários, por exemplo… O locale LC_MONETARY existe somente para que você possa pegar as regras correspondentes na estrutura lconv.

Para saber quais são as regras do locale selecionado, use a função localeconv() que retorna um ponteiro para uma estrutura lconv contendo um monte de informações (disponiveis na especificação da linguagem C, aqui). Você não deve modificar os valores da estrutura…

Uma maneira simples de livrar-se dos separadores de milhar, levando-se em conta o indicado em lconv, é usando uma rotina como essa aqui:

#include <string.h>
#include <locale.h>

char *strip_thousands_sep(char *s)
{
  struct lconv *pl;

  pl = localeconv();
  if (*pl->mon_thousands_sep)
  {
    char *p = s;

    while (p = strchr(p, *pl->mon_thousands_sep))
      strcpy(p, p + 1);  // Não há problema em fazer
                         // essa cópia. Não há overlapping
                         // aqui e ambos os ponteiros
                         // são válidos.
  }

  return s;
}

É claro, a localidade terá que ser ajustada antes de chamar essa rotina. E, como já dito, para retornar a localidade default, basta usar “C”, ao invés de “pt_BR.utf8”.

A localização também é útil para comparação de strings. A função strcoll() leva em consideração a localidade para comparar duas strings. O exemplo contido no site cppreference (exemplo, aqui) ilustra bem. Note que strcmp() e derivadas não lidarão com localização… Outra função para lidar com strings e localização útil é strxfrm(). Ela transforma (daí a abreviação xfrm) uma string numa representação que pode ser usada em strcmp(), ou derivadas, para ter o mesmo efeito de strcoll(). Isso pode ser útil em línguas como hebraico e árabe, que são escritas “de trás para frente”…

Instalando novas localidades no Linux:

Nada mais simples. Linux disponibiliza pacotes para linguagens específicas. Para listá-las todas (Debian/Ubuntu):

$ apt-cache search language-pack | grep 'pack-.. '
language-pack-af - translation updates for language Afrikaans
language-pack-am - translation updates for language Amharic
language-pack-an - translation updates for language Aragonese
language-pack-ar - translation updates for language Arabic
language-pack-as - translation updates for language Assamese
language-pack-az - translation updates for language Azerbaijani
language-pack-be - translation updates for language Belarusian
language-pack-bg - translation updates for language Bulgarian
language-pack-bn - translation updates for language Bengali
language-pack-bo - translation updates for language Tibetan
language-pack-br - translation updates for language Breton
language-pack-bs - translation updates for language Bosnian
language-pack-ca - translation updates for language Catalan; Valencian
language-pack-cs - translation updates for language Czech
language-pack-cy - translation updates for language Welsh
language-pack-da - translation updates for language Danish
language-pack-de - translation updates for language German
language-pack-dz - translation updates for language Dzongkha
language-pack-el - translation updates for language Greek, Modern (1453-)
language-pack-en - translation updates for language English
language-pack-eo - translation updates for language Esperanto
language-pack-es - translation updates for language Spanish; Castilian
language-pack-et - translation updates for language Estonian
language-pack-eu - translation updates for language Basque
language-pack-fa - translation updates for language Persian
language-pack-fi - translation updates for language Finnish
language-pack-fr - translation updates for language French
language-pack-ga - translation updates for language Irish
language-pack-gd - translation updates for language Gaelic; Scottish Gaelic
language-pack-gl - translation updates for language Galician
language-pack-gu - translation updates for language Gujarati
language-pack-he - translation updates for language Hebrew
language-pack-hi - translation updates for language Hindi
language-pack-hr - translation updates for language Croatian
language-pack-hu - translation updates for language Hungarian
language-pack-ia - translation updates for language Interlingua (International Auxiliary Language Association)
language-pack-id - translation updates for language Indonesian
language-pack-is - translation updates for language Icelandic
language-pack-it - translation updates for language Italian
language-pack-ja - translation updates for language Japanese
language-pack-ka - translation updates for language Georgian
language-pack-kk - translation updates for language Kazakh
language-pack-km - translation updates for language Central Khmer
language-pack-kn - translation updates for language Kannada
language-pack-ko - translation updates for language Korean
language-pack-ku - translation updates for language Kurdish
language-pack-lt - translation updates for language Lithuanian
language-pack-lv - translation updates for language Latvian
language-pack-mk - translation updates for language Macedonian
language-pack-ml - translation updates for language Malayalam
language-pack-mn - translation updates for language Mongolian
language-pack-mr - translation updates for language Marathi
language-pack-ms - translation updates for language Malay
language-pack-my - translation updates for language Burmese
language-pack-nb - translation updates for language Bokmål, Norwegian; Norwegian Bokmål
language-pack-ne - translation updates for language Nepali
language-pack-nl - translation updates for language Dutch; Flemish
language-pack-nn - translation updates for language Norwegian Nynorsk; Nynorsk, Norwegian
language-pack-oc - translation updates for language Occitan (post 1500)
language-pack-or - translation updates for language Oriya
language-pack-pa - translation updates for language Panjabi; Punjabi
language-pack-pl - translation updates for language Polish
language-pack-pt - translation updates for language Portuguese
language-pack-ro - translation updates for language Romanian
language-pack-ru - translation updates for language Russian
language-pack-si - translation updates for language Sinhala; Sinhalese
language-pack-sk - translation updates for language Slovak
language-pack-sl - translation updates for language Slovenian
language-pack-sq - translation updates for language Albanian
language-pack-sr - translation updates for language Serbian
language-pack-sv - translation updates for language Swedish
language-pack-ta - translation updates for language Tamil
language-pack-te - translation updates for language Telugu
language-pack-tg - translation updates for language Tajik
language-pack-th - translation updates for language Thai
language-pack-tr - translation updates for language Turkish
language-pack-ug - translation updates for language Uighur; Uyghur
language-pack-uk - translation updates for language Ukrainian
language-pack-uz - translation updates for language Uzbek
language-pack-vi - translation updates for language Vietnamese
language-pack-xh - translation updates for language Xhosa

Se adicionar language-pack-ja, por exemplo, a localidade jp_JP.utf8 será adicionada nas tabelas de localidade do sistema e ficará disponível para uso de seus programas.

Provavelmente seu professor não explicou isso direito pra você! (parte 2)

Neste outro artigo, aqui, eu te expliquei o funcionamento do stream stdout e do uso de fflush(). Mas, deixei alguns detalhes de fora, especialmente quanto ao stream stdin.

Em um grupo do Facebook tenho visto uma galera usando fflush(stdin) em resoluções de exercícios, usando linguagem C. Essa construção é inútil porque, por padrão, fflush() funciona apenas para streams que podem ser escritas. Este é o caso de stdout.

Outro detalhe é sobre o uso da função scanf(). Ela tem uns problemas que podem ser desesperadores para o novato.

Também têm um valor de retorno. A função devolve um valor do tipo int que é o número de itens que puderam ser convertidos na “varredura” (scan) ou -1, em caso de erro catastrófico. Se você fizer:

int a, b, n;

n = scanf("%d %d", &a, &b);

E entrar com duas strings não numéricas, vai obter n diferente de 2. Se entrar com “xpto xpto”, obterá n=0. Se entrar com “1 xpto”, obterá n=1… A função também interpreta ‘\n’ como “espaço” a ser ignorado. Se você entrar com apenas “1”, a função continuará esperando pelo segundo argumento a ser convertido!

O termo “varredura” é importante! A função procura pelos itens a serem convertidos, de acordo com o formato especificado, varrendo o stream stdin, até conseguir convertê-los ou falhar, ignorando os “espaços”. No exemplo acima, se entrássemos com:

"1          1" ou "       1 1"

Teríamos a e b convertidos corretamente.

No caso de itens que não podem ser convertidos, aparece um problema: Eles permanecem no buffer de stdin! Para demonstrar isso, eis um exemplo:

#include <stdio.h>

void main(void)
{
  int a, b, n;

  for (;;) {
    printf("Entre com dois números: "); fflush(stdout);
    n = scanf("%d %d", &a, &b);
    printf("%d valores convertidos: %d e %d\n", n, a, b);
  }
}

Compile e execute esse programinha e entre com “abc abc”… Observe os printf‘s sendo executados um monte de vezes! Isso acontece porque scanf() deixará no buffer de stdin os caracteres não convertidos da string lida anteriormente (como expliquei ai em cima), bem como qualquer caracter “não pulado”. Assim, scanf() tentará reconvertê-los, falhando diversas vezes até limpar todo o buffer de stdin.

Infelizmente não há jeito portável de fazer um flushing no stream stdin… Mas, usando uma extensão da glibc podemos corrigir esse problema facilmente:

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

int main(void)
{
  char *line;
  size_t line_size;
  int n;

  unsigned char b;
  char str[21];

  do {
    printf("Entre com um valor de 8 bits e uma string separados por espaço:\n");

    // força alocação da linha em getline().
    line_size = 0;
    line = NULL;

    // Lê uma linha de stdin.
    // getline() é uma extensão da glibc!
    if (getline(&line, &line_size, stdin) == -1)
    {
      fputs("ERRO alocando espaço para conter a linha!\n", stderr);
      exit(EXIT_FAILURE);
    }

    // Para termos um valor definido, em caso de falha.
    str[0] = b = 0;

    // Usa sscanf() porque quando scanf falha, deixa o stream stdin com lixo!
    n = sscanf(line, "%hhu %20s", &b, str);

    // livra-se da linha alocada.
    free(line);

    printf("%d itens convertidos: %hhu, \"%s\"\n", n, b, str);
  } while (1);
}

Aqui, sscanf uão usa um stream para obter os valores convertidos, ela usa uma string! A função getline() lerá uma linha da entrada (stdin) — ou seja, até char um ‘\n’, que será colocado no final do buffer alocado, precedendo o caracter NUL — e retornará com buffer apontado por line dinamicamente alocado (ou retorna -1 em caso de falha). Note que depois usar o sscanf fiz uma chamada a free(), livrando-se do buffer alocado por getline.

Se você digitar ENTER e obter uma linha vazia, ela terá 1 único caracter (‘\n’) o o NUL. Assim, sscanf falhará, mas não há nenhum buffer mantido por um stream para bagunçar o coreto.

Já que getline() é uma extensão, você pode optar por usar fgets(), mas perderá o recurso de alocação dinâmica automática, tendo de codificar isso por si mesmo… Ahhh… não use gets() para ler o stream stdin. Essa função é obsoleta desde a especificação C99 e não deve ser mais usada.

Postmortem: Erros comuns que já vi em C++ e COM

Há mais ou menos uma década, ou um pouco mais, cheguei a trabalhar com consultoria a respeito de otimização e caça de erros em códigos de “frameworks” para algumas empresas. Lembro-me de 3 projetos grandes que apresentavam os mesmos erros que, até onde sei, jamais foram solucionados.

Como sempre acontece, trata-se do uso indiscriminado de “facilidades”, sem o devido conhecimento de como elas funcionam. Coisa comum no desenvolvimento usando “orientação a objetos”, afinal, alguns “comportamentos” estão “encapsulados” dentro de um conjunto de classes e o programador, em teoria, não deveria nem querer saber como, só usá-los, certo? Bem… errado!

Abaixo, mostro alguns erros que já encontrei e é apenas uma lista pequena contida num texto gigantesto, desculpe…

1º: Objetos anônimos temporários

O primeiro problema comum de ser encontrado em códigos escritos em C++ é o desconhecimento sobre como uma linguagem de programação funciona… E não estou falando somente de suas regras sintáticas, mas da semântica da resolução de expressões e passagem de argumentos de funções. Vejamos um exemplo simples com objetos de alguma classe complexa qualquer, usando sobrecarga de operadores:

a = b + c;

Essa simples expressão é composta de dois operadores (‘=’ e ‘+’), onde o operador de adição sempre retornará um “objeto anônimo temporário”. Por quê? Ora, nem o objeto ‘b’, nem o objeto ‘c’ devem ser molestados e, o que queremos, é a adição desses dois como resultado. Temos que criar, on-the-fly, um terceiro objeto que represente essa “adição”… É comum que um operador desse seja definido de acordo com a assinatura abaixo:

MyClass MyClass::operator+(const Myclass& o);

O objeto retornado por esse operador será usado como argumento para o operador “=”, que o copiará para o interior do objeto ‘a’ e, só então, o objeto anônimo temporário deixará de existir.

Para objetos pequenos, cuja cópia é simples de ser feita, a perda de performance é quase inócua, mas para objetos complexos, que contém em seu interior, agregações, listas, árvores etc, a operação de cópia pode ser bem demorada, bem como as necessidades de recursos podem crescer um bocado. Suponha que o objeto ‘c’ tenha uns 2 MiB de dados agregados em seu interior e o objeto ‘b’ tenha 1 MiB… Suponha, agora, que no processo de “adição” esses dados sejam manipulados de forma tal a gerar o consumo de 3 MiB (que pode ser bem mais, dependendo da transformação necessária!)… Temos o consumo de 3 MiB adicionais apenas no objeto anônimo temporário, retornado pelo operador ‘+’!

Nesses casos, para evitar a criação de objetos temporários, seria interessante usarmos funções-membro especializadas ao invés de operadores. A expressão acima poderia ser reescrita como:

a.append(b, c);

Onde a função-membro append aceitaria duas referências para os argumentos ‘b’ e ‘c’. Podemos controlar a criação de objetos temporários, se houver necessidade de um.

Mais um exemplo: Suponha agora que a expressão seja bem mais complexa, como a = -(b + c) * (d >>= e);. Dependendo do que os operadores ‘-‘ (unário), ‘*’ e ‘>>=’ fazem, teremos uns 3 objetos anônimos temporários em potencial (um para t1=(b + c), outro para t2=t1*(d >>= e) e outro para t3=-t2). Cada um com seus próprios recursos… Isso sem contar que podemos ter outras expressões que usem os objetos originais e, mesmo com a otimização de common subexpression elimination, que não funciona muito bem para classes customizadas, já que a semântica dos operadores muda radicalmente, podemos ter n objetos temporários anônimos em uso num mesmo bloco.

A criação de objetos temporários acontece, também, com chamadas de funções, mas, neste caso, os objetos não são “anônimos”, mas uma cópia do original, se fizermos algo assim:

MyClass f(MyClass a, MyClass b) { ... }

Ao passar instâncias para a função f(), automaticamente o compilador gerará uma cópia das instâncias originais, porque ‘a’ e ‘b’ não podem modificá-las, sendo locais à função… Note que a função retorna um objeto anônimo da classe ‘obj’ também!

É claro que para solucionar esse tipo de coisa, pelo menos no que se refere aos argumentos, podemos usar referèncias:

MyClass f(const MyClass& a, const MyClas& b) { ... }

Aqui, o qualificador const garante que a instância referenciada não poderá ser modificada no interior da função… Isso é óbvio para um desenvolvedor experiente, mas, nessa época de .NET e Java, onde argumentos de funções de tipos complexos são, na verdade, referências, costuma-se esquecer do fato acima, causando grande pressão por uso de recursos…

2º: Tipos primitivos versus objetos “constantes”

Outra coisa que já observei é a tendência a usar classes que oferecem facilidades, ao invés de tipos primitivos, especialmente quando estamos lidando com constantes. Um exemplo clássico é a definição de “constantes” do tipo “string”… Nesses “frameworks” era comum ver algo assim:

const std::string ERROR1 = "Erro genérico";
const std::string ERROR2 = "Erro de qualquer bobagem";
const std::string ERROR3 = "Erro errado";
...
const std::string ERROR1023 = "Erro de um monte de erros";

O problema aqui é que o programador não está criando constantes. Está criando instâncias do objeto basic_string contendo contantes. Todos esses 1023 objetos terão que ser construídos, ou seja, código de construtores serão chamados. Só para ilustrar, eis o código em assembly gerado pelo GCC, para x86-64 (linux), do construtor da “constante” ERROR1, acima:

_GLOBAL__sub_I_test.cc:
  movabs rax, 7954877705826234949 ; A string de ERROR1.
  mov rdx,__dso_handle
  mov rsi,_ZL6ERROR1   ; A referência ao objeto ERROR1.
  mov [_ZL6ERROR1+16],rax
  mov rdi, _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEED1Ev
  mov eax, 28515
  mov qword [_ZL6ERROR1],_ZL6ERROR1+16
  mov dword [_ZL6ERROR1+24],1769122243
  mov [_ZL6ERROR1+28],ax
  mov qword [_ZL6ERROR1+8],14
  mov byte [_ZL6ERROR1+30],0
  jmp __cxa_atexit

É claro que a rotina também registra o destrutor (pulando para __cxa_atexit no fim das contas)… Mas, neste construtor, porque a string é pequena (14 bytes), ela cabe no registrador RAX e o valor atribuído no início da rotina é exatamente essa string parcial no formato little endian (os primeiros 8 bytes: 0x6E6567206F727245, formando “Erro gené”), a 5ª instrução, de cima para baixo completa a string (0x6972A9C3 ou “rico”), e a penúltima coloca o ‘\0’ final. Mas existem outras 8 instruções para esse simples construtor de apenas uma das “constantes” (dentre eles o ajuste do tamanho da string na ante-penúltima instrução). Para strings maiores que 16 bytes, contando o terminador ‘\0’, o construtor fica mais complicado… Assim, um código enorme será executado para inicializar os objetos.

Compare isso à simples declaração de arrays:

const char ERROR1[] = "Erro genérico";

O compilador só fará isso:

section .rodata

        global ERROR1
ERROR1: db 'Erro genérico',0

Nenhum construtor é criado.. Os dados simplesmanente são colocados no segmento de dados read-only e acessíveis por ponteiro. Existe outra vantagem nisso: O qualificador const, para tipos primitivos, tende a criar constantes de fato em C++. Por exemplo, se tivéssemos, em dois módulos separados:

// No módulo errors.cc
const char ERRO1[] = "Erro";

// No módulo xpto.cc
const char * const error = "Erro";

O linker tenderá a fazer os ponteiros ERROR1 e error apontarem para o mesmo lugar porque existirá apenas uma string “Erro” no código inteiro (otimização “merge duplicate strings“). E, como vimos no caso do uso de “objetos constantes”, eles são construídos e a string é copiada para o interior do objeto, triplicando o uso dos recursos (teriamos 3 strings “Erro” na memória se o objeto ERROR1 e ERROR2 foram inicializados com a string “Erro”).

Isso não parece ser um problema sério, mas considere que as classes definidas nesse framework eram usadas em objetos COM que, de acordo com o ambiente, podem ser carregados e descarregados da memória… Esse comportamento é comum, no que a Microsoft chama de “objetos interoperáveis” que é a mistura de COM (unmanaged code) com objetos .NET (managed code)

Ao usar constantes “reais”, definidos com tipos primitivos, o único problema é que para usá-las teremos que instanciar objetos temporários anônimos do tipo string, como em:

std::cerr << std::string(ERROR1) << '\n';

Mas, a criação deste objeto menos traumática. O compilador, provavelmente, criará uma única função com um construtor string::string(const char *); (ou um construtor de conversão). Diferente da criação de múltiplos objetos “constantes”, este objeto temporário é criado na hora de sua necessidade e destruído assim que sairmos do escopo do bloco onde ele existe.

3º: Conversão de tipos antes da hora

De maneira similar, outro fato relacionado às funções da Win32 API e objetos COM (OLE) é o uso de um tipo “especial” de string chamada BSTR. Em C e C++ uma string é definida como um array contendo os caracteres e terminada em ‘\0’. Note que não falei “array de chars” porque podemos ter arrays de wchar_t, onde cada item tem 16 bits de tamanho…

No caso de COM (OLE), algumas vezes é necessário usar uma estrutura diferente para strings, onde cada caracter é um wchar_t e o terminador é um ‘\0’, também com 16 bits de tamanho… Além disso, o primeiro wchar_t (ou unsigned short) do array contém o tamanho da string contida no array.

Tanto no Visual Studio quando no Borland C++ Builder (usado em dois dos projetos de que falei) contém classes especializadas para conter esse tipo de string e é comum a conversão do tipo de string ANSI (na nomenclatura do Windows, são strings de chars) ou Wide Strings (onde cada ítem é um wchar_t) para o tipo BSTR (ou _bstr_t). A conversão não pode ser mais simples, usando classes:

CBSTR bstr = str;

Dependendo do tipo de str (CString ou CStringW) a classe CBSTR poderá ter um construtor de conversão mais ou menos assim:

// Note: CString contém strings "ansi".
CBSTR::CBSTR(const CString& s)
{
  size_t i, length;
  char *p = s.c_str();

  length = strlen(p);
  this->str = new wchar_t[length+1];
  for (i = 1; i <= length; i++) this->str[i] = (wchar_t)*p++;
  this->str[i] = '\0';
  this->str[0] = (wchar_t)(length + 1); //?
}

A função acima, é claro, pode muito bem ser substituída por uma chamada a StrAllocString(), no caso de Wide Strings

O destrutor de CBSTR verifica se existe um ponteiro não nulo em this->str, efetua um delete [] this->str;, se for o caso, e zera this->length. Mas, o ponto aqui é que, cada conversão envolve a alocação de novo espaço e a cópia da string original, potencialmente duplicando o espaço originalmente usado, e triplicando o uso de recursos.

Assim, usar BSTRs antecipadamente é gasto de recursos. Deveriam manter as strings confinadas aos tipos genéricos (que ocupam 2 ou 3 bytes a menos, pelo menos, já que não contém o campo length) e só quando forem chamar uma função OLE, efetuassemos:

{ SomeAPIOLEFunction(CBSTR(str).bstr()); }

Onde o membro bstr() retorna o ponteiro para a string BSTR… Note o bloco… O objeto temporário seria destruído logo após o retorno da função…

O ponto aqui é que BSTRs são necessárias apenas para as chamadas dessas funções da Win32 API. Não há necessidade de mantê-las instanciadas mais do que o tempo necessário de seu uso. A mesma coisa acontece quando uma função da API devolver uma BSTR. Poderíamos fazer algo assim:

// Note: CStringW contém Wide string.
CStringW str;

{
  wchar_t *p;
  
  SomeAPIOLEFuncionGetsBSTR((BSTR *)p);
  str = CBSTR((BSTR *)p); // supondo que CBSTR aceite um ponteiro void...
                          // supondo que CStringW tenha um construtor de
                          //   conversão para BSTR *.
  // A expressão acima poderia ser substituída por:
  //
  //   str = (BSTR *)p;
  //
  // Se CStringW tiver alguma conversão de ponteiros desse tipo.

  // Pode ser necessário chamar SysFreeString((BSTR *)p) aqui!
}

Do mesmo jeito, CBSTR “morrerá” assim que o bloco for encerrado… Apenas por um breve momento teremos muito recurso em uso, mas strings deste tipo não são tão grandes assim (64 KiB, no máximo) e logo o final dos blocos as destruiriam…

4º: Agregações com containers errados

É comum, nessas classes complexas de frameworks, que o desenvolvedor queira manter listas, filas, pilhas e outras estruturas. O que é comum encontrar é o uso de containers errados para a finalidade que o objeto se dispõe a resolver. Por exemplo, já vi muita classe agregando vector<T> ou list<T> para conter uma lista de objetos ordenados. É clarqo que dá para fazer isso e a STL disponibliza a função sort() no header algorithm. Só que existem containers que são feitos para manterem itens ordenados à medida que os inserimos… É o exemplo de set e map (e seus irmãos que aceitam várias chaves idênticas, multiset e multimap). Eles usam uma red black tree para implementar esse comportamento e, portanto, têm tempo de pesquisa na ordem de \log n. Sendo bem mais rápidos do que os containers mais “fáceis” de usar.

5º: O preconceito contra ponteiros

Eis um dos motivos da “fuga” da programação procedural para a “orientada a objetos”. Tem um monte de gente que tem medo de ponteiros! Embora a construção e destruição “automática” de objetos seja bem interessante, a alocação e dealocação dinâmica de blocos de dados é bem mais rápida e gera código mais eficiente. Isso pode ser visto nos dois fragmentos de código abaixo:

// Usando um array de objetos como uma lista.
// O último item é NULL (para emular o end()
// do iterator, abaixo).
obj *list, *p;

for (p = list; p; p++)
  p->doSomething();
-----%<-----%<-----
// Usando um container vector<>:
std::vector<obj> list;
std::vector<obj>::iterator i;

for (i = list.begin(); i != list.end(); i++)
  i->doSomething();

O segundo código parece ser mais limpo e simples que o primeiro, mas ele esconde um monte de detalhes de implementação. Para objetos simples os dois códigos são quase que exatamente os mesmos (quase! o primeiro é mais eficiente), mas se seu objeto tiver funções virtuais, herança múltipla (comum no caso de COM), classes base virtuais, … o container vector pode gerar código mais complicado (lembre-se que ele é um template). E, como demonstrei neste artigo, um container não se comporta exatamente como se espera, às vezes.

Além disso, graças ao conceito de “referência”, a turma do C++ prefere usá-las, ao invés de ponteiros só porque a “notação” é mais simples, do ponto de vista da linguagem. Mas, note que lidar com ponteiros é coisa que o processador faz com facilidade. Ao adicionar comportamentos à classes, a abstração permite ao programador mais liberdade, com o custo de uma pequena, mas significativa, ineficiência.

6º: O uso cego da MFC ou da VCL não é a melhor maneira de implementar um objeto COM

Ambas a Microsoft Foundation Classes (no Visual Studio) e a Visual Components Library (no Borland C++ Builder ou Delphi) contém templates prontinhos para usar herança na implementação de classes baseadas nas interfaces IUnknown ou IDispatch, que são as mais usadas nesse tipo de codificação. Esses templates, geralmente, são construídos através de wizzards que constroem classes com nossas funções membro hardcoded na classe, mais ou menos assim:

class IMyClass : public IUnknown {
public:
  virtual void doSomething(void);
};

Onde tudo o que você tem que fazer é criar o código de doSomething(). No entanto, especialmente com a interface IDispatch, graças ao conceito de late binding, esse tipo de artifício pode tornar seu código bem complicado.

Não parece, mas é bem mais fácil usarmos “composição” para criarmos essas classes, mais ou menos assim (este é apenas um código de exemplo não testado… Não me lembro se a MFC ou a VCL implementam as funções virtuais de IUnknown – estou assumindo que sim!):

class IMyClass : public IUnknown {
private:
  MyClassInternal *myobjptr;
public:
  IMyClass() : myobjptr(NULL) {}

  // métodos virtuais sobrecarregados de IUnknown.
  ULONG Release(void);
  HRESULT QueryInterface(REFIID riid, void **pObj);

  virtual void doSomething(void);
};

ULONG IMyClass::Release(void)
{
  ULONG ref;

  ref = IUnknonwn::Release();

  if (!ref && myobjptr)
  {  
    delete myobjptr;
    myobjptr = NULL;
  }

  // Quando retorna 0 a COM Library
  // faz um "garbage collection" e livra-se
  // da instância.
  return ref;
}

HRESULT IMyClass::QueryInterface(REFIID riid, void **pObj)
{
  // Se a interface que o usuário quer é
  // a de nosso objeto...
  if (riid == IID_MyClass)
  {
    // Cria o objeto interno
    if (!myobjptr)
    {
      myobjptr = new MyClassInternal;

      // Se não conseguiu alocar
      // objeto interno, retorna erro.
      if (!myobjptr)
      {
        // QueryInterface() exige isso,
        // em caso de erro.
        *pObj = NULL;

        // Retorna erro (E_NOINTERFACE é o ideal?)
        return E_NOINTERFACE;
      }
    }

    // Devolve o objeto, adiciona 1 à referência
    // e retorna S_OK.
    *pObj = this;
    AddRef();
    return S_OK;
  }

  return IUnknown::QueryInterface(riid, pObj);
}

// Wrapper.
void IMyClass::doSomething(void)
{ myobjptr->doSomething(); }

Na construção do objeto, via QueryInterface devemos instanciar o objeto da classe MyClassInternal, caso o GUID correto seja informado. Isso implica em realizar uma chamada a IUnknown::AddRef() para reference counting, que, de outra forma, seria feita pela função-membro base virtual, sobrecarregada.

As vantagens estão no fato que as funções-membro de MyClassInternal podem ser codificadas sem que tenhamos que nos preocupar com as regras impostas pela COM (OLE), a não ser no caso de multithreading… Ainda, no caso da sobrecarga de IUnknown::AddRef(), que criará a instância na sua primeira chamada, devemos também sobrecarregar IUnknown::Release() que se livrará de nosso objeto “interno” quando o contador chegar a zero, garantindo que não teremos memory leakage. A outra vantagem é que a classe interna pode ser desenvolvida de forma independente do contexto de um objeto COM. Ela pode até mesmo ser testada separadamente.

A desvantagem óbvia é que toda chamada é feita indiretamente e à partir de uma função virtual (implicando em tripla indireção). No entanto, não há motivos para que a função membro da classe interna também seja virtual…

7º: O seu objeto pode não estar no seu computador!

Quanto lidamos com COM ou OLE isso é importante: Os seus objetos podem estar em qualquer lugar onde haja maneira de comunicação… Por exemplo, o seu programa, que é um cliente de um objeto, pede ao Windows que instancie o objeto cuja identificação é IID_MyClass (um GUID). Graças às configurações no “arquivo de registro”, o sistema sabe que este objeto pode estar num servidor do outro lado do mundo, dai ele envia uma mensagem (marshalling) encapsulando tanto o tipo de objeto desejado quanto os métodos sendo chamados… Do outro lado, a OLE Library recebe a mensagem, a decodifica (unmarshalling), instancia o objeto desejado, chama a função membro indicada e monta uma mensagem de resposta (marshalling, de novo). A OLE Library, do seu lado, recebe a mensagem, decodifica (unmarshalling, de novo) e faz a função retornar o valor desejado.

Este caso de instanciamento fora do seu computador, chamamos de instancia Out-of-Process e o objeto cliente contém um “pseudo” objeto com as mesmas características do objeto original, mas sem a sua implementação. Trata-se de um “proxy” (ou um “procurador”, que age em benefício do cliente). Do lado do objeto real temos um “stub” (um “toco” ou “ponta”, em tradução livre), que receberá a mensagem e lidará com o objeto como se fosse o próprio cliente.

No caso do instanciamento ser feito na mesma máquina e na mesma thread, chamamos de In-Process, onde o par proxy/stub não é necessário:

Proxy/Stub

A falha em entender esse simples conceito causa grandes problemas no uso de COM…

8º: Modelo de threading errado

Nesses projetos que lidei não encontrei um objeto COM sequer que implementasse multithreading. Todos usavam o conceito de STA (Single Threaded Appartment). Isso porque este é o modelo mais simples, que deixa a COM Library lidar com a sincronização entre chamadas de várias threads para o mesma função-membro, bloqueando todas, exceto uma. De fato, apenas uma thread pode usar um objeto STA, se multiplas threads quiserem usar um objeto, cada uma delas terá que instanciar o seu…

O conceito de “Apartamento” é o mesmo do da vida normal: Existem apartamentos onde vivem pessoas sozinhas e outros onde vivem uma família com várias pessoas. No caso, não estamos falando de pessoas, mas threads.

É fácil desenvolver objetos COM em apartamentos de solteiros (STA), mas a desvantagem é que, num ambiente WEB, por exemplo, apenas um cliente terá acesso ao objeto por vez e, mesmo que vários clientes tenham seus próprios objetos, o consumo de recursos será enorme. Ou seja, seus objetos serão o “gargalo” de performance de todo o seu sistema.

Usar um modelo de threading diferente, como MTA (Multithreading Appartment) ou NTA (Neutral Threading Appartment) não é tarefa para o fraco de coração, especialmente porque a documentação detalhada sobre o assundo não é facilmente compreensível nas páginas da MSDN (ou seja, como diabos COM realmente funciona? De qualquer maneira, você pode ler muito aqui).

A escolha de um modelo de threading é essencial para que seus objetos possam ser usados com a máxima performance possível, de acordo com o ambiente. No caso de MTAs, o sincronismo entre threads deve ser feito pelo próprio objeto (usando critical sections, por exemplo), diferente das STAs, onde a COM Library usa um loop de mensagens para sincronia… Isso implica que o termo “Apartamento” é apenas um método de “marshaling” diferente, ou seja, de comunicação entre objetos (o cliente e o servidor, ou seja, a COM Library), o que torna todo o conceito ainda mais complicado… Por que essa comunicação? É que o objeto pode existir tanto no contexto do seu processo, quanto em algum outro processo ou até mesmo em um outro computador. COM pressupõe o uso de RPC (Remote Procedure Call), onde o objeto em uso pode estar, teóricamente, em qualquer lugar.

Não vou mostrar um objeto MTA e muito menos um NTA. A “neutralidade” foi uma modelo implementado no Windows 2000 para tornar MTAs ainda mais rápidos… Deixo esses detalhes para seus estudos ou, quem sabe, um dia volto a falar neles…

9º: Para complicar as coisas: COM+

Felizmente nunca peguei um projeto sério que usasse COM+… COM e OLE estão presentes no Windows desde a versão 3.1, para MS-DOS. COM+ é uma extensão do COM onde “transações” são incorporadas à complexidade do modelo. A ideia é criar objetos que permitam automatizar commits e rollbacks, do mesmo jeito que ocorre com bancos de dados. Garantindo que certas operações sejam atômicas.

Mas, sinceramente, COM e OLE já é um assunto complicado demais e eu quis ressaltar, ai em cima, que nos projetos que vi os desenvolvedores não faziam ideia do que fosse isso… Aliás, conheço poucos que fazem ideia, ainda hoje.

strlen_sse42() usando função “intrínseca”.

Pode parecer que a instrução PCMPISTRI seja meio complicada de usar em C, já que ela oferece dois resultados diferentes: ECX, contendo um índice de acordo com a comparação, e os flags. Mas, felizmente, o valor de ECX será 16 se InRes2 estiver totalmente zerado! Assim, a função anterior, escrita em assembly, pode ser reescrita em C assim:

/* test.c

  Compilar com:
    gcc -Ofast -msse4.2 -o test test.c
*/
#include <stddef.h>
#include <x86intrin.h>

size_t strlen_sse42_c(const char *s)
{
  unsigned int index;
  size_t result;
  static const char ranges[16] = { 1, 255 };

  result = 0;
  do
  {
    index = _mm_cmpistri(*(__m128i *)ranges, 
                         *(__m128i *)s,
                         _SIDD_UBYTE_OPS         | 
                         _SIDD_CMP_RANGES        | 
                         _SIDD_NEGATIVE_POLARITY |
                         _SIDD_LEAST_SIGNIFICANT);

    result += index;
    s += sizeof(__m128i);
  } while (count == 16);

  return result;
}

O código final ficará semelhante, mas menos performático, ao anterior:

bits 64

section .rodata

  align 16
_ranges:  db 1, 255
          times 14 db 0

section .text

global strlen_sse42:
  align 16
strlen_sse42:
  movdqa xmm0,[_ranges]
  xor    eax,eax

.loop:
  pcmpistri xmm0, [rdi], 0x0_01_01_0_0  
  mov    edx,ecx
  add    rdi,16
  add    rax,rdx
  cmp    ecx,16
  jz     .loop
  
  ret

Algumas diferenças óbvias: a instrução MOVDQA é mais rápida que MOVDQU e exige que o array _ranges esteja alinhado. Eu deveria ter previsto isso no código em assembly no artigo anterior… O compilador escolheu fazer DUAS comparações, como instruído. Como não temos como verificar o flag ZF à partir da função intrínseca _mm_cmpistri, só nos restava comparar o valor retornado com 16.

Agora… é evidente que PCMPISTRI só está disponível se seu processador suportar SSE 4.2. Um método bem simples de usar essa função OU a função padrão do compilador é este:

#include <stddef.h>
#include <string.h>
#include <x86intrin.h>

// Daqui para frente, strlen será chamada por esse ponteiro!
size_t (*__strlen)(const char *);

static size_t strlen_sse42_c(const char *s)
{ ... }

// Esse atributo faz com que a função seja executada
// ANTES de main(). É interessante ter apenas uma dessas
// funções em seu programa, embora o atributo permita definir
// a ordem de execução...
static __attribute__((constructor)) void ctor(void)
{
  if (__builtin_cpu_supports("sse4.2"))
    __strlen = strlen_sse42_c;
  else
    __strlen = strlen;
}

As chamadas a __stlen, evidentemente, serão sempre indiretas, mas assim você garante a compatibilidade entre processadores ao usar a rotina. Além do mais, a quase totalidade das funções da libc são chamadas de forma indireta, já que localizam-se em libc6.so.

Surpresa! Contar ciclos no ring0 não é tão bom assim!

Estive aqui testando um módulo para o kernel para medir os ciclos de clock gastos por uma função qualquer e comparando isso com a minha rotina para fazer o mesmo, no userspace. A rotina sob teste é simples:

/* test.c */

// Não vamos usar esse array fora daqui,
// por enquanto...
int a[100] = { 0 };

void f(void)
{
  for (int i = 1; i < 100; i++)
    a[i] = a[i-1] + 1;
}

No userspace basta usar as funções inline begin_tsc e end_tsc, como mostradas abaixo:

/* cycle_counting.h */

#ifdef __x86_64__
# define REGS1 "rbx", "rcx"
# define REGS2 "rax, "rbx", "rcx", "rdx"
#else
# ifdef __i386__
# define REGS1 "ebx", "ecx"
# define REGS2 "eax, "ebx", "ecx", "edx"
# else
#  error "Need x86-64 or i386 platforms to use cycle counting!"
# endif
#endif

unsigned long long __local_tsc;

inline void begin_tsc(void)
{
  unsigned int a, d;

  __asm__ __volatile__ ("xorl %%eax,%%eax\n"
                        "cpuid\n"
                        "rdtsc"
                        : "=a" (a), "=d" (d)
                        :: REGS1);

  __local_tsc = ((unsigned long long)d << 32) | a;
}

inline unsigned long long end_tsc(void)
{
  unsigned int a, d;

  __asm__ __volatile__ ("rdtscp\n"
                        "movl %%eax,%0\n"
                        "movl %%edx,%1\n"
                        "xorl %%eax,%%eax\n"
                        "cpuid"
                        : "=m" (a), "=m" (d)
                        :: REGS2);

  // Retorna a contagem de ciclos.
  return ((unsigned long long)d << 32) + a - __local_tsc;
}

Já para usá-las no kernelspace, temos que criar um módulo:

/* cyclemod.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/hardirq.h>
#include <linux/preempt.h>
#include "cycle_counting.h"

//extern int a[];
extern void f(void);

static int __init cyclecnt_start ( void )
{
  int i;
  unsigned long flags;
  unsigned long long c;

  printk ( KERN_INFO "Loading Cycle-Counting module...\n" );

  preempt_disable();
  raw_local_irq_save(flags);

  begin_tsc();
  f();
  c = end_tsc();

  raw_local_irq_restore(flags);
  preempt_enable();

  printk ( KERN_INFO "\tCycles: %llu\n", c);

  return 0;
}

static void __exit cyclecnt_end ( void )
{
  printk ( KERN_INFO "Goodbye Cycle-Counter.\n" );
}

module_init ( cyclecnt_start );
module_exit ( cyclecnt_end );

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Frederico L. Pissarra");
MODULE_DESCRIPTION("Cycle Counting Module");

Para compilar esse bicho, crie o makefile abaixo e simplesmente execute make. Você verá diversos arquivos, mas o módulo é o com extensão .ko (kernel object).

ccflags-y := -Ofast
obj-m := cyclescount.o
cyclescounter-objs := cyclemod.o test.o

KVERSION = $(shell uname -r)

all:
  make -C /lib/modules/$(KVERSION)/build M=$(PWD) modules

clean:
  make -C /lib/modules/$(KVERSION)/build M=$(PWD) clean

A diferença na rotina acima é que desabilitamos as interrupções para o processador local (provavelmente o kernel usa apenas a instrução CLI) e a preempção (não haverá chaveamento de tarefa para essa thread)… Isso deveria eliminar algum custo que existe no userspace e nos dar uma contagem mais “precisa”, mas veja só… Não é isso o que acontece:

$ ./cnttest    # userspace app
Cycles: 552
$ sudo insmod cyclecounter.ko
$ sudo rmmod cyclecounter
$ dmesg | sed -n '/Load.\+Cycle-Count/,+2p'
[17630.739459] Loading Cycle-Counting module...
[17630.739464] 	Cycles: 748
[17630.747001] Goodbye Cycle-Counter.

Como assim, no userspace obtive uma contagem menor de ciclos do que no kernspace?

E agora, Zé?!

Sempre dá para melhorar um bocado…

Eis duas formas de escrever uma função. A segunda forma fica meio “ofuscada”, mas, como veremos, o tamanho e performance valem a pena… A função é usada no T50, que deixei no primeiro modelo para ficar mais legível:

static const char *suffixes[] = { "st", "nd", "rd", "th" };

const char *get_ordinal_suffix(unsigned int n)
{
  /* 11, 12 & 13 have 'th' suffix, not 'st, nd or rd'. */
  if ((n < 11) || (n > 13))
    switch (n % 10)
    {
    case 1:
      return suffixes[0];
    case 2:
      return suffixes[1];
    case 3:
      return suffixes[2];
    }

  return suffixes[3];
}

Bem simples e aparentemente direto… O código gerado é esse:

bits 64

section .rodata

suffix1:   db "st",0
suffix2:   db "nd",0
suffix3:   db "rd",0
suffix4:   db "th",0

section .text

global get_oridinal_suffix
get_ordinal_suffix:
  lea  edx,[rdi-11]
  mov  eax,suffix4
  cmp  edx,2
  jbe  .L1

  ; Isso é n = n % 10!
  mov  eax,edi
  mov  edx,3435973837
  mul  edx
  shr  edx,3
  lea  eax,[rdx + rdx*4]
  add  eax,eax
  sub  edi,eax

  mov  eax,suffix2
  cmp  edi,2
  je   .L1
  mov  eax,suffix3
  cmp  edi,3
  je   .L1
  cmp  edi,1
  mov  edx,suffix4
  mov  eax,suffix1
  cmovne rax,rdx

.L1:
  ret

Não fique espantado com a mágica que o compilador faz para calcular o resto inteiro da divisão por 10. Ele prefere usar uma multiplicação do que divisão por questão de performance (multiplicações são lentas, mas divisões são verdadeiras lesmas!).

Na rotina acima não há muito como escapar do cálculo do resto da divisão por 10 por causa das duas exceções (11 e 12), mas podemos melhorar um cadinho o switch usando um macete:

const char *get_ordinal_suffix2(unsigned int n)
{
  if (n >= 11 && n <= 13) return suffixes[3];

  return suffixes["\003\000\001\002\003"
                  "\003\003\003\003\003"[n % 10]];
}

O uso de uma string literal como endereço base para a própria string é perfeitamente válido em C já que a string literal nada mais é do que a declaração dos seus bytes e o retorno é o endereço base. Note que cada “caracter” do array foi codificado como sendo o índice de outro array (suffixes) e, por isso, a rotina final será assim:

...
section .rodata

_lstr: db 3,0,1,2,3,3,3,3,3,3,3,3,3,3,3,3,0
suffixes: dq suffix1, suffix2, suffix3, suffix4

section .text
...
  global get_ordinal_suffix2
get_ordinal_suffix2:
  lea  edx,[rdi-11]
  mov  eax,suffix4
  cmp  edx,2
  jbe  .L1

  ; Isso ainda é n = n % 10!
  mov  eax,edi
  mov  edx,3435973837
  mul  edx
  shr  edx,3
  lea  eax,[rdx + rdx*4]
  add  eax,eax
  sub  edi,eax

  movsx eax,byte [_lstr + rdi]
  mov  rax,[suffixes + rax*8]
.L1:
  ret  

As 10 instruções do switch foram substituídas por apenas duas!

Aliás, quanto ao “macete” da string, acima, podemos fazer uso de coisas como essas sem problemas:

char c1 = "frederico"[5]; // c1 = 'r'.
char c2 = 5["frederico"]; // c2 = 'r';
char k = (n % 10)["9876543210"]; // desde que n seja unsigned.

Todas essas construções são perfeitamente válidas…

Meia precisão: Não disponível para todos!

O padrão IEEE 754:2008 define um tipo de “meia precisão”, ou seja, uma estrutura de 16 bits de tamanho com 11 bits significativos e expoente de 5 bits:

Este tipo é definido para alguns processadores (como alguns ARM), mas não está disponível para a plataforma Intel, e é conhecido como __fp16.

As limitações são óbvias… o ε é de 2^{-10} e o bias do expoente é de 15. Ou seja, o menor valor, subnormal, que pode ser especificado é de 2^{-10}\cdot2^{-14}=2^{-24} e o maior valor, normalizado, é de (2^{11}-1)\cdot2^{15-10}=65504 — O 15-10 vem do fato de considerarmos todos os bits significativos como setados e apenas como componente inteiros, é necessário deslocar o “ponto” em 10 bits para a direita.

E a quantidade de algarismos decimais significativos é de apenas 4: p_{10}=\left\lceil p_{2}\cdot\log 2\right\rceil\approx\left\lceil11\cdot0,31\right\rceil.

Ahhh… lembre-se que o expoente final da escala é calculado como E=e-bias e, portanto, o valor mínimo é E_{min}=0-15+1=-14, porque um valor subnormal é obtido com o mesmo expoente do menor valor normalizado possível (onde E=1). O expoente máximo é E_{max}=30-15=15. Os expoentes e=0 e e=31 (note o e minúsculo!) são reservados para subnormais e não-números, respectivamente (por isso E_{max} é obtido à partir de e sendo 30, e não 31 (0x1f)…

Esse tipo limitado de representação é útil em alguns casos… Em GPUs, por exemplo, o half precision floating point existe e pode ser usado para cálculos bem rápidos, desde que tenhamos certeza de obedecer a faixa de abrangência.