次の方法で共有


Visual C++ の ARM への移行に関する一般的な問題

Microsoft C++ コンパイラ (MSVC) を使用する場合、同じ C++ ソースコードでも、x86 または x64 アーキテクチャと比べて、ARM アーキテクチャでは異なる結果が生成される可能性があります。

移行に関する問題の原因

x86 または x64 アーキテクチャから ARM アーキテクチャにコードを移行するときに発生する可能性のある多くの問題は、未定義、実装定義、または未指定の動作を呼び出す可能性があるソース コードのコンストラクトに関連しています。

"未定義の動作" は、C++ 標準で定義されていない動作であり、合理的な結果を得られない演算 (浮動小数点値を符号なし整数に変換したり、負の数またはその昇格された型のビット数を超える位置によって値をシフトしたりするなど) によって発生します。

"実装定義の動作" は、コンパイラ ベンダーが定義して文書化するように C++ の標準で求められている動作です。 プログラムでは、実装定義の動作に依存しても安全です。ただし、そうすることで移植可能でなくなる場合があります。 実装定義の動作の例としては、組み込みデータ型のサイズとそのアラインメント要件があります。 実装定義の動作によって影響を受ける可能性がある操作の例として、可変長引数リストへのアクセスがあります。

"未指定の動作" は、C++ 標準で意図的に非決定的のままにされている動作です。 動作は非決定的であると見なされますが、未指定の動作の特定の呼び出しは、コンパイラの実装によって決定されます。 ただし、コンパイラ ベンダーが結果を事前に決めたり、比較可能な呼び出し間での一貫した動作を保証したりする必要はありません。また、文書化の要件はありません。 未指定の動作の例として、関数呼び出しの引数を含むサブ式が評価される順序があります。

移行に関するその他の問題は、C++ 標準との対話方法が異なるという、ARM と x86 または x64 アーキテクチャのハードウェアの違いに起因します。 たとえば、x86 および x64 アーキテクチャの強力なメモリ モデルでは、過去に特定の種類のスレッド間通信を容易にするために使用されていた追加のプロパティが volatile 修飾変数に提供されます。 しかし、ARM アーキテクチャの弱いメモリ モデルでは、この使用がサポートされておらず、また C++ 標準でもこれは要求されていません。

重要

volatile には、x86 と x64 でのスレッド間通信の制限された形式の実装に使用できるいくつかのプロパティがありますが、これらの追加のプロパティは、一般にスレッド間通信を実装するのに十分ではありません。 C++ 標準では、そのような通信は代わりに適切な同期プリミティブを使用して実装するように推奨されています。

プラットフォームが異なると、これらの種類の動作が異なる可能性があるため、ソフトウェアが特定のプラットフォームの動作に依存している場合、プラットフォーム間でのソフトウェアの移植は困難になり、バグが発生しやすくなる可能性があります。 これらの種類の動作の多くは確認でき、安定していると思われるかもしれませんが、それらに依存していると、少なくとも移植可能ではなく、未定義または未指定の動作の場合はエラーにもなります。 このドキュメントで説明されている動作についても、依存しないようにする必要があり、将来のコンパイラまたは CPU 実装で変更される可能性があります。

移行に関する問題の例

このドキュメントの残りの部分では、これらの C++ 言語要素の異なる動作によって、異なるプラットフォームで異なる結果が生成されるしくみについて説明します。

浮動小数点値から符号なし整数への変換

ARM アーキテクチャでは、浮動小数点値が 32 ビット整数に変換されるときに、浮動小数点値が整数で表現できる範囲外の場合には、整数が表すことができる最も近い値に飽和します。 x86 および x64 アーキテクチャでは、整数が符号なしである場合は変換はラップされ、整数が符号付きの場合は -2147483648 に設定されます。 これらのアーキテクチャのいずれでも、浮動小数点値から小さい整数型への変換は直接サポートされていません。代わりに、32 ビットへの変換が実行され、結果は小さいサイズに切り捨てられます。

ARM アーキテクチャでは、飽和と切り捨ての組み合わせによって、符号なしの型への変換では、32 ビット整数が生成されるときに、より小さい符号なしの型が正しく生成されますが、値が小さい型が表すことができるよ大きいが、完全な 32 ビット整数を飽和するには小さ過ぎる場合、切り捨てられた結果が生成されます。 変換では 32 ビット符号付き整数に対しても正しく飽和しますが、飽和状態の符号付き整数の切り捨てにより、正の飽和値の場合は -1、負の飽和値の場合は 0 になります。 より小さな符号付き整数への変換では、予測できない切り捨て結果が生成されます。

x86 および x64 アーキテクチャでは、符号なし整数変換のラップ動作と、オーバーフロー時の符号付き整数変換の明示的な評価の組み合わせと、切り捨てにより、ほどんどのシフトの結果は、大きすぎる場合に予測不可能になります。

これらのプラットフォームは、NaN (非数) から整数型への変換を処理する方法にも違いがあります。 ARM では、NaN は0x00000000 に変換されますが、x86 と x64 では、0x80000000 に変換されます。

浮動小数点の変換は、値が変換先の整数型の範囲内にあることがわかっている場合にのみ信頼できます。

シフト演算子 (<<>>) の動作

