Delen via


Veelvoorkomende problemen met ARM-migratie van Microsoft C++

In dit document worden enkele veelvoorkomende problemen beschreven die kunnen optreden wanneer u code migreert van x86- of x64-architecturen naar de ARM-architectuur. Ook wordt beschreven hoe u deze problemen kunt voorkomen en hoe u de compiler gebruikt om ze te identificeren.

Opmerking

Wanneer dit artikel verwijst naar de ARM-architectuur, is deze van toepassing op zowel ARM32 als ARM64.

Bronnen van migratieproblemen

Veel problemen die kunnen optreden wanneer u code migreert van de x86- of x64-architecturen naar de ARM-architectuur, zijn gerelateerd aan broncodeconstructies die mogelijk niet-gedefinieerd, implementatiegedefinieerd of niet-gespecificeerd gedrag aanroepen.

Niet-gedefinieerd gedrag is gedrag dat niet wordt gedefinieerd door de C++-standaard en dat wordt veroorzaakt door een bewerking die geen redelijk resultaat heeft: bijvoorbeeld het converteren van een drijvende-kommawaarde naar een niet-ondertekend geheel getal of het verplaatsen van een waarde door een aantal posities dat negatief is of het aantal bits in het gepromoveerde type overschrijdt.

Implementatiegedefinieerde werking is het gedrag dat de C++-standaard vereist dat de leverancier van de compiler definieert en documenteert. Een programma kan veilig vertrouwen op door de implementatie gedefinieerd gedrag, ook al is dit mogelijk niet draagbaar. Voorbeelden van implementatiegedefinieerd gedrag zijn de grootten van ingebouwde gegevenstypen en hun uitlijningsvereisten. Een voorbeeld van een bewerking die mogelijk wordt beïnvloed door het door de implementatie gedefinieerde gedrag, heeft toegang tot de lijst met variabelenargumenten.

Niet-opgegeven gedrag is gedrag dat de C++-standaard opzettelijk niet-deterministisch laat. Hoewel het gedrag wordt beschouwd als niet-deterministisch, worden met name aanroepen van niet-opgegeven gedrag bepaald door de compiler-implementatie. Er is echter geen vereiste dat een compilerleverancier het resultaat vooraf bepaalt of consistent gedrag garandeert tussen vergelijkbare aanroepen, en er is geen vereiste voor documentatie. Een voorbeeld van niet-opgegeven gedrag is de volgorde waarin subexpressies, waaronder argumenten voor een functieaanroep, worden geëvalueerd.

Andere migratieproblemen kunnen worden toegeschreven aan hardwareverschillen tussen ARM- en x86- of x64-architecturen die anders communiceren met de C++-standaard. Het sterke geheugenmodel van de x86- en x64-architectuur biedt volatilebijvoorbeeld gekwalificeerde variabelen enkele extra eigenschappen die zijn gebruikt om bepaalde soorten communicatie tussen threads in het verleden mogelijk te maken. Maar het zwakke geheugenmodel van de ARM-architectuur biedt geen ondersteuning voor dit gebruik en de C++-standaard vereist dit ook niet.

Belangrijk

Hoewel volatile sommige eigenschappen kunnen worden gebruikt voor het implementeren van beperkte vormen van communicatie tussen threads op x86 en x64, zijn deze eigenschappen niet voldoende om communicatie tussen threads in het algemeen te implementeren. De C++-standaard raadt aan dat dergelijke communicatie wordt geïmplementeerd met behulp van de juiste synchronisatieprimitieven.

Omdat verschillende platforms dit soort gedrag anders kunnen uitdrukken, kan het overzetten van software tussen platforms lastig en foutgevoelig zijn als dit afhankelijk is van het gedrag van een specifiek platform. Hoewel veel van deze soorten gedrag kunnen worden waargenomen en stabiel kunnen worden weergegeven, is het vertrouwen erop ten minste niet-draagbaar en in het geval van niet-gedefinieerd of niet-gespecificeerd gedrag ook een fout. Zelfs het gedrag dat in dit document wordt geciteerd, mag niet worden vertrouwd en kan veranderen in toekomstige compilers of CPU-implementaties.

Voorbeeld van migratieproblemen

In de rest van dit document wordt beschreven hoe het verschillende gedrag van deze C++-taalelementen verschillende resultaten kan opleveren op verschillende platforms.

