Problemas comuns portando código

Nos dias de hoje, onde só se fala em nuvem e o hardware está ficando mais barato é mais comum a necessidade de migrar aplicações entre plataformas, de Solaris e HP para Linux por exemplo, ou ainda mais comum ter a mesma aplicação sendo executada em diferentes plataformas e trocando informações entre si. Estamos falando aqui de aplicações rodando em servidores *nix se comunicando com clientes nas mais diversas plataformas (Mac, Windows, Linux, etc.). Se você esta trabalhando com Java em todas as pontas você praticamente não terá problemas ou terá muito poucos, porém se estiver usando C ou C++ aí a coisa é diferente.

A plataforma 64 bits não é uma novidade, ela foi apresentada ao mundo por volta dos anos 90 (o primeiro micro processador lançado rodando em 64 bits foi em 1991 um MIPS R4000), e embora já faça tanto tempo, muito softwares escritos naquela época e alguns ainda hoje não estão preparados para lidar com os problemas decorrentes desta plataforma.

Mas que problemas são esses ? Para enumerar apenas alguns principais, temos:

  • alocação de memória maior que 2GB
  • diferentes definições para os tipos básicos de inteiros
  • diferente alinhamento de bits das estruturas

para citar apenas alguns, agora vamos aos detalhes:

Alocação de memória maior que 2GB

se você tem ou utiliza uma função que aloca / retorna / acessa / modifica um array alocado dinamicamente (malloc ou variantes) você provavelmente utiliza um código parecido com isso para acessar os seus dados
[code lang=”c”]
for (int x = 0; x != width; ++x)
{
for (int y = 0; y != height; ++y)
{
for (int z = 0; z != depth; ++z)
{
BigArray[z * width * height + y * width + x] = InitValue;
}
}
}
[/code]

você deve estar tentando imaginar qual seria o problema aqui, esse código funcionava bem, mas agora em 64 bits começou a dar problema e você não tem a mínima idéia do que do pode ser. Pois bem, se você olhar mais de perto, verá que todas as variáveis utilizadas no índice, são inteiros, então o resultado de

[code lang=”c”]z * width * height + y * width + x[/code]

também é um inteiro, e provavelmente você terá o endereço incorreto se estiver alocando mais de 2GB.

Diferentes definições para os tipos básicos de inteiros

Qual o tamanho de um long ?? Se você respondeu prontamente “o tamanho de um long é 8”, ou 4, ou qualquer outro valor, pasme, mas esta não é a resposta correta. A resposta correta é outra pergunta “em qual plataforma ?” Antes de achar que estou louco e parar de ler o artigo por aqui, você deve saber o seguinte, a ISO do C/C++ define o seguinte
[code lang=”c”]
sizeof(char) == 1
sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) [/code] e isso apenas isso, a ISO do C/C++ não define que um int deve ser 4 bytes e um long 8 bytes, apenas define que a regra acima deve ser seguida. Agora você deve estar se perguntando, "mas como vou saber então qual o tamanho do meu inteiro ?", e eu respondo, existem 2 formas, a primeira é olhar o arquivo "limits.h" ou "limits" ou ainda "limit" ou o equivalente para o seu Sistema Operacional e compilador. A segunda e mais simples, é fazer um sizeof e ver o que o programa imprime.

[code lang=”c”]
std::cout << "tamanho do long: " << sizeof(long) <<  std::endl [/code]

Diferente alinhamento de bits das estruturas

Vamos assumir por um momento que:
[code lang=”c”]
sizeof(char) = 1
sizeof(shot) = 2
sizeof(int) = 4
[/code]
como foi dito na sessão anterior os valores podem mudar de acordo com o SO e compilador, mas vamos assumir esses valores para o próximo exemplo. suponha a seguinte estrutura:
[code lang=”c”]
struct
{
char a;
short b;
int c;
} data;
[/code]

rodando o sizeof nela temos como resultado o tamanho 8, vamos ver porque:

+-----------------------------------+
|char| ** |  short |       int      |
+-----------------------------------+
0         2        4                8

