Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
In diesem Dokument werden einige der häufig auftretenden Probleme beschrieben, die beim Migrieren von Code von x86- oder x64-Architekturen zur ARM-Architektur auftreten können. Außerdem wird beschrieben, wie sie diese Probleme vermeiden und wie sie mithilfe des Compilers identifiziert werden können.
Hinweis
Wenn sich dieser Artikel auf die ARM-Architektur bezieht, gilt er sowohl für ARM32 als auch für ARM64.
Quellen für Migrationsprobleme
Viele Probleme, auf die Sie bei der Codemigration von der x86- oder x64-Architektur auf die ARM-Architektur möglicherweise stoßen, hängen mit Quellcodekonstrukten zusammen, die ein nicht definiertes, durch die Implementierung definiertes oder nicht spezifiziertes Verhalten hervorrufen können.
Undefiniertes Verhalten ist das Verhalten , das der C++-Standard nicht definiert, und das wird durch einen Vorgang verursacht, der kein angemessenes Ergebnis hat: z. B. das Konvertieren eines Gleitkommawerts in eine nicht signierte ganze Zahl oder das Verschieben eines Werts um eine Anzahl von Positionen, die negativ sind oder die Anzahl der Bits in seinem höhergestuften Typ überschreitet.
Durch die Implementierung definiertes Verhalten ist ein Verhalten, das vom C++-Standard verlangt wird, vom Compileranbieter definiert und dokumentiert zu werden. Ein Programm kann sich sicher auf durch die Implementierung definiertes Verhalten verlassen, auch wenn dies nicht immer portierbar ist. Beispiele für durch die Implementierung definiertes Verhalten sind u. a. die Größe der integrierten Datentypen und ihre Ausrichtungsanforderungen. Ein Beispiel für einen Vorgang, der von durch die Implementierung definiertem Verhalten beeinflusst werden kann, ist der Zugriff auf die Liste der variablen Argumente.
Nicht angegebenes Verhalten ist das Verhalten , das der C++-Standard absichtlich nicht deterministisch verlässt. Obwohl das Verhalten als nicht deterministisch betrachtet wird, werden bestimmte Aufrufe eines nicht angegebenen Verhaltens durch die Compilerimplementierung bestimmt. Es ist jedoch nicht erforderlich, dass ein Compileranbieter das Ergebnis vorbestimmt oder ein konsistentes Verhalten zwischen vergleichbaren Aufrufen garantiert und keine Dokumentation erforderlich ist. Ein Beispiel für nicht spezifiziertes Verhalten ist die Reihenfolge, in der Unterausdrücke, die Argumente zu einem Funktionsaufruf enthalten, ausgewertet werden.
Andere Migrationsprobleme können auf Hardwareunterschiede zwischen ARM- und x86- bzw. x64-Architekturen zurückzuführen sein, die auf unterschiedliche Weise mit dem C++-Standard interagieren. Beispielsweise verleiht das starke Speichermodell der x86- und x64-Architektur den volatile-qualifizierten Variablen einige zusätzliche Eigenschaften, die in der Vergangenheit genutzt wurden, um bestimmte Arten der Kommunikation zwischen Threads zu erleichtern. Das schwache Speichermodell der ARM-Architektur unterstützt diese Verwendung nicht, und auch der C++-Standard erfordert dies nicht.
Wichtig
Obwohl volatile einige Eigenschaften gewonnen hat, die zur Implementierung eingeschränkter Formen der Interthreadkommunikation auf x86 und x64 verwendet werden können, sind diese Eigenschaften nicht ausreichend, um eine allgemeine Interthreadkommunikation zu ermöglichen. Der C++-Standard empfiehlt, dass eine solche Kommunikation stattdessen mithilfe geeigneter Synchronisierungsprimitiven implementiert wird.
Da verschiedene Plattformen diese Art von Verhalten unterschiedlich ausdrücken können, ist die Portierung von Software zwischen Plattformen unter Umständen schwierig und fehleranfällig, wenn sie vom Verhalten einer bestimmten Plattform abhängt. Obwohl viele dieser Verhaltensweisen beobachtet werden können und stabil erscheinen mögen, ist es zumindest nicht portierbar, sich auf sie zu verlassen, und in den Fällen von nicht definiertem oder nicht spezifiziertem Verhalten ist dies auch ein Fehler. Selbst das in diesem Dokument zitierte Verhalten sollte nicht verwendet werden und könnte sich in zukünftigen Compilern oder CPU-Implementierungen ändern.
Beispiel für Migrationsprobleme
Im restlichen Dokument wird beschrieben, wie das unterschiedliche Verhalten dieser C++-Sprachelemente auf verschiedenen Plattformen zu unterschiedlichen Ergebnissen führen kann.
Konvertierung von Gleitkommazahlen in ganze Zahlen ohne Vorzeichen
Bei der ARM-Architektur erfolgt die Konvertierung eines Gleitkommawerts in eine 32-Bit-Ganzzahl auf den nächsten Wert, den die Ganzzahl darstellen kann, wenn der Gleitkommawert außerhalb des Bereichs liegt, den die Ganzzahl darstellen kann. Bei den x86- und x64-Architekturen wird die Konvertierung umgebrochen, wenn die ganze Zahl kein Vorzeichen hat, oder auf -2147483648 gesetzt, wenn die ganze Zahl ein Vorzeichen aufweist. Keine dieser Architekturen unterstützt direkt die Konvertierung von Gleitkommawerten in kleinere ganzzahlige Typen; stattdessen werden die Konvertierungen auf 32 Bit durchgeführt, und die Ergebnisse werden auf eine kleinere Größe gekürzt.
Für die ARM-Architektur bedeutet die Kombination von Sättigung und Kürzung, dass bei der Konvertierung in Typen ohne Vorzeichen kleinere Typen ohne Vorzeichen korrekt gesättigt werden, wenn diese eine 32-Bit-Ganzzahl sättigt, aber ein gekürztes Ergebnis für Werte erzeugt wird, die größer sind, als der kleinere Typ darstellen kann, aber zu klein, um die volle 32-Bit-Ganzzahl zu sättigen. Bei der Konvertierung werden auch 32-Bit-Ganzzahlen mit Vorzeichen korrekt gesättigt, aber das Kürzen von gesättigten, ganzen Zahlen mit Vorzeichen ergibt -1 für positiv gesättigte Werte und 0 für negativ gesättigte Werte. Die Konvertierung in einen kürzeren ganzzahligen Datentyp mit Vorzeichen erzeugt ein verkürztes, nicht vorhersehbares Ergebnis.
Bei den x86- und x64-Architekturen macht die Kombination aus Umbruchverhalten bei Konvertierungen von ganzen Zahlen ohne Vorzeichen und der expliziten Bewertung für Konvertierungen von ganzen Zahlen mit Vorzeichen bei einer Überschreitung, zusammen mit der Kürzung, die Ergebnisse für die meisten Verschiebungen unvorhersagbar, wenn sie zu groß sind.
Diese Plattformen unterscheiden sich auch darin, wie sie die Konvertierung von NaN (Not-a-Number) in ganzzahlige Typen verarbeiten. Bei ARM-Architekturen wird NaN in 0x0000000000, bei x86- und x64-Architekturen in 0x80000000 konvertiert.
Auf die Konvertierung von Gleitkommawerten kann man sich nur verlassen, wenn bekannt ist, dass der Wert innerhalb des Bereichs des ganzzahligen Typs liegt, in den er konvertiert wird.
Umschaltoperatorverhalten (<<>>)
Bei der ARM-Architektur kann ein Wert um bis zu 255 Bit nach links oder rechts verschoben werden, bevor sich das Muster wiederholt. Bei x86- und x64-Architekturen wird das Muster bei jedem Vielfachen von 32 wiederholt, es sei denn, die Quelle des Musters ist eine 64-Bit-Variable. In diesem Fall wiederholt sich das Muster bei jedem Vielfachen von 64 auf x64 und jedes Vielfache von 256 auf x86, wobei eine Softwareimplementierung verwendet wird. Zum Beispiel ist für eine 32-Bit-Variable, die einen um 32 Positionen nach links verschobenen Wert von 1 hat, bei ARM das Ergebnis 0, bei x86 ist das Ergebnis 1 und bei x64 ist das Ergebnis ebenfalls 1. Wenn die Quelle des Werts jedoch eine 64-Bit-Variable ist, dann ist das Ergebnis auf allen drei Plattformen 4294967296, und der Wert wird erst dann „umgebrochen“, wenn er um 64 Positionen bei x64 bzw. 256 Positionen bei ARM und x86 verschoben wird.
Da das Ergebnis eines Schichtvorgangs, der die Anzahl der Bits im Quelltyp überschreitet, nicht definiert ist, muss der Compiler in allen Situationen kein einheitliches Verhalten aufweisen. Wenn zum Beispiel beide Operanden einer Verschiebung zur Kompilierzeit bekannt sind, kann der Compiler das Programm optimieren, indem er mit einer internen Routine das Ergebnis der Verschiebung vorab berechnet und dann den Verschiebevorgang durch das Ergebnis ersetzt. Wenn der Betrag der Verschiebung zu groß oder negativ ist, kann das Ergebnis der internen Routine anders ausfallen als das Ergebnis desselben Verschiebeausdrucks, der von der CPU ausgeführt wird.
Verhalten von variablen Argumenten (VarArgs)
Bei der ARM-Architektur unterliegen Parameter aus der Liste der variablen Argumente, die auf dem Stapel übergeben werden, der Ausrichtung. Beispielsweise wird ein 64-Bit-Parameter an einer 64-Bit-Grenze ausgerichtet. Bei x86 und x64 werden Argumente, die auf dem Stapel übergeben werden, nicht ausgerichtet und werden dicht gepackt. Dieser Unterschied kann dazu führen, dass eine variadische Funktion wie printf Speicheradressen liest, die als Abstand auf ARM vorgesehen waren, wenn das erwartete Layout der Variablenargumentliste nicht exakt übereinstimmt, obwohl es für eine Teilmenge einiger Werte in den x86- oder x64-Architekturen möglicherweise funktioniert. Betrachten Sie das folgende Beispiel:
// 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 diesem Fall kann der Fehler behoben werden, indem sichergestellt wird, dass die richtige Formatangabe verwendet wird, sodass die Ausrichtung des Arguments berücksichtigt wird. Dieser Code ist korrekt:
// CORRECT: use %I64d for 64-bit integers
printf("%I64d\n", 1LL);
Auswertungsreihenfolge der Argumente
Da ARM-, x86- und x64-Prozessoren so unterschiedlich sind, können sie unterschiedliche Anforderungen an Compilerimplementierungen und auch unterschiedliche Möglichkeiten für Optimierungen darstellen. Aus diesem Grund und wegen anderer Faktoren wie Aufrufkonvention und Optimierungseinstellungen könnte ein Compiler Funktionsargumente in unterschiedlicher Reihenfolge für verschiedene Architekturen oder bei Änderung der anderen Faktoren auswerten. Dies kann dazu führen, dass sich das Verhalten einer App, die auf einer bestimmten Auswertungsreihenfolge beruht, unerwartet ändert.
Diese Art von Fehler kann auftreten, wenn Argumente für eine Funktion Nebeneffekte haben, die sich auf andere Argumente für die Funktion im gleichen Aufruf auswirken. In der Regel ist diese Art von Abhängigkeit leicht zu vermeiden, kann aber von Abhängigkeiten verdeckt werden, die schwer zu erkennen sind oder durch Operatorüberladungen. Betrachten Sie dieses Codebeispiel:
handle memory_handle;
memory_handle->acquire(*p);
Dies ist zwar klar definiert, aber wenn -> und * überladene Operatoren sind, dann wird dieser Code in etwas übersetzt, das dem Folgenden ähnelt:
Handle::acquire(operator->(memory_handle), operator*(p));
Und wenn zwischen operator->(memory_handle) und operator*(p) eine Abhängigkeit besteht, kann der Code von einer bestimmten Auswertungsreihenfolge abhängen, obwohl der ursprüngliche Code so aussieht, als ob es keine mögliche Abhängigkeit gibt.
volatile Standardverhalten des Schlüsselworts
Der Microsoft C++-Compiler (MSVC) unterstützt zwei verschiedene Interpretationen des volatile Speicherqualifizierers, den Sie mithilfe von Compilerschaltern angeben können. Der Schalter /volatile:ms wählt die erweiterte volatile Semantik von Microsoft aus, die eine feste Reihenfolge sicherstellt, wie dies traditionell für x86 und x64 aufgrund des starken Speichermodells bei diesen Architekturen der Fall war. Der Schalter /volatile:iso wählt die strikte volatile Semantik des C++-Standards aus, die keine feste Reihenfolge garantiert.
Auf der ARM-Architektur (mit Ausnahme von ARM64EC) ist /volatile:iso standardmäßig eingestellt, da ARM-Prozessoren ein schwach geordnetes Speichermodell aufweisen und ARM-Software traditionell nicht auf die erweiterten Semantiken von /volatile:ms angewiesen ist und normalerweise nicht mit Software interagieren muss, die das tut. Dennoch ist es manchmal praktisch oder sogar erforderlich, ein ARM-Programm zu kompilieren, um die erweiterte Semantik zu verwenden. Beispielsweise ist es möglicherweise zu teuer, ein Programm für die Verwendung der ISO-C++-Semantik zu portieren, oder die Treibersoftware muss der herkömmlichen Semantik entsprechen, damit sie ordnungsgemäß funktioniert. In diesen Fällen können Sie den Parameter /volatile: ms verwenden. Zum erneuten Erstellen der traditionellen volatilen Semantik bei ARM-Zielen muss der Compiler jedoch Arbeitsspeicherabgrenzungen um alle Lese- oder Schreibvorgänge einer volatile-Variablen einfügen, um eine feste Reihenfolge zu erzwingen, was sich negativ auf die Leistung auswirken kann.
Auf den Architekturen x86, x64 und ARM64EC lautet der Standardwert "/volatile:ms ", da ein Großteil der Software, die bereits mit MSVC erstellt wurde, für diese Architekturen erstellt wurde. Wenn Sie x86-, x64- und ARM64EC-Programme kompilieren, können Sie den Switch "/volatile:iso " angeben, um unnötige Abhängigkeiten von der herkömmlichen veränderbaren Semantik zu vermeiden und die Portabilität zu fördern.