Megosztás:


A Microsoft C++ ARM áttelepítésének gyakori problémái

Ez a dokumentum ismerteti azokat a gyakori problémákat, amelyekkel az x86-os vagy x64-architektúrákból az ARM-architektúrába való kódáttelepítés során találkozhat. Azt is ismerteti, hogyan kerülheti el ezeket a problémákat, és hogyan használhatja a fordítót az azonosításukra.

Megjegyzés:

Amikor ez a cikk az ARM-architektúrára hivatkozik, az ARM32-re és az ARM64-re is vonatkozik.

A migrálási problémák forrásai

Az x86-os vagy x64-architektúrákból az ARM-architektúrába való kódáttelepítéssel kapcsolatos számos probléma olyan forráskód-szerkezetekhez kapcsolódik, amelyek meghatározatlan, implementált vagy meghatározatlan viselkedést indíthatnak el.

A nem definiált viselkedés az a viselkedés, amelyet a C++ szabvány nem határoz meg, és ezt egy olyan művelet okozza, amelynek nincs ésszerű eredménye: például egy lebegőpontos érték hozzárendelés nélküli egész számmá alakítása, vagy egy érték eltolása több olyan pozícióval, amely negatív vagy meghaladja az előléptetett típus bitjeinek számát.

A implementáció által definiált viselkedés az a viselkedés, amelyet a C++ szabvány megkövetel a fordító szállítójától, hogy definiálja és dokumentálja. A programok biztonságosan támaszkodhatnak a implementáció által meghatározott viselkedésre, annak ellenére, hogy ez nem feltétlenül hordozható. A implementáció által definiált viselkedés például a beépített adattípusok mérete és azok igazítási követelményei. Példa egy olyan műveletre, amelyre hatással lehet a implementáció által definiált viselkedés, ha hozzáfér a változóargumentumok listájához.

A meghatározatlan viselkedés az a viselkedés, amelyet a C++ szabvány szándékosan nem határoz meg. Bár a viselkedést nem determinisztikusnak tekintik, a nem meghatározott viselkedések meghívását a fordító implementációja határozza meg. Nincs azonban szükség arra, hogy a fordító szállítója előre meghatározza az eredményt, vagy konzisztens viselkedést garantáljon a hasonló meghívások között, és nincs szükség a dokumentációra. A meghatározatlan viselkedésre példa az a sorrend, amelyben a függvényhívás argumentumait tartalmazó alkifejezések kiértékelése történik.

Az egyéb migrálási problémák az ARM és az x86 vagy x64 architektúrák közötti hardverkülönbségeknek tulajdoníthatók, amelyek másképp használják a C++ szabványt. Az x86-os és x64-architektúra erős memóriamodellje például olyan további tulajdonságokat biztosít volatilea minősített változók számára, amelyeket a múltban bizonyos típusú szálközi kommunikáció megkönnyítésére használtak. Az ARM-architektúra gyenge memóriamodellje azonban nem támogatja ezt a használatot, és a C++ szabvány sem követeli meg.

Fontos

Bár volatile szerezhet olyan tulajdonságokat, amelyek az x86 és x64 platformokon a szálközi kommunikáció korlátozott formáinak megvalósítására használhatók, ezek a tulajdonságok nem elegendőek a szálközi kommunikáció általános megvalósításához. A C++ szabvány azt javasolja, hogy az ilyen kommunikációt a megfelelő szinkronizálási primitívek használatával implementálják.

Mivel a különböző platformok eltérő módon fejezhetik ki ezeket a viselkedéseket, a szoftverek platformok közötti portolása nehéz és hibalehetőséget jelenthet, ha egy adott platform viselkedésétől függ. Bár sok ilyen viselkedés figyelhető meg, és stabilnak tűnhet, az ezekre való támaszkodás legalább nem hordozható, és a meghatározatlan vagy meghatározatlan viselkedés esetén is hiba. Még a dokumentumban idézett viselkedésre sem szabad támaszkodni, és a jövőbeni fordítókban vagy CPU-implementációkban is változhat.

Példa áttelepítési problémákra

A dokumentum többi része azt ismerteti, hogy ezeknek a C++ nyelvi elemeknek a különböző viselkedése hogyan hozhat különböző eredményeket különböző platformokon.

