Porting applicazioni a 64 bit

Categories: Programming


Con l’avvento dei moderni processori a 64 bit (in primis INTEL Itanium e AMD Opteron) nasce l’esigenza per molte applicazioni di essere “portate” a 64 bit. Un processore a 64 bit è un processore dotato di registri interni ed ALU (Arithmetic Logic Unit) a 64 bit. Tra i principali vantaggi di un processore a 64 bit :
1 – La possibilità di indirizzare direttamente 4 exabyte (2^63) di memoria (più memoria a
disposizione, e non solo); I processori a 32 bit possono indirizzare al massimo 4 Gbyte.
2 – Dimensioni dei file. Un processore a 64 bit consente dimesioni fino a 4 exabyte, mentre i 32 bithanno un limite a 2 Gbyte (2^31).
3 – Operazioni matematiche che sfruttano 64 bit sono possibili sui processori a 32 bit ma
ovviamente su macchine a 64 bit tali operazioni risulteranno più veloci
4 – Un altro aspetto è il formato della data nei sistemi linux che è espressa con una variabile intera a 32 bit con un limite sul range temporale gestito (fino al 2038)

Come avrete notato ho omesso la questione velocità poiché c’è una diatriba su questo aspetto. Una cosa è certa però, usare applicazioni a 32 bit su architetture a 64 bit è dispendioso dal punto di vista delle prestazioni poiché il processore garantisce una sorta di compatibilità ma deve compiere più lavoro; quello che si deve fare è ricompilare il sorgente dell’applicazione su una macchina a 64 bit (a patto che il sorgente sia scritto “ad arte” e di seguito vedremo gli aspetti da considerare) ma alla fine della compilata noteremo che la nostra applicazione “pesa” di più proprio per effetto del fatto che contiene più dati (se prima un intero era 32 bit ora è 64 bit). La questione più significativa da tenere in considerazione nel porting di applicazioni a 64 bit è sicuramente quella relativa ai puntatori e la dimensione degli interi. Comuqnue per un porting “sicuro” o comunque completo andrebbero valutati i seguenti aspetti :

1 – Dichiarazioni di variabili.
2 – Espressioni
3 – Assegnazioni
4 – Costanti
5 – Endianesimo
6 – Definizioni di tipi
7 – Parametri di funzioni

Che tratterò nei relativi paragrafi. Comincerei con i vari modelli di dati :

Ogni modello di dati indica la dimensione che le variabili possono assumere ed è una scelta da fare in fase di compilazione. Il modello LP64 (ad esempio) indica che i Long e i Puntatori hanno dimensione 64 bit mentre LLP64 indica che i Long Long e i Puntatori hanno dimensione 64 bit. Da precisare che i tipi in virgola mobile (float e double) sono fuori da questa tabella per via del fatto che hanno dimensione fissa 32 e 64 bit rispettivamente (in entrambe le architetture. Il modello dei dati è importante perché ci permette di capire come sono rappresentati (allineati) i dati in memoria. Di fatti il compilatore allinea le strutture dati ai limiti naturali dell’architettura sottostante, una struttura verrà allineata ai limiti di 64 bit per un’architettura a 64 bit. Esempio :

struct myTest
{
int i1;
double d;
int i2;
};

Il compilatore inserisce dei padding per allineare la struttura ai limiti che l’architettura stessa gestisce “naturalmente”, ovvero senza che il processore si debba spiazzare per prendere solo 32 bit all’interno dei 64 che naturalemente raggiungerebbe. Dichiarazioni di variabili. Per far in modo che un’applicazione possa lavorare a 32 e 64 bit gli accorgimenti da prendere in fase di dichiarazione di variabili sono :

* Usare appropriatamente i suffissi “L” ed “U” per le costanti intere.
* Usare unsigned int in modo appropriato per evitare estensioni di segno.
* Preferire variabili di tipo int e long invece di char per incrementare le preformance (vedi
allineamento).
* Utilizzare per i puntatori a caratteri il suffisso unsigned per evitare estensioni di segno.

Espressioni
Per quanto riguarda le espressioni bisogna ricordarsi che :
* La somma di 2 interi con segno restituisce un intero con segno.
* La somma di un intero con un long int restituisce un long (stessa cosa per gli altri tipi).
* La somma di un unsigned con un signed restutuisce un unsigned.

Assegnazioni
Nei sistemi con architettura a 64 bit puntatori int e long non hanno la stessa dimensione (vedi modello dei dati) quindi i problemi maggiori possono nascere nell’assegnazione di variabili che nel modello a 32 bit avevano stessa dimensione mentre nelle architetture a 64 bit no. Quindi qualche raccomandazione :
* Non assegnare variabili di tipo long a variabili di tipo int.
* Non memorizzare puntatori in variabili di tipo int.
* Non memorizzare interi in variabili di tipo puntatore.
* Attenzione nel mischiare tipi signed e tipi unsigned. Proprio per il punto visto in “Espressioni” il risultato tra un signed e un unsigned da unsigned quando si ha a che fare con espressioni con tipi signed ed unsigned conviene opportunamente castare :

long tot;
int first = -2
unsigned second = -1;
n = i + k; // –> castare opportunamente : n = (long) i + k;

Endianesimo
L’endianesimo si riferisce al modo in cui sono memorizzati/organizzati i dati in memoria. Possono essere organizzati in little endian o big endian.
* Little endian : il byte meno significativo è memorizzato a partire dalla locazione di memoria più bassa.
* Big endian : Il byte più significativo è memorizzato a partire dalla locazione di memoria più bassa. A titolo di esempio processori come intel o amd lavorano con modalità little endian; processori motorola big endian. L’endianesimo è importante, quindi, quando si mettono in comunicazione sistemi eterogenei. Il formato rete è big endian ovverose si spedisce un pacchetto su rete lo si deve spedire nel formato big endian (formato rete appunto). Ci sono delle funzioni che aiutano nella trasformazione tra formato rete e formato host : hton16, hton32, ntoh16, ntoh32. Quando si lavora con sistemi a 64 bit non ci sono funzioni standard come le precedenti e quindi ci vengono incontro delle macro (definite nei sistemi linux) : bswap_16, bswap_32, bswap_64.

Esempio : Il valore 1 (decimale) = 0x0001 (00=MSB, 01=LSB) lo rappresentiamo in memoria nel seguente modo:
little big
0x1 1   0x1 0
0x2 0   0x2 1

Tipi ILP32 LP64 LLP64
char 8 8 8
short 16 16 16
int 32 32 32
long 32 64 32
long long 64 64 64
pointer 32 64 64

Definizioni di tipo
La raccomandazione è d’obbligo per quanto riguarda l’uso dei tipi nativi. Per una questione di portabilità non conviene utilizzare direttamente nel codice I tipi definiti dal linguaggio ma fare un header file con un wrapper dei tipi per semplificare un futuro porting.


    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.