x86 アセンブリ命令がありますADC
。これは「キャリーで追加」を意味することがわかりました。これはどういう意味ですか/何をしますか? この命令の動作を C++ でどのように実装しますか?
情報:
Windows でコンパイルされています。32 ビットの Windows インストールを使用しています。私のプロセッサは Intel の Core 2 Duo です。
ADCはADDと同じですが、プロセッサのキャリーフラグが設定されている場合はさらに1が追加されます。
ただし、Intelプロセッサにはadcと呼ばれる特別な命令があります。このコマンドは、addコマンドと同様に動作します。唯一の余分なことは、値キャリーフラグも追加することです。したがって、これは大きな整数を追加するのに非常に便利です。16ビットレジスタを備えた32ビット整数を追加したいとします。どうすればそれができますか?さて、最初の整数がレジスタペアDX:AXに保持され、2番目の整数がBX:CXに保持されているとしましょう。こうやって:
add ax, cx adc dx, bx
ああ、最初に、下位16ビットがadd ax、cxによって追加されます。次に、addの代わりにadcを使用して上位16ビットが追加されます。これは、オーバーフローが発生した場合、キャリービットが上位16ビットに自動的に追加されるためです。したがって、面倒なチェックは必要ありません。この方法は64ビットなどに拡張できます...注:32ビット整数の加算が上位16ビットでもオーバーフローすると、結果は正しくなく、キャリーフラグが設定されます(例:50億の加算)。 50億に。
これ以降はすべて、実装で定義された動作のゾーンにかなり分類されることを忘れないでください。
これは、VS 2010(32ビット、WinXp)で機能する小さなサンプルです。
警告:$ 7.4 / 1-「asm宣言は条件付きでサポートされています。その意味は実装で定義されています。[注:通常、情報を実装からアセンブラに渡すために使用されます。—endnote]」
int main(){
bool carry = false;
int x = 0xffffffff + 0xffffffff;
__asm {
jc setcarry
setcarry:
mov carry, 1
}
}
ADCの動作は、CとC++の両方でシミュレートできます。次の例では、2つの数値を追加します(1つの符号なしに収まるには大きすぎるため、符号なしの配列として格納されます)。
unsigned first[10];
unsigned second[10];
unsigned result[11];
.... /* first and second get defined */
unsigned carry = 0;
for (i = 0; i < 10; i++) {
result[i] = first[i] + second[i] + carry;
carry = (first[i] > result[i]);
}
result[10] = carry;
お役に立てれば。
C++ 言語にはキャリー フラグの概念がないため、ADC
命令の周りに組み込み関数ラッパーを作成するのは面倒です。ただし、インテルはとにかくそれを行いました unsigned char _addcarry_u32 (unsigned char c_in, unsigned a, unsigned b, unsigned * out);
。最後に確認したところ、gcc はこれをうまく処理できませんでした (キャリーの結果を CF に残すのではなく、整数レジスタに保存します) が、Intel 独自のコンパイラがうまく機能することを願っています。
アセンブリのドキュメントについては、 x86タグ wikiも参照してください。
コンパイラは、1 つのレジスタよりも広い整数を追加するときに ADC を使用します。たとえばint64_t
、32 ビット コードまたは__int128_t
64 ビット コードでの追加です。
#include <stdint.h>
#ifdef __x86_64__
__int128_t add128(__int128_t a, __int128_t b) { return a+b; }
#endif
# clang 3.8 -O3 for x86-64, SystemV ABI.
# __int128_t args passed in 2 regs each, and returned in rdx:rax
add rdi, rdx
adc rsi, rcx
mov rax, rdi
mov rdx, rsi
ret
Godbolt コンパイラ Explorerからの asm 出力。clang-fverbose-asm
はそれほど冗長ではありませんが、gcc 5.3 / 6.1 は 2 つmov
の命令を浪費するため、読みにくくなっています。
イディオム/を使用adc
するキャリーアウトを使用して、コンパイラを手に持って を発行したり、実行したりすることができる場合があります。しかし、これを拡張して、代わりにan からキャリーアウトを取得することは、現在のコンパイラでは不可能です。安全に実行した場合、コンパイラは複数のチェックを最適化してそれぞれを実行することができません。add
uint64_t sum = a+b;
carry = sum < a;
adc
add
c+d+carry_in
+
c+d+carry
_ExtInt
add/adc/.../adc のチェーンを取得する方法が 1 つあります_ExtInt(width)
。16,777,215 ビットまでの任意のサイズの固定ビット幅型を提供する Clang の新機能です (ブログ投稿)。2020 年 4 月 21 日に clang の開発バージョンに追加されたため、リリースされたバージョンにはまだ含まれていません。
これは、ある時点で ISO C および/または C++ に表示されることを願っています。N2472提案は明らかに「ISO WG14 C 言語委員会によって積極的に検討されている」ようです。
typedef _ExtInt(256) wide_int;
wide_int add ( wide_int a, wide_int b) {
return a+b;
}
-O2
x86-64 用のclang トランク( Godbolt )で次のようにコンパイルします。
add(int _ExtInt<256>, int _ExtInt<256>):
add rsi, r9
adc rdx, qword ptr [rsp + 8]
adc rcx, qword ptr [rsp + 16]
mov rax, rdi # return the retval pointer
adc r8, qword ptr [rsp + 24] # chain of ADD / 3x ADC!
mov qword ptr [rdi + 8], rdx # store results to mem
mov qword ptr [rdi], rsi
mov qword ptr [rdi + 16], rcx
mov qword ptr [rdi + 24], r8
ret
明らか_ExtInt
に、呼び出し規約がレジスターを使い果たすまで、整数レジスターで値によって渡されます。(少なくともこの初期のバージョンでは、おそらく x86-64 SysV は、16 バイトより大きい構造体のように、2 つまたは 3 つのレジスタより広い場合、それを「メモリ」として分類する必要があります。便利です。他の引数を最初に置いて、それらが置き換えられないようにしてください。)
最初の _ExtInt 引数は R8:RCX:RDX:RSI にあり、2 番目の下位 qword は R9 にあり、残りはメモリにあります。
戻り値オブジェクトへのポインターは、RDI の非表示の最初の引数として渡されます。x86-64 System V は最大 2 つの整数レジスタ (RDX:RAX) を返すだけであり、これはそれを変更しません。
これにはバグがあります。この入力を試してください:
unsigned first[10] = {0x00000001};
unsigned second[10] = {0xffffffff, 0xffffffff};
結果は {0, 0, 1, ...} のはずですが、結果は {0, 0, 0, ...} です
この行を変更します:
carry = (first[i] > result[i]);
これに:
if (carry)
carry = (first[i] >= result[i]);
else
carry = (first[i] > result[i]);
それを修正します。