Lebegőpontos szám átalakítása előjel nélküli egész számmá

Az ARM-architektúrában, ha egy lebegőpontos értéket 32 bites egész számmá alakítunk át, az a legközelebbi értékre korlátozódik, amelyet az egész szám képviselhet, ha a lebegőpontos érték kívül esik azon a tartományon, amelyet az egész szám képviselhet. Az x86- és x64-architektúrákon az átalakítás körbefut, ha az egész szám előjel nélküli, vagy -2147483648-ra állítódik, ha az egész szám előjeles. Ezen architektúrák egyike sem támogatja közvetlenül a lebegőpontos értékek kisebb egész számtípusokra való átalakítását; ehelyett a konvertálások 32 bitesre lesznek végrehajtva, és az eredményeket kisebb méretre csonkolja a rendszer.

Az ARM-architektúra esetében a telítettség és a csonkolás kombinációja azt jelenti, hogy az aláíratlan típusokra való átalakítás megfelelően telíti a kisebb, alá nem írt típusokat, ha egy 32 bites egész számot telít, de csonkolt eredményt ad a kisebb típusnál nagyobb értékek esetében, de túl kicsi ahhoz, hogy a teljes 32 bites egész számot telíthesse. Az átalakítás a 32 bites előjeles egész számok esetében is megfelelően telít, de a telített, előjeles egész számok csonkolása pozitívan telített értékek esetén -1-t eredményez, a negatívan telített értékek esetén pedig 0-t. A kisebb előjeles egész számmá való átalakítás kiszámíthatatlan csonkolt eredménnyel jár.

Az x86 és x64 architektúrák esetén az aláíratlan egész szám konverziók körbefordulási viselkedése, az aláírt egész szám konverziók túlcsorduláskor történő explicit értékelése, valamint a csonkolás kombinációja kiszámíthatatlanná teheti az eltolások eredményeit, ha túl nagyok.

Ezek a platformok abban is különböznek, hogy hogyan kezelik a NaN (Not-a-Number) egész típusúra való konvertálását. ARM-en a NaN 0x00000000; x86 és x64 rendszeren 0x80000000 lesz.

A lebegőpontos átalakításra csak akkor lehet támaszkodni, ha tudja, hogy az érték a konvertálandó egész szám típusának tartományán belül van.

Shift operátor (<<>>) viselkedése

Az ARM-architektúrában egy érték akár 255 bitesre is eltolható, mielőtt a minta ismétlődni kezd. X86 és x64 architektúrák esetén a minta a 32 minden többszörösénél ismétlődik, kivéve, ha a minta forrása egy 64 bites változó. Ebben az esetben a minta x64-en a 64 minden többszörösénél, x86-on pedig a 256-os többszörösénél ismétlődik, ahol szoftveres implementációt alkalmaznak. Például egy 32 bites változó esetében, amelynek értéke 1 balra 32 pozícióval balra, ARM-ben az eredmény 0, x86-on az eredmény 1, x64-en pedig 1. Ha azonban az érték forrása egy 64 bites változó, akkor az eredmény mindhárom platformon 4294967296, és az érték nem "körbefut", amíg el nem tolódik 64 pozíció x64-en, vagy 256 pozíciót az ARM-en és az x86-on.

Mivel a forrástípus bitjeinek számát meghaladó műszakművelet eredménye nincs meghatározva, a fordítónak nem kell konzisztens viselkedést kialakítania minden helyzetben. Ha például egy műszak mindkét operandusa ismert a fordításkor, a fordító egy belső rutin használatával optimalizálhatja a programot a műszak eredményének előkomponálására, majd az eredmény helyettesítésére a műszakművelet helyett. Ha az eltolás mértéke túl nagy vagy negatív, a belső rutin eredménye eltérhet a processzor által végrehajtott eltolási kifejezés eredményétől.

Változóargumentumok (varargs) viselkedése

Az ARM architektúrán a stacken továbbított, a változó argumentumok listájából származó paraméterek igazításnak vannak alávetve. Egy 64 bites paraméter például egy 64 bites határvonalhoz van igazítva. X86-on és x64-en a veremen átadott argumentumokat nem szükséges igazítani, és szorosan illeszkednek. A különbség azt eredményezheti, hogy egy variadikus függvény, mint például printf, az ARM-en kitöltésként szánt memóriacímet olvas, ha a változó argumentumok listájának elvárt elrendezése nem egyezik meg pontosan, még akkor is, ha ez x86 vagy x64 architektúráknál egyes értékek esetén működhet. Fontolja meg ezt a példát:

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