Conversie van zwevende komma naar ongetekend geheel getal

In de ARM-architectuur wordt de conversie van een drijvende-kommawaarde naar een 32-bits geheel getal verzadigd tot de dichtstbijzijnde waarde die het gehele getal kan vertegenwoordigen als de drijvende-kommawaarde zich buiten het bereik bevindt dat het gehele getal kan vertegenwoordigen. In de x86- en x64-architecturen loopt de conversie rond als het gehele getal niet is ondertekend of is ingesteld op -2147483648 als het gehele getal is ondertekend. Geen van deze architecturen ondersteunt de conversie van drijvende-kommawaarden rechtstreeks naar kleinere gehele getallen; In plaats daarvan worden de conversies uitgevoerd naar 32 bits en worden de resultaten afgekapt tot een kleinere grootte.

Voor de ARM-architectuur betekent de combinatie van verzadiging en afkapping dat conversie naar niet-ondertekende typen kleinere niet-ondertekende typen correct verzadigt wanneer het een 32-bits geheel getal verzadigt, maar een afgekapt resultaat produceert voor waarden die groter zijn dan het kleinere type, maar te klein om het volledige 32-bits gehele getal te verzadigen. Conversie is ook correct gesatureerd voor 32-bits ondertekende gehele getallen, maar het afkappen van verzadigde, ondertekende gehele getallen resulteert in -1 voor positief gesatureerde waarden en 0 voor negatief gesatureerde waarden. Conversie naar een kleiner ondertekend geheel getal produceert een afgekapt resultaat dat onvoorspelbaar is.

Voor de x86- en x64-architecturen maakt de combinatie van wrap-around-gedrag voor niet-ondertekende gehele getallenconversies en expliciete waardering voor ondertekende gehele getallenconversies bij overloop, samen met afkapping, de resultaten voor de meeste verschuivingen onvoorspelbaar als ze te groot zijn.

Deze platforms verschillen ook in de manier waarop ze de conversie van NaN (Not-a-Number) naar gehele getallen verwerken. In ARM wordt NaN geconverteerd naar 0x00000000; op x86 en x64 wordt deze geconverteerd naar 0x80000000.

Conversie van drijvende komma kan alleen worden gebruikt als u weet dat de waarde binnen het bereik van het gehele getaltype valt waarnaar deze wordt geconverteerd.

Gedrag van shiftoperator (<<>>)

In de ARM-architectuur kan een waarde worden verschoven naar links of rechts tot 255 bits voordat het patroon begint te herhalen. In x86- en x64-architecturen wordt het patroon herhaald op elk veelvoud van 32, tenzij de bron van het patroon een 64-bits variabele is. In dat geval wordt het patroon herhaald op elk veelvoud van 64 op x64 en elk veelvoud van 256 op x86, waarbij een software-implementatie wordt gebruikt. Voor een 32-bits variabele met een waarde van 1 verschoven naar links met 32 posities, is in ARM het resultaat 0, op x86 is het resultaat 1 en op x64 is het resultaat ook 1. Echter, als de bron van de waarde een 64-bits variabele is, dan is het resultaat op alle drie de platforms 4294967296, en 'loopt' de waarde niet over tot deze is verschoven over 64 posities op x64, of 256 posities op ARM en x86.

Omdat het resultaat van een dienstbewerking die het aantal bits in het brontype overschrijdt, niet is gedefinieerd, hoeft de compiler in alle situaties geen consistent gedrag te hebben. Als bijvoorbeeld beide operanden van een dienst bekend zijn tijdens het compileren, kan de compiler het programma optimaliseren met behulp van een interne routine om het resultaat van de dienst vooraf te berekenen en vervolgens het resultaat te vervangen in plaats van de dienstbewerking. Als de verschuivingshoeveelheid te groot of negatief is, kan het resultaat van de interne routine afwijken van het resultaat van dezelfde shift-expressie als uitgevoerd door de CPU.

Gedrag van variabele argumenten (varargs)

