x86-64 ABI のポインタに 32 ビット オフセットを追加する場合、符号またはゼロ拡張は必要ですか? 質問する

x86-64 ABI のポインタに 32 ビット オフセットを追加する場合、符号またはゼロ拡張は必要ですか? 質問する

概要: 最適化のガイドとしてアセンブリ コードを調べていたところ、ポインターに int32 を追加するときに、多くの符号拡張またはゼロ拡張が見つかりました。

void Test(int *out, int offset)
{
    out[offset] = 1;
}
-------------------------------------
movslq  %esi, %rsi
movl    $1, (%rdi,%rsi,4)
ret

最初は、コンパイラが 32 ビット整数を 64 ビット整数に追加する際に問題が発生すると考えていましたが、Intel ICC 11、ICC 14、および GCC 5.3 でこの動作を確認しました。

これ私の調査結果を確認しましたが、符号拡張またはゼロ拡張が必要かどうかは明らかではありません。この符号/ゼロ拡張は、上位 32 ビットがまだ設定されていない場合にのみ必要です。しかし、x86-64 ABI はそれを要求するほどスマートではないでしょうか?

レジスタのスピルによってコードのキャッシュフットプリントが増加するため、すべてのポインタオフセットを ssize_t に変更するのは気が進みません。

ベストアンサー1

はい、引数または戻り値レジスタの上位 32 ビットにはゴミが含まれていると想定する必要があります。逆に、呼び出しまたは戻り値を返すときに、上位 32 ビットにゴミを残すことは許可されています。つまり、上位ビットを無視する負担は受信側にあり、上位ビットをクリーンアップする負担は渡す側にあるわけではありません。

64ビットの実効アドレスの値を使用するには、64ビットに符号またはゼロ拡張する必要があります。x32ABIgcc は、配列インデックスとして使用される潜在的に負の整数を変更するすべての命令に対して、64 ビットのオペランド サイズを使用する代わりに、32 ビットの実効アドレスを頻繁に使用します。


標準:

x86-64 システム V ABI_Boolレジスタのどの部分がゼロにされるか(つまり)についてのみ言及していますbool。20 ページ:

型の値_Boolがレジスタまたはスタックに返されるか渡される場合、ビット 0 には真理値が含まれ、ビット 1 から 7 はゼロになります (脚注 14: 他のビットは未指定のままであるため、それらの値のコンシューマー側は、8 ビットに切り捨てられたときに 0 または 1 になることを信頼できます)

%alまた、全体ではなく、可変引数関数の FP レジスタ引数の数を保持することについても%rax

そこにはgithub の問題を開くこの質問についてx32 および x86-64 ABI ドキュメントの github ページ

ABI は、引数や戻り値を保持する整数レジスタやベクトル レジスタの上位部分の内容にそれ以上の要件や保証を課していないため、要件や保証はありません。この事実は、Michael Matz (ABI のメンテナーの 1 人) からの電子メールで確認されています。「一般に、ABI で何かが指定されていないと、それを信頼することはできません。」

彼はまた、例えばclang >= 3.6 では、addps高位要素のゴミによって速度が低下したり、余分な FP 例外が発生する可能性がある がバグとして使用されています。(そういえば、報告すべきだった)。彼は、AMDのglibc数学関数の実装でこの問題が一度あったと付け加えた。通常のCコードできるdoubleスカラーまたは引数を渡すときに、ベクトル regs の上位要素にゴミを残しますfloat


標準では(まだ)文書化されていない実際の動作:

狭い関数の引数、たとえ_Bool/であってもbool、32ビットに符号またはゼロ拡張されます。clangは、この動作に依存するコードも作成します。(どうやら2007年から). ICC17それをしない、 それでICCとclangはABI互換ではないC の場合でも、最初の 6 つの整数引数のいずれかが 32 ビットより狭い場合は、x86-64 SysV ABI の ICC コンパイル コードから clang コンパイル関数を呼び出さないでください。

これは戻り値には適用されず、引数にのみ適用されます。gcc と clang はどちらも、受け取る戻り値にはその型の幅までの有効なデータのみが含まれると想定しています。たとえば、 gcc は を返す関数を作成しchar、 の上位 24 ビットにゴミを残します。%eax

ABIディスカッショングループの最近のスレッド8 ビットおよび 16 ビットの引数を 32 ビットに拡張するためのルールを明確にし、実際にこれを要求するように ABI を変更するという提案でした。主要なコンパイラ (ICC を除く) はすでにこれを行っていますが、呼び出し元と呼び出し先の間の契約が変更されることになります。

ここに例があります(他のコンパイラで確認するか、コードを微調整してください)Godboltコンパイラエクスプローラーここでは、パズルの 1 つのピースだけを示す単純な例を多数含めましたが、これは多くのことを示す例でもあります):

extern short fshort(short a);
extern unsigned fuint(unsigned int a);

extern unsigned short array_us[];
unsigned short lookupu(unsigned short a) {
  unsigned int a_int = a + 1234;
  a_int += fshort(a);                 // NOTE: not the same calls as the signed lookup
  return array_us[a + fuint(a_int)];
}

