Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
I det här dokumentet beskrivs några vanliga problem som kan uppstå när du migrerar kod från x86- eller x64-arkitekturer till ARM-arkitekturen. Den beskriver också hur du undviker dessa problem och hur du använder kompilatorn för att identifiera dem.
Anmärkning
När den här artikeln refererar till ARM-arkitekturen gäller den både ARM32 och ARM64.
Problem med migreringskällor
Många problem som kan uppstå när du migrerar kod från x86- eller x64-arkitekturerna till ARM-arkitekturen är relaterade till källkodskonstruktioner som kan anropa odefinierat, implementeringsdefinierat eller ospecificerat beteende.
Odefinierat beteende är ett beteende som C++-standarden inte definierar, och som orsakas av en åtgärd som inte har något rimligt resultat: till exempel konvertera ett flyttalsvärde till ett osignerat heltal eller flytta ett värde med ett antal positioner som är negativa eller överskrider antalet bitar i den upphöjda typen.
Implementeringsdefinierat beteende är ett beteende som C++-standarden kräver att kompilatorns leverantör definierar och dokumenterar. Ett program kan på ett säkert sätt förlita sig på implementeringsdefinierat beteende, även om det kanske inte är portabelt. Exempel på implementeringsdefinierat beteende är storlekar på inbyggda datatyper och deras justeringskrav. Ett exempel på en åtgärd som kan påverkas av implementeringsdefinierat beteende är åtkomst till listan med variabelargument.
Ospecificerat beteende är ett beteende som C++-standarden lämnar avsiktligt icke-terministiskt. Även om beteendet anses vara icke-terministiskt, bestäms särskilda anrop av ospecificerat beteende av kompilatorimplementeringen. Det finns dock inget krav på att en kompilatorleverantör ska förbestäma resultatet eller garantera konsekvent beteende mellan jämförbara anrop, och det finns inget krav på dokumentation. Ett exempel på ospecificerat beteende är i vilken ordning underuttryck, som inkluderar argument till ett funktionsanrop, utvärderas.
Andra migreringsproblem kan hänföras till maskinvaruskillnader mellan ARM- och x86- eller x64-arkitekturer som interagerar med C++-standarden på olika sätt. Till exempel ger volatileden starka minnesmodellen i x86- och x64-arkitekturen -kvalificerade variabler några extra egenskaper som har använts för att underlätta vissa typer av kommunikation mellan trådar tidigare. Men ARM-arkitekturens svaga minnesmodell stöder inte den här användningen, och inte heller kräver C++-standarden den.
Viktigt!
Även om volatile får vissa egenskaper som kan användas för att implementera begränsade former av kommunikation mellan trådar på x86 och x64, är dessa egenskaper inte tillräckliga för att implementera kommunikation mellan trådar i allmänhet. C++-standarden rekommenderar att sådan kommunikation implementeras med lämpliga synkroniseringspri primitiver i stället.
Eftersom olika plattformar kan uttrycka den här typen av beteende på olika sätt kan det vara svårt och buggbenäget att portera programvara mellan plattformar om det beror på beteendet för en specifik plattform. Även om många av dessa typer av beteende kan observeras och kan verka stabila, är det åtminstone inte bärbart att förlita sig på dem, och i fall av odefinierat eller ospecificerat beteende är det också ett fel. Inte ens det beteende som anges i det här dokumentet bör användas och kan ändras i framtida kompilatorer eller CPU-implementeringar.
Exempel på migreringsproblem
Resten av det här dokumentet beskriver hur de olika beteendet för dessa C++-språkelement kan ge olika resultat på olika plattformar.
Konvertering av flyttal till osignerat heltal
I ARM-arkitekturen mättas konverteringen av ett flyttalsvärde till ett 32-bitars heltal till närmaste värde som heltalet kan representera om flyttalsvärdet ligger utanför det intervall som heltalet kan representera. I x86- och x64-arkitekturerna görs konverteringen om om heltalet är osignerat, eller ställs till -2147483648 om heltalet är signerat. Ingen av dessa arkitekturer stöder direkt konvertering av flyttalsvärden till mindre heltalstyper. I stället utförs konverteringarna till 32 bitar och resultaten trunkeras till en mindre storlek.
För ARM-arkitekturen innebär kombinationen av mättnad och trunkering att konvertering till osignerade typer korrekt mättar mindre osignerade typer när det mättar ett 32-bitars heltal, men ger ett trunkerat resultat för värden som är större än den mindre typen kan representera men för små för att mätta det fullständiga 32-bitars heltalet. Konverteringen utför också korrekt mättning för 32-bitars signerade heltal, men trunkering av mättade, signerade heltal resulterar i -1 för positivt mättade värden och 0 för negativt mättade värden. Konvertering till ett mindre signerat heltal ger ett trunkerat resultat som är oförutsägbart.
För x86- och x64-arkitekturerna gör kombinationen av wrap-around-beteende för osignerade heltalskonverteringar och explicit värdesättning för signerade heltalskonverteringar vid overflow, tillsammans med trunkering, resultaten för de flesta skift oförutsägbara om de är för stora.
Dessa plattformar skiljer sig också åt i hur de hanterar konvertering av NaN (inte ett tal) till heltalstyper. På ARM konverterar NaN till 0x00000000; på x86 och x64 konverteras den till 0x80000000.
Flyttalskonvertering kan bara användas om du vet att värdet ligger inom intervallet för den heltalstyp som det konverteras till.
Skiftoperator-beteende (<<>>)
I ARM-arkitekturen kan ett värde flyttas åt vänster eller höger upp till 255 bitar innan mönstret börjar upprepas. I x86- och x64-arkitekturer upprepas mönstret vid varje multipel av 32 om inte källan till mönstret är en 64-bitarsvariabel. I så fall upprepas mönstret vid varje multipel av 64 på x64 och varje multipel av 256 på x86, där en programvaruimplementering används. En 32-bitarsvariabel som till exempel har värdet 1 skiftad till vänster med 32 positioner är resultatet 0 på ARM, på x86 är resultatet 1 och på x64 är resultatet också 1. Men om källan till värdet är en 64-bitarsvariabel är resultatet på alla tre plattformarna 4294967296 och värdet "omsluts" inte förrän det har flyttats 64 positioner på x64 eller 256 positioner på ARM och x86.
Eftersom resultatet av en skiftåtgärd som överskrider antalet bitar i källtypen är odefinierat, behöver kompilatorn inte ha konsekvent beteende i alla situationer. Om båda operanderna i ett skift till exempel är kända vid kompileringstillfället kan kompilatorn optimera programmet med hjälp av en intern rutin för att förkompilera resultatet av skiftet och sedan ersätta resultatet i stället för skiftåtgärden. Om skiftmängden är för stor eller negativ kan resultatet av den interna rutinen skilja sig från resultatet av samma skiftuttryck som processorn kör.
Beteende för variabelargument (varargs)
I ARM-arkitekturen kan parametrar från listan med variabelargument som skickas i stacken justeras. En 64-bitarsparameter justeras till exempel mot en 64-bitarsgräns. På x86 och x64 påverkas argument som skickas på stacken inte av justering och packas tätt. Den här skillnaden kan orsaka en variadisk funktion som printf att läsa minnesadresser som var avsedda som utfyllnad på ARM om den förväntade layouten för listan med variabelargument inte matchas exakt, även om den kan fungera för en delmängd av vissa värden i x86- eller x64-arkitekturerna. Tänk på det här exemplet:
// 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);
I det här fallet kan felet åtgärdas genom att se till att rätt formatspecifikation används så att argumentets justering beaktas. Den här koden är korrekt:
// CORRECT: use %I64d for 64-bit integers
printf("%I64d\n", 1LL);
Utvärderingsordning för argument
Eftersom ARM-, x86- och x64-processorer är så olika kan de presentera olika krav för kompilatorimplementeringar och även olika optimeringsmöjligheter. På grund av detta kan en kompilator tillsammans med andra faktorer som anropskonvention och optimeringsinställningar utvärdera funktionsargument i en annan ordning i olika arkitekturer eller när de andra faktorerna ändras. Detta kan orsaka att beteendet för en app som förlitar sig på en viss utvärderingsordning ändras oväntat.
Den här typen av fel kan inträffa när argument till en funktion har biverkningar som påverkar andra argument för funktionen i samma anrop. Vanligtvis är den här typen av beroende lätt att undvika men kan döljas av beroenden som är svåra att urskilja eller av operatoröverbelastning. Tänk dig det här kodexemplet:
handle memory_handle;
memory_handle->acquire(*p);
Detta verkar väldefinierat, men om -> och * är överbelastade operatorer översätts den här koden till något som liknar detta:
Handle::acquire(operator->(memory_handle), operator*(p));
Och om det finns ett beroende mellan operator->(memory_handle) och operator*(p)kan koden förlita sig på en specifik utvärderingsordning, även om den ursprungliga koden ser ut som om det inte finns något möjligt beroende.
volatile standardbeteende för nyckelord
MSVC-kompilatorn stöder två olika tolkningar av volatile lagringskvalificeraren som du kan ange med hjälp av kompilatorväxlar.
Växeln /volatile:ms väljer Microsofts utökade flyktiga semantik som garanterar stark ordning, vilket har varit det traditionella fallet för x86 och x64 på grund av den starka minnesmodellen i dessa arkitekturer.
Växeln /volatile:iso väljer den strikta C++-standardens flyktiga semantik som inte garanterar stark ordning.
I ARM-arkitekturen (förutom ARM64EC) är standardvärdet /volatile:iso eftersom ARM-processorer har en svagt ordnad minnesmodell, och eftersom ARM-programvaran inte har ett arv av att förlita sig på den utökade semantiken /volatile:ms och vanligtvis inte behöver interagera med programvara som gör det. Det är dock fortfarande ibland praktiskt eller till och med nödvändigt att kompilera ett ARM-program för att använda den utökade semantiken. Det kan till exempel vara för dyrt att porta ett program för att använda ISO C++-semantiken, eller så kan drivrutinsprogramvaran behöva följa den traditionella semantiken för att fungera korrekt. I dessa fall kan du använda växeln /volatile:ms ; Men för att återskapa den traditionella flyktiga semantiken på ARM-mål måste kompilatorn infoga minnesbarriärer runt varje läsning eller skrivning av en volatile variabel för att framtvinga stark ordning, vilket kan ha en negativ inverkan på prestanda.
På arkitekturerna x86, x64 och ARM64EC är standardvärdet /volatile:ms eftersom mycket av programvaran som redan har skapats för dessa arkitekturer med hjälp av MSVC förlitar sig på dem. När du kompilerar x86-, x64- och ARM64EC-program kan du ange växeln /volatile:iso för att undvika onödigt beroende av traditionella flyktiga semantik och för att främja portabilitet.