In de ARM-architectuur zijn parameters uit de lijst met variabelenargumenten die op de stack worden doorgegeven, onderworpen aan uitlijning. Een 64-bits parameter wordt bijvoorbeeld uitgelijnd op een 64-bits grens. Op x86 en x64 zijn argumenten die op de stack worden doorgegeven niet onderworpen aan uitlijning en worden ze strak verpakt. Dit verschil kan ertoe leiden dat een variantische functie zoals printf geheugenadressen leest die zijn bedoeld als opvulling in ARM als de verwachte indeling van de lijst met variabelenargumenten niet exact overeenkomt, zelfs als het mogelijk werkt voor een subset van bepaalde waarden in de x86- of x64-architecturen. Kijk eens naar dit voorbeeld:

// 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 dit geval kan de fout worden opgelost door ervoor te zorgen dat de juiste indelingsspecificatie wordt gebruikt, zodat de uitlijning van het argument wordt overwogen. Deze code is juist:

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

Volgorde van argumentevaluatie

Omdat ARM-, x86- en x64-processors zo verschillend zijn, kunnen ze verschillende vereisten voor compiler-implementaties en ook verschillende mogelijkheden voor optimalisaties presenteren. Daarom kan een compiler, samen met andere factoren zoals aanroepende conventie- en optimalisatie-instellingen, functieargumenten in een andere volgorde evalueren op verschillende architecturen of wanneer de andere factoren worden gewijzigd. Dit kan ertoe leiden dat het gedrag van een app die afhankelijk is van een specifieke evaluatievolgorde onverwacht wordt gewijzigd.

Dit soort fout kan optreden wanneer argumenten voor een functie bijwerkingen hebben die van invloed zijn op andere argumenten voor de functie in dezelfde aanroep. Meestal is dit soort afhankelijkheid gemakkelijk te vermijden, maar kan worden verborgen door afhankelijkheden die moeilijk te onderscheiden zijn of door overbelasting van operatoren. Bekijk dit codevoorbeeld:

handle memory_handle;

memory_handle->acquire(*p);

Dit lijkt goed gedefinieerd, maar als -> en * overbelaste operators zijn, wordt deze code vertaald naar iets dat er ongeveer als volgt uitziet:

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

En als er een afhankelijkheid is tussen operator->(memory_handle) en operator*(p), kan de code afhankelijk zijn van een specifieke evaluatievolgorde, ook al lijkt de oorspronkelijke code erop dat er geen mogelijke afhankelijkheid is.

volatile standaardgedrag voor trefwoorden

De MSVC-compiler (Microsoft C++) ondersteunt twee verschillende interpretaties van de volatile opslagkwalificatie die u kunt opgeven met behulp van compilerswitches. De schakeloptie /vluchtig:ms selecteert de uitgebreide vluchtige semantiek van Microsoft die een sterke volgorde garandeert, zoals het traditionele geval is geweest voor x86 en x64 vanwege het sterke geheugenmodel op deze architecturen. De /volatile:iso switch selecteert de strikte C++ standaard vluchtige semantiek die geen sterke volgorde garandeert.

In de ARM-architectuur (met uitzondering van ARM64EC) is de standaard /volatile:iso omdat ARM-processors een zwak geordend geheugenmodel hebben, en omdat ARM-software geen traditie heeft om te vertrouwen op de uitgebreide semantiek van /volatile:ms en meestal niet hoeft te communiceren met software die dat wel doet. Het is echter nog steeds handig of zelfs vereist om een ARM-programma te compileren om de uitgebreide semantiek te gebruiken. Het kan bijvoorbeeld te kostbaar zijn om een programma over te zetten om de ISO C++-semantiek te gebruiken, of stuurprogrammasoftware moet zich mogelijk houden aan de traditionele semantiek om correct te functioneren. In deze gevallen kunt u de /volatile:ms-switch gebruiken; Als u echter de traditionele vluchtige semantiek op ARM-doelen opnieuw wilt maken, moet de compiler geheugenbarrières invoegen rond elke lees- of schrijfbewerking van een volatile variabele om sterke volgorde af te dwingen, wat een negatieve invloed kan hebben op de prestaties.

Op de x86-, x64- en ARM64EC-architecturen is de standaardinstelling /volatile:ms , omdat veel van de software die al is gemaakt voor deze architecturen, afhankelijk is van msVC. Wanneer u x86-, x64- en ARM64EC-programma's compileert, kunt u de /volatile:iso-switch opgeven om onnodige afhankelijkheid van de traditionele vluchtige semantiek te voorkomen en de portabiliteit te bevorderen.

Zie ook

Microsoft C++ configureren voor ARM-processors