sábado, 15 de maio de 2010

Delphi strings (O que, quando e como) - Otimizações

As versões mais recentes do Delphi oferecem pelo menos 4 tipos de strings. Cada uma delas com características e comportamentos próprios, tendo sua aplicabilidade destinada a situações diferentes.
Esse artigo está dividido em duas partes. A primeira irá descrever esses quatro tipos de strings e suas características; a segunda etapa é composta por análises de situações comuns na programação e que constituem oportunidades para um otimizações.


Breve Histórico

ShortString
Este é um tipo de string oriundo das primeiras versões do Delphi e herdado do Turbo Pascal, que possuia este como o único tipo de string.
A menos que uma alocação manual seja feita, o tipo ShortString reside na stack e não na heap e, do ponto de vista de alocação de memória, tem o mesmo comportameto que os tipos básicos alocados estaticamente (integer, boolean, char, recors, enum, ...).
Quando o tipo ShortString é utilizado o Delphi pré-aloca um bloco de 256 bytes e utiliza o primeiro byte (AStr[0]) para armazenar o tamanho 'utilizado'.

var
  AStr: ShortString;


Você também pode especificar um tamanho máximo para as strings, mas esse valor não pode passar 255.

var
  s: string[50];
  e: string[256]; // error

PChar
A limitação de 255 bytes para string representa um fator crítico para aplicações do mundo real.
No Delphi 1 foi introduzido o tipo PChar que era um tipo semelhante ao "char *" da linguagem C. Entretanto, devido a segmentação da memória do Windows ser de 16-bits, o tipo PChar estava limitado a 65535 bytes (64 KB).
Diferentemente do tipo ShortString, PChar não possui um "campo" destinado a armazenar o tamanho da string, mas possui um terminador "null".



Long String (AnsiString)
No Delphi 2 foi introduzido o tipo AnsiString com a finalidade de prover uma forma eficiente e rápida de trabalhar com strings grandes (32-bits). Agora era possível manipular strings com até 2 GB.
Comparando a estrutura do tipo AnsiString podemos dizer que ele é uma "forma híbrida" dos tipos PChar e ShortString. Uma porque ele utiliza um terminador "null" para indicar o final da string (igual ao PChar) e, segundo, porque adota um campo para armazenar o tamanho da string e o primeiro caracter inicia na posição 1.

var
  s: AnsiString;


Observe o mapa de memória da variável s, além do campo "Lenght" esse tipo de sring mantém um outro campo, "RefCount". Resumidamente, esse campo é incrementado sempre que a variável é referencida. Isso permite ao Delphi gerenciar o tempo de vida da string, liberando a memória quando a string não mais é utilizada.


WideString
WideString foi o último tipo de string adicionado à famíla Delphi, mais precisamente, introduzido na versão 6. No entanto, somente a partir da versão 2009 é que se tornou o tipo padrão de string.
A finalidade do tipo WideString é o suporte a caracteres Unicode e a diferença reside no fato de que cada caracter WideString é representado por 2 bytes e não 1, como em AnsiString.

var
  s: WideString;



O tipo String

No Delphi, o tipo string não é nada mais do que um alias para ShortString, PChar, AnsiString ou WideString, dependendo da versão do Delphi que for utilizado.
Por exemplo, no Delphi 7 o tipo string é equivalente a AnsiString; já no Delphi 2009 e, mas recentemente, no Delphi 2010, string é equivalente a WideString.
O uso do tipo string deve ser aplicado com um pouco de cautela, principalmente se retro-compatibilidade ou portabilidade for uma das necessidades do código fonte produzido.
Tomamos por exemplo o lançamento do Delphi 2009, que trouxe aos usuários o desafio da migração do código fonte de manipulação de strings para o formato wide.


Semântica e comportamento

Para o correto uso das string é necessário entender sua semântica e seu comportamento.

a) Exemplo 1
Lembre-se, o tipo ShortString pre-aloca 256 bytes e operações de atribuição resultarão em cópia do conteúdo fonte.

var
  s: ShortString;
  b: ShortString;
begin
        ...
  s := 'Teste';
  b := s;     // O conteúdo de 's' é copiado para 'b'
  s[1] := 'X' // A variável 'b' ainda contém 'Teste'


b) Exemplo 2
O tipo PChar está frequentemente associado a alocação dinânimica e atribuições somente copiam o ponteiro do destino, não o conteúdo.

var
  s: PChar;
  b: PChar;
begin
        ...
  s := 'Teste';
  b := s;     // A variável 'b' aponta para 's'
  s[0] := 'X' // Tanto 's' quanto 'b' contém 'Xeste'


c) Exemplo 3
Tanto o tipo AnsiString quanto WideString são semelhantes ao tipo PChar quando uma atribuição direta é feita, ou seja, somente é copiado o ponteiro do destino. Além disso, AnsiString e WideString possuem um campo RefCount (usado para saber quando a string não é mais utilizada) que, na atribuição direta, é incrementado..
Observe o exemplo baixo.

