Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Tento dokument popisuje některé běžné problémy, se kterými se můžete setkat při migraci kódu z architektur x86 nebo x64 do architektury ARM. Popisuje také, jak se těmto problémům vyhnout a jak použít kompilátor k jejich identifikaci.
Poznámka:
Pokud se tento článek týká architektury ARM, platí pro ARM32 i ARM64.
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é standard 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 přesunutí hodnoty o řadu 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.
Nespecifikované chování je chování , které standardní jazyk C++ záměrně ponechá nedeterministické. I když je chování považováno za nedeterministické, konkrétní vyvolání nespecifikovaného chování je určeno implementací kompilátoru. Není však nutné, aby dodavatel kompilátoru předem předdedeminoval výsledek nebo zajistil konzistentní chování mezi srovnatelnými vyvoláním a neexistuje žádný požadavek na 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í lze přičíst rozdílům mezi architekturami ARM a x86 (x64), které se 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 vlastnosti nejsou dostatečné 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 i chování uvedené v tomto dokumentu by se nemělo spoléhat a mohlo by se změnit v budoucích kompilátorech nebo implementacích procesoru.
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í řádovou čárkou na nezáporné celé číslo
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 se znaménkem. Žádná z těchto architektur přímo nepodporuje převod hodnot s plovoucí desetinnou čárkou na menší typy celých čísel; místo toho probíhají převody na 32 bitů a výsledky jsou redukovány na menší velikost.
U architektury ARM kombinace saturace a zkrácení znamená, že převod na bezznaménkové typy správně nasytí menší bezznaménkové typy, když nasytí 32bitové celé číslo. Nicméně pro hodnoty, které jsou větší, než jaké menší typ může reprezentovat, ale příliš malé na to, aby dosáhly saturace celého 32bitového celého čísla, vytvoří zkrácený výsledek. Převod také správně nasycuje 32bitová celá čísla, ale zkrácení sytých, podepsaných celých čísel vede k hodnotě -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í čárkou lze se spoléhat pouze v případě, že víte, že hodnota spadá do rozsahu celého čísla, 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 při každém násobku 256 na x86, kde je použita softwarová implementace. 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 nepřetočí zpět na začátek, dokud není posunuta o 64 pozic na x64 nebo 256 pozic na 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 posunu příliš velká nebo záporná, může se výsledek interní procedury lišit od výsledku stejného výrazu posunu, jak jej 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 argumenty předávané v zásobníku nepodléhají zarovnání a jsou těsně sbalené. Tento rozdíl může způsobit, že variadická funkce jako printf může čítat adresy paměti, které byly určeny jako výplň na ARM, pokud očekávané rozložení seznamu proměnných argumentů neodpovídá přesně, ačkoli může fungovat pro podmnožinu některých hodnot na 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, takže se bere v úvahu 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 lze obvykle snadno obejít, ale může být zastřen jinými závislostmi, které jsou obtížně rozpoznatelné, nebo přetížením operátorů. Podívejte se na tento příklad kódu:
handle memory_handle;
memory_handle->acquire(*p);
To se zdá být dobře definované, ale pokud -> a * jsou přetížené operátory, pak se tento kód překládá na něco podobného 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.
volatile Výchozí chování klíčového slova
Kompilátor Microsoft C++ (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 vybírá rozšířenou sémantiku volatile od Microsoft, která zaručuje silné pořadí, jak je tomu tradičně u x86 a x64 vzhledem k silnému paměťovému modelu těchto architektur. 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á historii spoléhání na rozšířenou sémantiku /volatile:ms a obvykle nemusí komunikovat se softwarem, který na ni spoléhá. 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; nicméně k opětovnému vytvoření tradiční sémantiky nestálosti na platformách ARM musí kompilátor vložit paměťové bariéry kolem každého čtení nebo zápisu proměnné k vynucení silného uspořádání, což může nepříznivě ovlivnit výkon.
Ve architekturách x86, x64 a ARM64EC je výchozí hodnota /volatile:ms , protože většina softwaru, který už byl pro tyto architektury vytvořen pomocí MSVC, na ně spoléhá. Při kompilaci programů x86, x64 a ARM64EC můžete použít přepínač /volatile:iso, abyste předešli zbytečné závislosti na tradičních proměnlivých sémantikách a podpořili přenositelnost.