# clang-3.8 -O3  for x86-64.    arg in %rdi.  (Actually in %di, zero-extended to %edi by our caller)
lookupu(unsigned short):
    pushq   %rbx                      # save a call-preserved reg for out own use.  (Also aligns the stack for another call)
    movl    %edi, %ebx                # If we didn't assume our arg was already zero-extended, this would be a movzwl (aka movzx)
    movswl  %bx, %edi                 # sign-extend to call a function that takes signed short instead of unsigned short.
    callq   fshort(short)
    cwtl                              # Don't trust the upper bits of the return value.  (This is cdqe, Intel syntax.  eax = sign_extend(ax))
    leal    1234(%rbx,%rax), %edi     # this is the point where we'd get a wrong answer if our arg wasn't zero-extended.  gcc doesn't assume this, but clang does.
    callq   fuint(unsigned int)
    addl    %ebx, %eax                # zero-extends eax to 64bits
    movzwl  array_us(%rax,%rax), %eax # This zero-extension (instead of just writing ax) is *not* for correctness, just for performance: avoid partial-register slowdowns if the caller reads eax
    popq    %rbx
    retq

注:は同等ですが、小さくはありません。の戻り値の上位ビットがゼロに設定されることmovzwl array_us(,%rax,2)に依存できる場合、コンパイラはinsnを使用する代わりにを使用することもできます。%raxfuint()array_us(%rbx, %rax, 2)add


パフォーマンスへの影響

high32 を未定義のままにしておくのは意図的なものであり、これは良い設計上の決定だと思います。

32 ビット操作を実行する場合、上位 32 を無視するのは無料です。32ビット演算はその結果を64ビットにゼロ拡張する。mov edx, ediしたがって、64 ビット アドレッシング モードまたは 64 ビット操作で reg を直接使用できる場合にのみ、追加のものが必要になります。

一部の関数では、引数がすでに 64 ビットに拡張されているため、insn が節約されないため、呼び出し側が常にこれを行う必要があるのは無駄になる可能性があります。一部の関数では、引数の符号とは逆の拡張を必要とする方法で引数を使用するため、呼び出し側に何をするかを決定させるのが適切です。

ただし、符号の有無に関係なく 64 ビットにゼロ拡張することは、ほとんどの呼び出し元では無料であり、ABI 設計の適切な選択であった可能性があります。引数 regs はいずれにせよ上書きされるため、下位 32 のみを渡す呼び出し全体で完全な 64 ビット値を保持したい場合は、呼び出し元ですでに追加の操作を行う必要があります。したがって、通常は、呼び出し前に何かの 64 ビットの結果が必要で、切り捨てられたバージョンを関数に渡す場合にのみ、追加のコストがかかります。x86-64 SysV では、結果を RDI で生成して使用し、call fooEDI のみを参照することができます。

16ビットおよび8ビットのオペランドサイズは、誤った依存関係(AMD、P4、Silvermont、およびそれ以降のSnBファミリ)、部分的なレジスタストール(SnB以前)、または軽微な速度低下(Sandybridge)につながることが多いため、8ビットおよび16ビットの型を引数渡しのために32ビットに拡張する必要があるという文書化されていない動作には意味があります。GCC はなぜ部分レジスタを使用しないのですか?これらのマイクロアーキテクチャの詳細については、こちらをご覧ください。


static inline実際のコードでは、小さな関数はそうあるべきであり、引数処理のinsnは大きな関数の小さな部分であるため、これはおそらくコードサイズにとって大きな問題ではない。. コンパイラが両方の定義を認識できる場合、インライン化がなくても、プロシージャ間の最適化によって呼び出し間のオーバーヘッドを削除できます。(コンパイラが実際にこれをどの程度うまく実行できるかはわかりません。)

uintptr_t使用する関数シグネチャを変更すると、64 ビット ポインターの全体的なパフォーマンスが向上するか低下するかはわかりません。スカラーのスタック スペースについては心配する必要はありません。ほとんどの関数では、コンパイラーは、呼び出し保持レジスタ (や%rbxなど%rbp) をプッシュ/ポップして、独自の変数をレジスタ内に保持します。4B ではなく 8B のスピル用のわずかな余分なスペースは無視できます。

コード サイズに関しては、64 ビット値を扱うには、通常は必要のない一部の insn に REX プレフィックスが必要です。32 ビット値が配列インデックスとして使用される前に、その値に対して何らかの操作が必要な場合は、64 ビットへのゼロ拡張は無料で行われます。符号拡張は、必要な場合は常に追加の命令を必要とします。ただし、コンパイラは、命令を節約するために、最初から符号拡張して 64 ビットの符号付き値として扱うことができますが、その代償として、より多くの REX プレフィックスが必要になります。(符号付きオーバーフローは UB であり、ラップアラウンドするように定義されていないため、コンパイラは、 をint i使用するループ内で符号拡張をやり直すことを回避できることがよくありますarr[i]。)

現代のCPUは、通常、insnサイズよりもinsn数を重視します。ホットコードは、CPUのuopキャッシュから実行されることがよくあります。それでも、コードを小さくするとuopキャッシュの密度が向上します。コードサイズを節約でき、insnの数を増やしたり遅くしたりしなくてもよいのであれば、それはメリットですが、通常、他のものを犠牲にするほどの価値はありません。多くコードサイズの。

[reg + disp8]たとえば、 の代わりに、後続の 12 個の命令のアドレス指定を可能にするために、LEA 命令を 1 つ追加するなどですdisp32。または、xor eax,eax複数のmov [rdi+n], 0命令の前に、imm32=0 をレジスタ ソースに置き換えます。(特に、RIP 相対 + 即時では不可能なマイクロ融合が可能になる場合、重要なのは命令数ではなく、フロントエンドの uop 数であるためです。)

おすすめ記事