var
  s: AnsiString;
  b: AnsiString;
begin
        ...
  s := 'Teste';
  b := s;     // Ambas apontam para a mesma área de memória e
              //o campo "RefCount' é incrementado.


d) Exemplo 4
Vamos tomar o mesmo exemplo anterior, mas com uma linha a mais de código.

var
  s: AnsiString;
  b: AnsiString;
begin
        ...
  s := 'Teste';
  b := s;      // b recebe o ponteiro de 's'e o RefCount é incrementado.
  s[1] := 'X'; // Faz uma cópia de 'b' com o primeiro byte modificado e
               //decrementa o RefCount de 'b'.

Vamos detalhar o trabalho realizado pelo Delphi em cada uma das linhas ilustrando as estruturas em memória.

Step 1)
  s := 'Teste';


É feito a alocação de memória para armazena a string. O campo "Lenght" é setado para 5 (que é o número de caracteres da string) e o campo "RefCount" é setado para 1.
A variável 's' passa a apontar para o início da string.

Step 2)
  b := s;

Observe que a linha de código acima simplesmente copiou o ponteiro da variável 's' para a variável 'b' e incrementou o campo 'RefCount', ou seja, abas as variáveis estão apontando para a mesma área de memória.

Step 3)
  s[1] := 'X';


Aqui a alteração de um simples byte resultou e várias instruções.
Pelo fato do "RefCount" ser superior a 1, ou seja, por haver mais do que uma referência, foi realizada a alocação de uma nova string; copiado o conteúdo da string original; setado 1 para o "RefCount"; alterado o primeiro byte para 'X'; atribuído o ponteiro da nova string para a variável 's'.
Por último, o 'RefCount' da string original é decrementado.



Otimizações no uso de strings

Uso de parâmetros do tipo const
A plavrava reservada "const" é um modificador usado para definir algo como estático, que não muda. E quando associadas com parâmetros do tipo string, há um incremento no desempenho que pode ser perceptível, dependendo da intensidade de uso.
Vamos tomar como base uma função simples.

function GetStrSize(s: string): Integer;
begin
  Result := Length(s);
end;;

Aqui o parâmetro "s" irá incondicionalmente incrementar o campo "RefCount" da string a qual ele está referenciando antes de iniciar a execução da primeira linha de código da função.
Pode parecer um tarefa simples e pouco honerosa para o sistema, mas é totalmente desnecessária porque o parâmetro é somente utilizado para leitura e nunca para escrita.
Uma código fonte mais eficiente, nesse caso, é facilmente obtido apenas especificando o parâmetro como "constante".

function GetStrSize2(const s: string): Integer;
begin
  Result := Length(s);
end;

A boa notícia é que as otimizações implementada nos compiladores mais recentes já detectam esse tipo de situação e geram um código final mais eficiente.


Evitando retorno do tipo string
Quando você for codificar uma função na qual é pretendido retornar uma string modificada da que é passada por parâmetro, é aconselhado que seja feito através do próprio parâmetro. A menos, é claro, que você necessite do valor original.

function AddChar(const s: string): string;
begin
  Result := s + '*';
end;

O exemplo acima, como pode ser notado, apenas acrescenta um caracter no final da string passa por parâmetro.
Já a função abaixo faz a mesma coisa, mas retorna a nova string através do próprio parâmetro.

procedure AddChar2(var s: string);
begin
  s := s + '*';
end;

Tanto o modificador "const" da primeira função quanto o modificador "var" da segunda função fazem com que não seja necessário o incremento do "RefCount". Entretanto a segunda função é mais eficiente porque não requer que a string do parâmetro seja duplicada, porque a concatenação é feita diretamente na string original.


Postergando condicionais de comparação de string
Uma situação bem corriqueira no dia-a-dia, principalmente quando se está trabalhando com algum tipo de parser, é codificar testes condicionais com mais do que uma validação onde há comparações string.

var
  lFailed: Boolean;
  lText: string;
begin
  ...
  if (lText = '') and (not lFailed) then
  ...
end;

No exemplo acima o primeiro teste do "if" faz uma comparação entre string e o segundo é uma comparação que envolve uma variável booleana.
Sabendo que uma comparação entre string é uma tarefa "onerosa" para o processador, o mais sensato é alterarmos a ordem das condições de forma que os testes mais simples sejam atendidos primeiros. Isso evita, nesse nosso exemplo, uma comparação de string desnecessária quando a variável lFailed for False.
Esse é um tipo de Boas Práticas de Programação pode ser extentido para casos de forma geral que exigam um processamento extra.


