Běžné problémy s migrací ARM v prostředí Visual C++

Při použití kompilátoru Microsoft C++ (MSVC) může stejný zdrojový kód jazyka C++ vést k různým výsledkům architektury ARM, než to dělá v architekturách x86 nebo x64.

Zdroje problémů s migrací

Řada problémů, se kterými se můžete setkat při migraci kódu z architektur x86 nebo x64 do architektury ARM, souvisí s konstrukty zdrojového kódu, které můžou vyvolat nedefinované, definované implementace nebo nespecifikované chování.

Nedefinované chování je chování , které standardní jazyk C++ nedefinuje, a to je způsobeno operací, která nemá žádný rozumný výsledek: například převod hodnoty s plovoucí desetinnou čárkou na celé číslo bez znaménka nebo posun hodnoty o několik pozic, které jsou záporné nebo překračují počet bitů v jeho povýšeného typu.

Chování definované implementací je chování , které standard C++ vyžaduje, aby dodavatel kompilátoru definoval a dokumentoval. Program může bezpečně spoléhat na chování definované implementací, i když to nemusí být přenosné. Příklady chování definované implementací zahrnují velikosti předdefinovaných datových typů a jejich požadavků na zarovnání. Příkladem operace, která může být ovlivněna chováním definovaným implementací, je přístup k seznamu argumentů proměnných.

Nezadané chování je chování , které standardní jazyk C++ záměrně ponechá nedeterministické. I když se chování považuje za ne deterministické, konkrétní vyvolání nezadaného chování je určeno implementací kompilátoru. Není však nutné, aby dodavatel kompilátoru předem předdedeterminoval výsledek nebo zajistil konzistentní chování mezi srovnatelnými vyvoláním a není nutné pro dokumentaci. Příkladem nezadaného chování je pořadí, ve kterém se vyhodnocují dílčí výrazy, které zahrnují argumenty volání funkce.

Jiné problémy s migrací můžou být přiřazovat rozdíly mezi architekturami ARM a x86 nebo x64, které interagují se standardem C++. Například model silné paměti architektury x86 a x64 poskytuje volatile-kvalifikované proměnné některé další vlastnosti, které byly použity k usnadnění určitých druhů komunikace mezi vlákny v minulosti. Model slabé paměti architektury ARM ale nepodporuje toto použití, ani standard C++ho nevyžaduje.

Důležité

Přestože volatile získá některé vlastnosti, které lze použít k implementaci omezených forem komunikace mezi vlákny v x86 a x64, tyto další vlastnosti nejsou dostačující k implementaci komunikace mezi vlákny obecně. Standard jazyka C++ doporučuje, aby byla taková komunikace implementována pomocí vhodných primitiv synchronizace.

Vzhledem k tomu, že různé platformy mohou tyto druhy chování vyjádřit odlišně, může být přenos softwaru mezi platformami složitý a náchylný k chybám, pokud závisí na chování konkrétní platformy. I když lze pozorovat mnoho z těchto druhů chování a může se zdát stabilní, spoléhání na ně je alespoň nepřenosné a v případě nedefinovaného nebo nezadaného chování je také chyba. Dokonce ani chování, které je uvedeno v tomto dokumentu, by se nemělo spoléhat a v budoucích kompilátorech nebo implementacích procesoru by se mohlo změnit.

Příklady problémů s migrací

Zbytek tohoto dokumentu popisuje, jak různé chování těchto elementů jazyka C++ může vést k různým výsledkům na různých platformách.

Převod čísla s plovoucí desetinou čárkou na celé číslo bez znaménka

V architektuře ARM převod hodnoty s plovoucí desetinou čárkou na 32bitové celé číslo saturuje na nejbližší hodnotu, kterou celé číslo může představovat, pokud je hodnota s plovoucí desetinou čárkou mimo rozsah, který může celé číslo představovat. V architekturách x86 a x64 se převod zalomí, pokud je celé číslo bez znaménka, nebo je nastaveno na -2147483648 pokud je celé číslo podepsáno. Žádná z těchto architektur přímo nepodporuje převod hodnot s plovoucí desetinou čárkou na menší celočíselné typy; místo toho se převody provádějí na 32 bitů a výsledky jsou zkráceny na menší velikost.

U architektury ARM kombinace sytosti a zkrácení znamená, že převod na typy bez znaménka správně sytí menší typy bez znaménka, když sytí 32bitové celé číslo, ale vytvoří zkrácený výsledek pro hodnoty, které jsou větší než menší typ, mohou představovat, ale příliš malé na nasycení celého 32bitového celého čísla. Převod také správně nasytí 32bitových celých čísel, ale zkrácení sytých celých čísel má za následek hodnotu -1 pro hodnoty s pozitivním nasycením a 0 pro záporně nasycené hodnoty. Převod na menší celé číslo se znaménkem vytvoří zkrácený výsledek, který je nepředvídatelný.

U architektur x86 a x64 se kombinace chování obtékání pro převody celých čísel bez znaménka a explicitní ocenění pro převody celých čísel s znaménkem společně se zkrácením vytvoří výsledky pro většinu směn nepředvídatelné, pokud jsou příliš velké.

Tyto platformy se také liší v tom, jak zpracovávají převod NaN (Not-a-Number) na celočíselné typy. V ARM se NaN převede na 0x00000000; na platformě x86 a x64 se převede na 0x80000000.

Převod s plovoucí desetinou čárkou se dá spoléhat jenom v případě, že víte, že hodnota je v rozsahu celočíselného typu, na který se převádí.

Chování operátoru Shift (<<>>)