ARM アーキテクチャでは、パターンの繰り返しが開始されるまで、値を左または右に 255 ビットまでシフトできます。 x86 および x64 アーキテクチャでは、パターンのソースが 64 ビットの変数でない限り、32 のすべての倍数でパターンが繰り返されます。64 ビットの変数の場合は、パターンは、x64 では 64 のすべての倍数、x86 では 256 の倍数 (ソフトウェア実装が採用されている) で繰り返されます。 たとえば、値 1 がで左に 32 シフトされた 32 ビット変数の場合、ARM では結果は 0 になり、x86 では結果は 1 になります。x64 の場合も結果は 1 になります。 ただし、値のソースが 64 ビットの変数の場合、3 つのすべてのプラットフォームでの結果は 4294967296 になります。また、値は、64 個 (x64 の場合) または 256 個 (ARM と x86 の場合) シフトされるまで "ラップ" されません。

ソース型のビット数を超えるシフト演算の結果は未定義であるため、コンパイラはすべての状況での一貫した動作を要求されません。 たとえば、シフトの両方のオペランドがコンパイル時にわかっている場合、コンパイラは、内部ルーチンを使用してシフトの結果を事前計算してから、シフト演算の場所に結果を代入することで、プログラムを最適化できます。 シフト量が大きすぎる場合、または負の場合は、内部ルーチンの結果が、CPU によって実行される同じシフト式の結果と異なる可能性があります。

可変長引数 (varargs) の動作

ARM アーキテクチャでは、スタックで渡される可変長引数リストのパラメーターはアラインメントの対象となります。 たとえば、64 ビットのパラメーターは 64 ビット境界にアラインされます。 x86 と x64 では、スタックで渡される引数はアラインメントとパックの対象になりません。 この違いにより、printf などの可変個引数関数は、可変長引数リストの必要なレイアウトが正確に一致しない場合に、ARM でパディングとして意図されたメモリアドレスを読み取ることにあります。ただし、これは x86 または x64 アーキテクチャでは一部の値のサブセットでは動作する場合があります。 以下に例を示します。

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

この場合、引数のアラインメントが考慮されるように正しいフォーマット指定が使用されるようにすることで、バグを修正できます。 このコードは正しいものです。

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

引数の評価順序

ARM、x86、および x64 のプロセッサは非常に異なるため、コンパイラの実装には異なる要件があり、最適化についても異なる機能があります。 このため、呼び出し規則や最適化の設定などの他の要素と共に、コンパイラは、アーキテクチャが異なる場合、または他の要素が変更された場合に、関数の引数を異なる順序で評価する場合があります。 これにより、特定の評価順序に依存するアプリの動作が予期せず変更される可能性があります。

この種のエラーは、関数の引数に、同じ呼び出し内の関数の他の引数に影響を与える副作用がある場合に発生する可能性があります。 通常、この種の依存関係は簡単に回避できますが、識別しにくい依存関係や演算子のオーバーロードによって隠されることがあります。 次のコードの例を考えてみます。

handle memory_handle;

memory_handle->acquire(*p);

これは正しく定義されているように見えますが、->* がオーバーロードされた演算子の場合、このコードは次のようなものに変換されます。

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

また、operator->(memory_handle)operator*(p) の間に依存関係がある場合は、元のコードに考えられる依存関係はないように見えますが、コードは特定の評価順序に依存する可能性があります。

volatile キーワードの既定の動作

MSVC コンパイラでは、コンパイラ スイッチを使用して指定できる volatile ストレージ修飾子の 2 つの異なる解釈がサポートされています。 /volatile:ms スイッチは、厳密な順序付けを保証する Microsoft 拡張 volatile セマンティクスを選択します。これは、x86 と x64 の強力なメモリ モデルにより、これらのアーキテクチャの従来のケースでした。 /volatile:iso スイッチは、強力な順序付けを保証しない厳密な C++ 標準 volatile セマンティクスを選択します。

ARM アーキテクチャ (ARM64EC を除く) では、ARM プロセッサには弱い順序のメモリ モデルがあり、ARM ソフトウェアには /volatile:ms の拡張セマンティクスに依存するレガシがないため、既定値は /volatile:iso であり、通常はそうするソフトウェアとインターフェイスする必要がないためです。 ただし、拡張されたセマンティクスを使用するように ARM プログラムをコンパイルすることが便利、または必要な場合もあります。 たとえば、ISO C++ セマンティクスを使用するようにプログラムを移植するにはコストがかかりすぎる場合や、ドライバー ソフトウェアが正常に機能するため、従来のセマンティクスに従う必要がある場合があります。 このような場合は、/volatile:ms スイッチを使用できます。ただし、ARM ターゲットに対して従来の volatile セマンティクスを再作成するには、コンパイラによって volatile 変数のすべての読み取りまたは書き込みの周りにメモリ バリアが挿入され、厳密な順序付けが強制される必要があります。これにより、パフォーマンスに悪影響を及ぼす可能性があります。

x86、x64、および ARM64EC アーキテクチャでは、MSVC を使用してこれらのアーキテクチャ用に既に作成されているソフトウェアの多くがに依存しているため、既定値は /volatile:ms です。 x86、x64、およびARM64EC プログラムをコンパイルするときに、従来の揮発性セマンティクスへの不要な依存を回避し、移植性を高めるために、 /volatile:iso スイッチを指定できます。

関連項目

ARM プロセッサ用の Visual C ++ の構成する