Běžné problémy s migrací ARM v prostředí Visual C++
Stejný zdrojový kód jazyka Visual C++ může na architektuře ARM vyprodukovat jiné výstupy, než na architekturách x86 a x64.
Zdroje pro problémy s migrací
Mnoho problémů, které se mohou vyskytnout při migraci kódu z architektury x86 nebo x64 do architektury ARM souvisí s konstrukcí zdrojového kódu, který by mohl vyvolat nedefinované chování, chovaní definované implementací nebo nespecifikované chování.
Nedefinované chování
Chování, které standard jazyka C++ nedefinuje a které 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 posunutí hodnoty o určitý negativní počet míst nebo pokud počet bitů přesáhne počet bitů jeho propagovaného typu.Chování definované implementací
Chování, které standard C++ vyžaduje, aby bylo definováno a zdokumentováno dodavatelem kompilátoru.Program může bez obav spoléhat na chování definované implementací, přestože pak nemusí být program přenosný.Příklady chování definované implementací zahrnují velikosti předdefinovaných datových typů a jejich požadavky na zarovnání.Příkladem operace, která může být ovlivněna chováním definovaným implementací je přístup k seznamu proměnných argumentů.Nespecifikované chování
Chování, které standard jazyka C++ ponechává záměrně nedeterministické.Ačkoliv chování je považováno za nedeterministické, jsou některá vyvolání nespecifikovaného chování určena implementací kompilátoru.Nicméně neexistuje žádný požadavek na dodavatele kompilátoru, aby určil výsledek nebo zaručil konzistentní chování mezi srovnatelnými vyvoláními. Neexistuje zde ani požadavek na dokumentaci.Příkladem nespecifikovaného chování je pořadí, ve kterém jsou dílčí podvýrazy, které obsahují argumenty pro volání funkce, vyhodnocovány.
Jiné problémy s migrací lze považovat za hardwarové rozdíly mezi ARM a architekturami x86 a x64, které spolupracují se standardem C++ odlišně.Například silný paměťový model architektury x86 a x64 poskytuje kvalifikovaným volatile proměnným některé další vlastnosti, které byly v minulosti používány k usnadnění určitých druhů mezivláknové komunikace.Ale slabý paměťový model architektury ARM nepodporuje toto použití a ani standard C++ jej nevyžaduje.
Důležité |
---|
I když volatile poskytuje některé vlastnosti, které lze použít k implementaci omezené formy mezivláknové komunikace, na procesorech x86 nebo x64 tyto další vlastnosti nejsou obecně dostatečné pro implementaci mezivláknové komunikace.Standard jazyka C++ doporučuje aby takováto komunikace byla namísto toho implementována pomocí příslušných primitivních typů pro synchronizaci. |
Protože různé platformy mohou tyto druhy chování interpretovat jinak, může být přenos softwaru mezi platformami obtížný a náchylný na chyby, pokud závisí na chování konkrétní platformy.Přestože mnoho z těchto typů chování lze pozorovat a mohou vypadat stabilně, spoléhání se na ně vede minimálně k nepřenosnosti a v případech nedefinovaného, nebo nespecifikovaného chování, jde navíc o chybu.I na chování, které je uvedené v tomto dokumentu se nelze spoléhat a implementace kompilátorů nebo procesorů by se mohla v budoucnu změnit.
Příklady problémů s migrací
Zbytek tohoto dokument popisuje, jak různá chování těchto prvků jazyka C++ mohou vyprodukovat různé výsledky na různých platformách.
Převod hodnoty s plovoucí desetinnou čárkou na celé číslo bez znaménka
Na architektuře ARM jsou hodnoty s plovoucí desetinnou čárkou převedeny na nejvyšší hodnotu 32-bitového celého čísla, které může celé číslo reprezentovat, jestliže je hodnota mimo rozsah, který může celé číslo reprezentovat.Na architekturách x86 a x64 číslo přeskočí na začátek rozsahu, pokud se jedná o celé číslo bez znaménka, nebo bude nastaveno na -2147483648, jestliže se jedná o celé číslo se znaménkem.Žádná z těchto architektur přímo nepodporuje převod hodnoty s plovoucí desetinnou čárkou na menší celočíselné typy. Místo toho jsou převody prováděny do 32 bitů a výsledky jsou zkráceny na menší velikost.
U architektury ARM kombinace doplnění a zkrácení znamená, že konverze na typy bez znaménka správně doplní menší typy bez znaménka při doplňování 32-bitového celého čísla, vyprodukuje ale zkrácený výsledek pro hodnoty, které jsou větší než může menší typ reprezentovat, ale je příliš malý, aby mohl doplnit úplné 32-bitové celé číslo.Převod také správně doplňuje pro 32-bitová celá čísla se znaménkem, ale zkrácení doplněných, celých čísel se znaménkem může vést k výsledku -1 pro kladně doplněné a 0 pro negativně doplněné hodnoty.Převod na menší celé typy se znaménkem vytváří zkrácený výsledek, který nelze předvídat.
U architektur x86 a x64 kombinace chování, kdy číslo u celých čísel bez znaménka přeskočí na začátek, a explicitní valuace pro celé číslo se znaménkem při přetečení společně se zkrácením, vytváří nepředvídatelné výsledky u většiny posunutí, pokud jsou hodnoty příliš velké.
Tyto platformy se také liší ve způsobu jejich zpracování převodu NaN (není číslo) na celočíselné typy.V ARM je NaN převedeno na 0x00000000, v x86 a x64 se převede na 0x80000000.
Na převod typů s plovoucí čárkou se lze spoléhat pouze, pokud je hodnota v rámci rozsahu celočíselného typu, na který je převáděna.
Chování operátoru posunutí (<< >>)
Na architektuře ARM lze hodnotu přesunout doleva nebo doprava až o 255 bitů předtím, než se začne opakovat.Na architekturách x86 a x64 se vzor opakuje s každým násobkem 32, ale pokud je zdroj vzoru 64-bitová proměnná, opakuje se vzor na architektuře x64 s každým násobkem 64 a na architektuře x86 s každým násobkem 256.Například pro 32-bitovou proměnnou, která má hodnotu 1 bude výsledkem posunutí o 32 míst doleva na architektuře ARM 0, na architektuře x86 1 a na architektuře x64 bude také 1.Je-li zdrojová hodnota 64-bitovou proměnnou, pak je výsledek na všech třech platformách 4294967296 a hodnota "nepřetéká okolo" dokud ji nepřesuneme o 64 pozic na architektuře x64, nebo o 256 pozic na architekturách ARM a x86.
Protože výsledek operace posunutí, který překračuje počet bitů v typu zdroje, není definován, není povinné, aby kompilátor měl konzistentní chování ve všech situacích.Například pokud jsou oba operandy posunutí známy během kompilace, může kompilátor optimalizovat program pomocí vnitřních rutin pro předpočítání výsledku posunutí a následné nahrazení výsledku v místě operace posunutí.Je-li počet posunutí příliš velký, nebo záporný, může se výsledek vnitřní rutiny lišit od výsledku stejného výrazu posunutí provedeného procesorem.
Chování proměnných argumentů (varargs)
Na architektuře ARM jsou parametry ze seznamu proměnných argumentů, které byly předány v zásobníku, zarovnány.Například 64-bitový parametr je zarovnán na hranici 64-bitů.Na procesorech x86 a x64 nebudou předané argumenty na zásobníku zarovnány a budou pevně zabaleny.Tento rozdíl může způsobit, že variadické funkce podobné printf budou číst paměťové adresy, které byly určeny pro odsazení na architekturách ARM, pokud nebude seznam proměnných přesně odpovídat, přesto to může fungovat u podmnožiny některých hodnot na architekturách x86 a x64.Vezměme si jako 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ě lze chybu vyřešit zajištěním, aby byl použit správný formát specifikace tak, aby bylo bráno v potaz zarovnání argumentu.Tento kód je správný:
// CORRECT: use %I64d for 64-bit integers
printf("%I64d\n", 1LL);
Pořadí vyhodnocování argumentů
Protože procesory ARM, x86 a x64 jsou příliš odlišné, mohou předkládat různé požadavky na implementaci kompilátorů a také různé možnosti optimalizace.Vzhledem k tomu a společně s dalšími faktory, jako je konvence volání a nastavení optimalizace kompilátoru může kompilátor vyhodnotit argumenty funkcí v jiném pořadí na různých architekturách nebo když jsou změněny jiné faktory.To může způsobit, že chování aplikace, které je založeno na konkrétním uspořádaní se může neočekávaně změnit.
Tento druh chyby může nastat, pokud argumenty funkce mají vedlejší účinky, které mají vliv na další argumenty funkce ve stejném volání.Obvykle lze tomuto druhu závislosti snadno předejít, ale někdy může být skryt závislostmi, které je obtížné rozeznat, nebo přetěžováním operátorů.Vezměme si jako příklad:
handle memory_handle;
memory_handle->acquire(*p);
Vypadá dobře definovaný, ale když budou operátory -> a *, pak bude tento kód přeložen na hodnotu, která bude vypadá takto:
Handle::acquire(operator->(memory_handle), operator*(p));
A pokud existuje závislost mezi operátory 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á jako by tam nebyly žádná závislost.
Výchozí chování klíčového slova volatile
Kompilátor jazyka C++ společnosti Microsoft podporuje dva různé výklady kvalifikátoru nestálého úložiště, které lze určit pomocí přepínačů kompilátoru.Přepínač /volatile:ms vybírá rozšířenou volatilní sémantiku společnosti Microsoft, která zajistí silné seřazení, což byl tradiční případ pro architektury x86 a x64 v kompilátoru společnosti Microsoft z důvodu existence silného paměťového modelu na těchto architekturách.Přepínač /volatile:iso vybere přísnou standardní volatilní sémantiku jazyka C++, která nezaručuje silné řazení.
Na architektuře ARM, je výchozí hodnota /volatile:iso protože ARM procesory mají slabě seřazený paměťový model a protože software pro ARM nemá historii spoléhání se na rozšířenou sémantiku /volatile:ms a nemusí obvykle komunikovat se softwarem, který toto má.Stále je však někdy vhodné, nebo je dokonce vyžadováno, kompilovat aplikaci ARM pro použití rozšířené sémantiky.Například, když je přenesení programu pro použití sémantik ISO C++ příliš nákladné nebo musí ovladač dodržovat tradiční sémantiku, aby pracoval správně.V těchto případech lze použít přepínač /volatile:ms. Nicméně pro opětovné vytvoření tradiční volatilní sémantiky cílící na ARM, musí kompilátor vložit paměťové překážky okolo každého čtení či zápisu proměnné volatile k vynucení silného řazení, které má negativní dopad na výkon.
Na architekturách x86 a x64, je výchozí hodnota /volatile:ms, protože velká část softwaru, který již byl vytvořen pro tyto architektury pomocí kompilátoru Microsoft C++, na nich závisí.Při kompilaci programů x86 a x64 lze zadat přepínač /volatile:iso pomáhající vyhnout se zbytečným závislostem na tradiční volatilní sémantice a pro podporu přenositelnosti.