Ebben az esetben a hiba kijavítható úgy, hogy a megfelelő formátumspecifikációt használja az argumentum igazításának figyelembe adásával. Ez a kód helyes:

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

Argumentum-kiértékelési sorrend

Mivel az ARM, az x86 és az x64 processzorok annyira eltérőek, különböző követelményeket támaszthatnak a fordítók implementációihoz, és különböző optimalizálási lehetőségeket is kínálhatnak. Emiatt más tényezőkkel, például a híváskonvencióval és az optimalizálási beállításokkal együtt a fordító más sorrendben értékelheti ki a függvényargumentumokat a különböző architektúrákon, vagy ha a többi tényező módosul. Ez azt okozhatja, hogy egy adott értékelési sorrendre támaszkodó alkalmazás viselkedése váratlanul megváltozik.

Ez a hiba akkor fordulhat elő, ha egy függvény argumentumai olyan mellékhatásokat okoznak, amelyek hatással vannak a függvény más argumentumaira ugyanabban a hívásban. Általában ez a fajta függőség könnyen elkerülhető, de elhomályosíthatja a függőségek, amelyek nehezen felismerhető, vagy operátorok túlterhelése. Tekintse meg ezt a példakódot:

handle memory_handle;

memory_handle->acquire(*p);

Ez jól definiáltnak tűnik, de ha ->* túlterhelt operátorok, akkor a kód a következőhöz hasonlóra lesz lefordítva:

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

És ha függőség van operator->(memory_handle) és operator*(p) között, a kód egy adott kiértékelési sorrendre támaszkodhat, annak ellenére, hogy az eredeti kód úgy tűnik, nincs lehetséges függőség.

volatile kulcsszó alapértelmezett viselkedése

A Microsoft C++ (MSVC) fordító támogatja a volatile tárterület-minősítő két különböző értelmezését, amelyeket fordítókapcsolók használatával adhat meg. A /volatile:ms kapcsoló a Microsoft kiterjesztett illékony szemantikáját választja, amely garantálja az erős rendezést, ahogyan az x86 és az x64 esetében is előfordult, mivel ezeken az architektúrákon erős memóriamodell található. A /volatile:iso kapcsoló kiválasztja a szigorú C++ szabvány szerinti 'volatile' szemantikát, amely nem garantálja az erős sorrendet.

Az ARM-architektúrában (ARM64EC kivételével) az alapértelmezett érték a /volatile:iso , mivel az ARM-processzorok gyengén rendezett memóriamodellel rendelkeznek, és mivel az ARM-szoftverek nem rendelkeznek a /volatile:ms kiterjesztett szemantikájának támaszkodásával, és általában nem kell csatlakozniuk azokhoz a szoftverekhez, amelyek igen. Azonban néha még mindig kényelmes vagy akár szükséges lehet egy ARM-program fordítása a használatra a kiterjesztett szemantika szerint. Előfordulhat például, hogy túl költséges egy program portálása az ISO C++ szemantikához, vagy az illesztőprogram-szoftvereknek a hagyományos szemantikához kell igazodnia a helyes működéshez. Ezekben az esetekben használhatja a /volatile:ms kapcsolót; Azonban az ARM-célok hagyományos illékony szemantikájának újbóli létrehozásához a fordítónak memóriakorlátokat kell beszúrnia egy volatile változó minden egyes olvasása vagy írása köré az erős rendezés kényszerítése érdekében, ami negatív hatással lehet a teljesítményre.

Az x86, x64 és ARM64EC architektúrák esetében az alapértelmezett érték a /volatile:ms , mivel az msVC használatával már létrehozott szoftverek nagy része ezekre támaszkodik. Az x86, x64 és ARM64EC programok lefordításakor megadhatja az /volatile:iso kapcsolót, hogy elkerülje a hagyományos illékony szemantikára való szükségtelen támaszkodást, és elősegítse a hordozhatóságot.

Lásd még

A Microsoft C++ konfigurálása ARM-processzorokhoz