Redundância de chamadas
Por mais otimizado que a biblioteca do Delphi esteja, a chamada a uma função de manipulação de string sempre impõem alguma penalidade se comparado com operações simples, e evitar chamadas desnecessárias ou redundantes certamente é um cuidado bem vindo.

O código abaixo é um pequeno trecho retirado de uma método de validação de email. Podemos notar o uso repetido e redundante da função "Pos".

function IsValidEmail(const EMail: string): Boolean;
begin
  ...
  if (Pos('@', EMail) <> 0) and (Pos('.', EMail) <> 0) then
  begin
    if (Pos('@', EMail) = 1) or (Pos('@', EMail) = Length(EMail)) or
        (Pos('.', EMail) = 1) or (Pos('.', EMail) = Length(EMail)) or (Pos(' ', EMail) = 0) then
      Result := False
  ...

Esse é um exemplo simples que utiliza funções específicas para tratamento de strings, mas o problema não é exclusivo e estende-se para qualquer outro tipo de redundância.

function IsValidEmail(const EMail: string): Boolean;
var
  lPos_Dot: Integer;
  lPos_Arroba: Integer;
begin
  ...
  lPos_Dot := Pos('.', EMail);
  lPos_Arroba := Pos('@', EMail);

  if (lPos_Arroba <> 0) and (lPos_Dot <> 0) then
  begin
    if (lPos_Arroba = 1) or (lPos_Arroba = Length(EMail)) or
       (lPos_Dot = 1) or (lPos_Dot = Length(EMail)) or (Pos(' ', EMail) = 0) then
      Result := False
  ...

A simples adição de duas variáveis locais evitou quatro chamadas desnecessárias à função "Pos".


Verificando se uma string está vazia
Existem muitas maneiras de conferir se uma string é ou não vazia, mas você sabe qual delas é a mais eficiente?
Abaixo eu apresento três das formas mais freqüentemente empregadas.

var
  lMyStr: string;
begin
  lMyStr := Caption;
  if lMyStr = '' then
    lMyStr := 'Maneira mais eficiente';
  if Length(lMyStr) = 0 then
    lMyStr := 'Maneira menos eficiente';
  if lMyStr[1] = '' then
    lMyStr := 'Forma desaconselhada';

Para entender melhor a verdadeira razão do primeiro if conter a forma mais adequada de verificar se um string está ou não vazia, vou postar o código assemble gerado pelo compilador. Mesmo você não estando familiarizado com a linguagem assembly, tenho certeza que será fácil o entendimento.

O código gerado pelo compilador é apresentado logo abaixo da linha do código fonte respectiva e cada um dos  if acima está destacado em uma cor diferente.

fuStrTest.pas.127: if lMyStr = '' then
07992023 837DFC00         cmp dword ptr [ebp-$04],$00
07992027 750D             jnz $07992036
fuStrTest.pas.128: lMyStr := 'Maneira mais eficiente';
07992029 8D45FC           lea eax,[ebp-$04]
0799202C BADC209907       mov edx,$079920dc
07992031 E842F0FFFF       call $07991078
fuStrTest.pas.129: if Length(lMyStr) = 0 then
07992036 8D45FC           lea eax,[ebp-$04]
07992039 E822F0FFFF       call $07991060
0799203E E83DF0FFFF       call $07991080
07992043 85C0             test eax,eax
07992045 750D             jnz $07992054
fuStrTest.pas.130: lMyStr := 'Maneira menos eficiente';
07992047 8D45FC           lea eax,[ebp-$04]
0799204A BA18219907       mov edx,$07992118
0799204F E824F0FFFF       call $07991078
fuStrTest.pas.131: if lMyStr[1] = '' then
07992054 8B45FC           mov eax,[ebp-$04]
07992057 66833800         cmp word ptr [eax],$00
0799205B 750D             jnz $0799206a
fuStrTest.pas.132: lMyStr := 'Forma desaconselhada';
0799205D 8D45FC           lea eax,[ebp-$04]
07992060 BA54219907       mov edx,$07992154
07992065 E80EF0FFFF       call $07991078

Somente observando o número de instruções necessárias para cada uma das três situações já é suficiente para comprovar que o primeiro if é o mais eficiente. Entretanto o segundo if, que utiliza a função Length, merece alguns comentários.
Observe que há duas instruções call (call $07991060 e call $07991080) que são, na verdade, chamadas para as funções EnsureUnicodeString e UStrLen, respectivamente. Essas funções são compostas por diversas instruções e, do ponto de vista de micro otimizações, impõem uma penalidade de performance bastante grande.
Mesmo não tento realizados testes estatísticos mais precisos, posso afirmar que um simples if lMyStr = '' then é incontáveis vezes mais rápido que o uso da função Length.
Novamente ressaltando que isso é do ponto de vista de micro otimizações e para a maioria dos desenvolvedores o ganho de desempenho seria imperceptível.