char ocupa 1 byte, temos 1 byte vazio (representado pelos asteriscos), depois o short ocupando 2 bytes e finalmente o int ocupando 4 bytes. Mas por que isso acontece ? Isso acontece, porque os dados de estruturas são alocados utilizando espaçamento em potência de 2 ou seja, o char ocupou a posição zero, com tamanho 1, o próximo disponível é a posição 2, e assim por diante. Sabendo disso vamos ver o que acontece, quando mudamos nossa estrutura para o seguinte:

[code lang=”c”]
struct
{
char a;
int c;
short b;
} data;
[/code]

apenas alteramos a declaração da estrutura, alterando a ordem dos elementos, colocando o int logo após o char. Mas uma vez rodando o sizeof temos o tamanho de 12, mas como isso é possível ? Vamos ao desenho:

+---------------------------------------------+
|char| ** | ** | ** |       int      |  short |
+---------------------------------------------+
0         2        4                8         12

agora vemos que o char continuou ocupando 1 byte, e continuamos perdendo mais 1, porém o int não pode começar na posição, pois ele precisa de 4 bytes, então ele foi para a próxima posição disponível em que fosse possível alocar 4 bytes, a posição 4, desperdiçando assim mas 2 bytes; logo em seguida temos o nosso short. Não sei se vocês notram, mas o short ocupou 4 bytes ao invés dos 2 bytes da estrutura anterior. Isso aconteceu  porque os elementos devem ser alinhados, seguindo o alinhamento do maior membro, nesse caso o int. Agora que você conhece essas regras, você também sabe que pode otimizar o tamanho da sua estrutura agrupando os membros utilizando o tamahno e o tipo, assim se precisarmos adicionar mais 1 char à estrutura original, podemos fazer da seguinte forma:

[code lang=”c”]
struct
{
char a;
char d;
short b;
int c;
} data;
[/code]

e com isso utilizar aquele byte que estava perdido e mantemos o tamanho da estrutura como 8 bytes, se colocarmos o novo char no final da estrutura (após o int) ele seria alinhado utilizando o tamanho do int, neste caso 4 bytes, e a estrutura teria tamanho 12. Dessa forma teremos:

+-----------------------------------+
|char|char|  short |       int      |
+-----------------------------------+
0         2        4                8

Isso é importante para sistemas que utilizam comunicação via rede e precisam trocar mensagens com diferentes plataformas, uma vez que o alinhamento e tamanho depende da plataforma e do compilador. Pensando ainda no alinhamento, com certeza seria interessante ter uma estrutura de 8 bytes ao invés de 12 bytes em uma plataforma móvel por exemplo. Apesar desse alinhamento com potência de 2 ser o padrão, você dizer ao compilador para tentar otimizar o alinhamento utilizando uma prerrogativa de compilação da seguinte forma

[code lang=”c”]
#pragma pack(push)
#pragma pack(2)
struct
{
int c;
short b;
} data;
#pragma pack(pop)
[/code]

dessa forma, a estrutura que teria tamanho 8 bytes, terá o tamanho de 6, se o compilador suportar o pack. A maioria dos compiladores de hoje suportam esse parâmetro, e o gcc permite até passar o parâmetro como uma opção para o compilador via linha de comando:
[code lang=”c”]-fpack-struct[=n][/code]
onde “n” deve ser um número correspondente a portência de 2, e significa que membros que precisem de um tamanho maior que o especificado PODEM ficar fora do alinhamento. Pessoalmente eu prefiro a anotação no código, pois fica mais explícito para a próxima pessoa que dará manutenção no código.

Espero ter dado uma pequena visão de alguns problemas que frequentemente ocorrem quando um sistema está sendo portado de uma plataforma para a outra, especialmente quando falamos de 32 e 64 bits, mas não recebem a devida atenção antes de começar a codificação.

One Reply to “Problemas comuns portando código”

  1. puxa… complicado… não entendi muita coisa (mas é claro, já não estou tão envolvido nesse mundo)
    por outro lado, tem muita coisa q dah para entender… deu para sacar o tamanho da pica!! uhAHUAHUUHA
    =) muito bom!!!
    aquele abraço!!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.