Problemi comuni relativi alla migrazione di Visual C++ ARM
Quando si usa il compilatore Microsoft C++ (MSVC), lo stesso codice sorgente C++ potrebbe produrre risultati diversi nell'architettura ARM rispetto alle architetture x86 o x64.
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 è il comportamento che lo standard C++ lascia intenzionalmente non deterministico. Sebbene il comportamento sia considerato non deterministico, determinate chiamate di comportamento non specificato sono determinate dall'implementazione del compilatore. Non esiste tuttavia alcun requisito per un fornitore del compilatore di predeterminare il risultato o garantire 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 modello di memoria avanzata dell'architettura x86 e x64 fornisce volatile
variabili qualificate alcune proprietà aggiuntive usate per facilitare determinati tipi di comunicazione tra 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à aggiuntive 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 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 integer senza segno a virgola mobile
Nell'architettura arm, la conversione di un valore a virgola mobile in un intero a 32 bit saturazione 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 o è impostato 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 tipi senza segno più piccoli quando saturazione un intero a 32 bit, ma produce un risultato troncato per i valori maggiori del tipo più piccolo può rappresentare ma troppo piccolo per saturare l'intero intero a 32 bit. La conversione è anche satura correttamente per interi con segno a 32 bit, ma il troncamento di interi con segno saturo ha come risultato -1 per i valori con saturazione positiva e 0 per i valori saturi negativi. 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 ritorno a capo per le conversioni di interi senza segno e la valutazione esplicita per le conversioni di interi con segno in overflow, insieme al troncamento, rendono i risultati per la maggior parte dei turni imprevedibili se sono troppo grandi.
Queste piattaforme differiscono anche nel modo in cui gestiscono la conversione di NaN (Not-a-Number) in tipi integer. In ARM, NaN esegue la conversione in 0x00000000; in x86 e x64, viene convertito in 0x80000000.
La conversione a virgola mobile può essere basata solo su se si sa che il valore è compreso nell'intervallo del tipo integer in 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, dove 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 in tutte e tre le piattaforme è 4294967296 e il valore non esegue il wrapping fino a quando non viene spostato 64 posizioni su x64 o 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. In x86 e x64, gli argomenti passati nello stack non sono soggetti all'allineamento e al pacchetto strettamente. Questa differenza può causare una funzione variadic come printf
leggere gli indirizzi di memoria previsti come riempimento in ARM se il layout previsto dell'elenco di argomenti variabili non corrisponde esattamente, anche se potrebbe funzionare per un subset 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 a volte 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.
Comportamento predefinito della parola chiave volatile
Il compilatore 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 standard C++ strict che non garantisce un ordinamento sicuro.
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 nelle destinazioni arm, il compilatore deve inserire barriere di memoria per ogni lettura o scrittura di una volatile
variabile per applicare un ordinamento sicuro, 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à.