Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Questo documento descrive alcuni dei problemi comuni che possono verificarsi durante la migrazione del codice dalle architetture x86 o x64 all'architettura arm. Descrive anche come evitare questi problemi e come usare il compilatore per identificarli.
Annotazioni
Quando questo articolo fa riferimento all'architettura arm, si applica sia ad ARM32 che a ARM64.
Origini dei problemi di migrazione
Molti problemi che possono verificarsi quando si esegue la migrazione del codice dalle architetture x86 o x64 all'architettura arm sono correlati a costrutti di codice sorgente che potrebbero richiamare un comportamento non definito, definito dall'implementazione o non specificato.
Il comportamento non definito è il comportamento che lo standard C++ non definisce e che è causato da un'operazione che non ha alcun risultato ragionevole: ad esempio, la conversione di un valore a virgola mobile in un intero senza segno o lo spostamento di un valore in base a un numero di posizioni negative o superiori al numero di bit nel tipo alzato di livello.
Il comportamento definito dall'implementazione è il comportamento che lo standard C++ richiede al fornitore del compilatore di definire e documentare. Un programma può basarsi in modo sicuro sul comportamento definito dall'implementazione, anche se questa operazione potrebbe non essere portabile. Esempi di comportamento definito dall'implementazione includono le dimensioni dei tipi di dati predefiniti e i relativi requisiti di allineamento. Un esempio di operazione che potrebbe essere influenzata dal comportamento definito dall'implementazione consiste nell'accedere all'elenco di argomenti delle variabili.
Il comportamento non specificato è un comportamento che lo standard C++ lascia intenzionalmente non deterministico. Anche se il comportamento è considerato non deterministico, determinate chiamate di comportamento non specificato sono determinate dall'implementazione del compilatore. Non è tuttavia necessario che un fornitore del compilatore predetermini il risultato o garantisca un comportamento coerente tra chiamate confrontabili, e non esiste alcun requisito per la documentazione. Un esempio di comportamento non specificato è l'ordine in cui vengono valutate le sottoespressione, che includono argomenti di una chiamata di funzione.
Altri problemi di migrazione possono essere attribuiti alle differenze hardware tra le architetture ARM e x86 o x64 che interagiscono con lo standard C++ in modo diverso. Ad esempio, il forte modello di memoria dell'architettura x86 e x64 fornisce alle variabili volatile-qualificate alcune proprietà extra che sono state usate per facilitare determinati tipi di comunicazione inter-thread in passato. Ma il modello di memoria debole dell'architettura ARM non supporta questo uso, né lo standard C++ lo richiede.
Importante
Anche se volatile ottiene alcune proprietà che possono essere usate per implementare forme limitate di comunicazione tra thread in x86 e x64, queste proprietà non sono sufficienti per implementare la comunicazione tra thread in generale. Lo standard C++ consiglia di implementare tale comunicazione usando invece primitive di sincronizzazione appropriate.
Poiché piattaforme diverse potrebbero esprimere questi tipi di comportamento in modo diverso, la conversione di software tra piattaforme può essere difficile e soggetta a bug se dipende dal comportamento di una piattaforma specifica. Anche se molti di questi tipi di comportamento possono essere osservati e potrebbero apparire stabili, basarsi su di essi è almeno non portabile e, nei casi di comportamento non definito o non specificato, è anche un errore. Anche il comportamento citato in questo documento non deve essere basato su e potrebbe cambiare nelle future implementazioni di compilatori o CPU.
Problemi di migrazione di esempio
Nella parte restante di questo documento viene descritto il modo in cui i diversi comportamenti di questi elementi del linguaggio C++ possono produrre risultati diversi su piattaforme diverse.
Conversione di un numero in virgola mobile a un intero senza segno
Nell'architettura ARM, la conversione di un valore a virgola mobile in un intero a 32 bit satura al valore più vicino che l'intero può rappresentare se il valore a virgola mobile non è compreso nell'intervallo che l'intero può rappresentare. Nelle architetture x86 e x64, la conversione esegue il wrapping se l'intero è senza segno, oppure viene impostata su -2147483648 se l'intero è con segno. Nessuna di queste architetture supporta direttamente la conversione di valori a virgola mobile in tipi integer più piccoli; Le conversioni vengono invece eseguite a 32 bit e i risultati vengono troncati a dimensioni inferiori.
Per l'architettura ARM, la combinazione di saturazione e troncamento significa che la conversione in tipi senza segno satura correttamente i tipi senza segno più piccoli quando satura un intero a 32 bit, ma produce un risultato troncato per i valori che sono maggiori di quelli che il tipo più piccolo può rappresentare ma troppo piccoli per saturare l'intero a 32 bit. La conversione è anche correttamente saturata per interi con segno a 32 bit, ma il troncamento di interi con segno saturi ha come risultato -1 per i valori saturati positivamente e 0 per i valori saturati negativamente. La conversione in un intero con segno più piccolo produce un risultato troncato imprevedibile.
Per le architetture x86 e x64, la combinazione di comportamento di riavvolgimento per le conversioni di interi senza segno e la valutazione esplicita per le conversioni di interi con segno in caso di overflow, insieme al troncamento, rende i risultati per la maggior parte degli spostamenti imprevedibili se sono troppo grandi.
Queste piattaforme differiscono anche nel modo in cui gestiscono la conversione di NaN (Not-a-Number) in tipi integer. Su ARM, NaN viene convertito in 0x00000000; su x86 e x64, viene convertito in 0x80000000.
Si può fare affidamento sulla conversione a virgola mobile solo se si sa che il valore rientra nell'intervallo del tipo intero a cui viene convertito.
Comportamento dell'operatore Shift (<<>>)
Nell'architettura arm, un valore può essere spostato a sinistra o a destra fino a 255 bit prima che il modello inizi a ripetersi. Nelle architetture x86 e x64, il modello viene ripetuto a ogni multiplo di 32, a meno che l'origine del modello non sia una variabile a 64 bit. In tal caso, il modello si ripete a ogni multiplo di 64 su x64 e ogni multiplo di 256 su x86, in cui viene usata un'implementazione software. Ad esempio, per una variabile a 32 bit con valore 1 spostato a sinistra di 32 posizioni, su ARM il risultato è 0, su x86 il risultato è 1 e su x64 il risultato è anche 1. Tuttavia, se l'origine del valore è una variabile a 64 bit, il risultato su tutte e tre le piattaforme è 4294967296 e il valore non si avvolge fino a quando non viene spostato di 64 posizioni su x64 o di 256 posizioni su ARM e x86.
Poiché il risultato di un'operazione di spostamento che supera il numero di bit nel tipo di origine non è definito, il compilatore non deve avere un comportamento coerente in tutte le situazioni. Ad esempio, se entrambi gli operandi di uno spostamento sono noti in fase di compilazione, il compilatore può ottimizzare il programma usando una routine interna per precompilare il risultato dello spostamento e quindi sostituendo il risultato al posto dell'operazione di spostamento. Se la quantità di spostamento è troppo grande o negativa, il risultato della routine interna potrebbe essere diverso dal risultato della stessa espressione di spostamento eseguita dalla CPU.
Comportamento degli argomenti variabili (varargs)
Nell'architettura arm i parametri dell'elenco di argomenti variabili passati nello stack sono soggetti all'allineamento. Ad esempio, un parametro a 64 bit è allineato su un limite a 64 bit. Su x86 e x64, gli argomenti passati nello stack non sono soggetti all'allineamento e occupano spazio in modo compatto. Questa differenza può causare una funzione variadica come printf a leggere indirizzi di memoria destinati a essere semplicemente riempimenti su ARM se la disposizione prevista dell'elenco degli argomenti variabili non corrisponde esattamente, anche se potrebbe funzionare per un sottoinsieme di alcuni valori nelle architetture x86 o x64. Si consideri questo esempio:
// notice that a 64-bit integer is passed to the function, but '%d' is used to read it.
// on x86 and x64 this may work for small values because %d will "parse" the low-32 bits of the argument.
// on ARM the calling convention will align the 64-bit value and the code will print a random value
printf("%d\n", 1LL);
In questo caso, il bug può essere corretto assicurandosi che venga usata la specifica del formato corretta in modo che venga considerato l'allineamento dell'argomento. Questo codice è corretto:
// CORRECT: use %I64d for 64-bit integers
printf("%I64d\n", 1LL);
Ordine di valutazione degli argomenti
Poiché i processori ARM, x86 e x64 sono così diversi, possono presentare requisiti diversi alle implementazioni del compilatore e anche opportunità diverse per le ottimizzazioni. Per questo motivo, insieme ad altri fattori come la convenzione di chiamata e le impostazioni di ottimizzazione, un compilatore potrebbe valutare gli argomenti di funzione in un ordine diverso in architetture diverse o quando vengono modificati gli altri fattori. Ciò può causare la modifica imprevista del comportamento di un'app che si basa su un ordine di valutazione specifico.
Questo tipo di errore può verificarsi quando gli argomenti di una funzione hanno effetti collaterali che influiscono su altri argomenti per la funzione nella stessa chiamata. In genere questo tipo di dipendenza è facile da evitare, ma può essere oscurato dalle dipendenze difficili da distinguere o dall'overload degli operatori. Si consideri questo esempio di codice:
handle memory_handle;
memory_handle->acquire(*p);
Ciò appare ben definito, ma se -> e * sono operatori di overload, questo codice viene convertito in un elemento simile al seguente:
Handle::acquire(operator->(memory_handle), operator*(p));
E se esiste una dipendenza tra operator->(memory_handle) e operator*(p), il codice potrebbe basarsi su un ordine di valutazione specifico, anche se il codice originale sembra che non esista alcuna dipendenza possibile.
volatile comportamento predefinito della parola chiave
Il compilatore Microsoft C++ (MSVC) supporta due diverse interpretazioni del qualificatore di archiviazione che è possibile specificare usando le opzioni del volatile compilatore. L'opzione /volatile:ms seleziona la semantica volatile estesa di Microsoft che garantisce un ordinamento sicuro, come è stato il caso tradizionale per x86 e x64 a causa del modello di memoria avanzata in tali architetture. L'opzione /volatile:iso seleziona la semantica volatile del C++ standard rigorosa che non garantisce un forte ordinamento.
Nell'architettura arm (ad eccezione di ARM64EC), il valore predefinito è /volatile:iso perché i processori ARM hanno un modello di memoria ordinato in modo debole e poiché il software ARM non ha una legacy di basarsi sulla semantica estesa di /volatile:ms e in genere non deve interfacciarsi con il software che lo fa. Tuttavia, a volte è ancora utile o anche necessario compilare un programma ARM per usare la semantica estesa. Ad esempio, potrebbe essere troppo costoso convertire un programma per usare la semantica ISO C++ o il software driver potrebbe dover rispettare la semantica tradizionale per funzionare correttamente. In questi casi, è possibile usare l'opzione /volatile:ms ; tuttavia, per ricreare la semantica volatile tradizionale sui target ARM, il compilatore deve inserire barriere di memoria attorno a ogni lettura o scrittura di una variabile volatile per applicare un forte ordinamento, che può avere un impatto negativo sulle prestazioni.
Nelle architetture x86, x64 e ARM64EC, il valore predefinito è /volatile:ms perché gran parte del software già creato per queste architetture usando MSVC si basa su di essi. Quando si compilano programmi x86, x64 e ARM64EC, è possibile specificare l'opzione /volatile:iso per evitare la dipendenza non necessaria dalla semantica volatile tradizionale e promuovere la portabilità.