V architektuře ARM je možné hodnotu posunout doleva nebo doprava až do 255 bitů, než se vzor začne opakovat. V architekturách x86 a x64 se vzor opakuje při každém násobku 32, pokud zdrojem vzoru není 64bitová proměnná; v takovém případě se vzor opakuje při každém násobku 64 na x64 a každý násobek 256 v x86, kde se používá implementace softwaru. Například pro 32bitovou proměnnou, která má hodnotu 1 posunutou doleva o 32 pozic, v ARM je výsledek 0, na x86 je výsledek 1 a na x64 je výsledek také 1. Pokud je ale zdrojem hodnoty 64bitová proměnná, výsledek na všech třech platformách je 4294967296 a hodnota se "obtéká kolem", dokud nepřesunou 64 pozic na x64 nebo 256 pozic v ARM a x86.

Vzhledem k tomu, že výsledek operace posunu, která překračuje počet bitů ve zdrojovém typu, není definován, kompilátor nemusí mít konzistentní chování ve všech situacích. Pokud jsou například oba operandy posunu známé v době kompilace, kompilátor může program optimalizovat pomocí interní rutiny, aby předkomputoval výsledek směny a potom výsledek nahradit místo operace směny. Pokud je velikost směny příliš velká nebo záporná, může se výsledek interní rutiny lišit od výsledku stejného výrazu směny, který provádí procesor.

Chování proměnných argumentů (varargs)

V architektuře ARM se parametry ze seznamu proměnných argumentů předávané v zásobníku řídí zarovnáním. Například 64bitový parametr je zarovnán na 64bitové hranici. U x86 a x64 nejsou argumenty předávané v zásobníku předmětem zarovnání a balení těsně. Tento rozdíl může způsobit, že variadická funkce jako printf čtení adres paměti, které byly určeny jako odsazení v ARM, pokud očekávané rozložení seznamu argumentů proměnných přesně neodpovídá, i když může fungovat pro podmnožinu některých hodnot v architekturách x86 nebo x64. Podívejte se na tento příklad:

// 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);

V tomto případě je možné chybu opravit tak, že zajistíte, aby se použila správná specifikace formátu, aby bylo použito zarovnání argumentu. Tento kód je správný:

// CORRECT: use %I64d for 64-bit integers
printf("%I64d\n", 1LL);

Pořadí vyhodnocení argumentů

Vzhledem k tomu, že procesory ARM, x86 a x64 jsou tak odlišné, mohou prezentovat různé požadavky na implementace kompilátoru a také různé příležitosti pro optimalizace. Z tohoto důvodu může kompilátor společně s dalšími faktory, jako jsou nastavení konvence volání a optimalizace, vyhodnotit argumenty funkce v jiném pořadí v různých architekturách nebo při změně jiných faktorů. To může způsobit neočekávané změny chování aplikace, která spoléhá na konkrétní pořadí vyhodnocení.

Tento druh chyby může nastat, když argumenty funkce mají vedlejší účinky, které mají vliv na jiné argumenty funkce ve stejném volání. Tento druh závislosti se obvykle snadno vyhne, ale někdy může být nejasný závislostmi, které jsou obtížně rozpoznatelné, nebo přetížením operátoru. Podívejte se na tento příklad kódu:

handle memory_handle;

memory_handle->acquire(*p);

Zobrazí se dobře definované, ale pokud -> a * jsou přetížené operátory, tento kód se přeloží na něco, co se podobá tomuto:

Handle::acquire(operator->(memory_handle), operator*(p));

A pokud existuje závislost mezi operator->(memory_handle) a operator*(p), může kód spoléhat na konkrétní pořadí vyhodnocení, i když původní kód vypadá, že neexistuje žádná možná závislost.

výchozí chování klíčového slova volatile

Kompilátor MSVC podporuje dvě různé interpretace kvalifikátoru volatile úložiště, které můžete určit pomocí přepínačů kompilátoru. Přepínač /volatile:ms vybere rozšířenou sémantiku nestálé společnosti Microsoft, která zaručuje silné řazení, stejně jako tradiční případ pro x86 a x64 kvůli silnému paměťovému modelu v těchto architekturách. Přepínač /volatile:iso vybere striktní sémantiku nestálého jazyka C++, která nezaručuje silné řazení.

V architektuře ARM (s výjimkou ARM64EC) je výchozí /volatile:iso , protože procesory ARM mají slabě seřazený model paměti a protože software ARM nemá starší verzi spoléhání na rozšířenou sémantiku /volatile:ms a obvykle nemusí rozhraní se softwarem, který dělá. Někdy je ale stále vhodné nebo dokonce nutné zkompilovat program ARM pro použití rozšířené sémantiky. Například může být příliš nákladné portovat program pro použití sémantiky ISO C++ nebo software ovladače může muset dodržovat tradiční sémantiku, aby správně fungoval. V těchto případech můžete použít přepínač /volatile:ms , ale k opětovnému vytvoření tradiční sémantiky nestálosti cílů ARM musí kompilátor vložit bariéry paměti kolem každého čtení nebo zápisu proměnné k vynucení silného volatile pořadí, což může mít negativní dopad na výkon.

Na architekturách x86, x64 a ARM64EC je výchozí hodnota /volatile:ms , protože většina softwaru, který už pro tyto architektury byl vytvořen pomocí MSVC, na nich spoléhá. Při kompilaci programů x86, x64 a ARM64EC můžete určit přepínač /volatile:iso , aby se zabránilo zbytečnému závislosti na tradiční sémantice nestálé a zvýšit přenositelnost.

Viz také

Konfigurace Visual C++ pro procesory ARM