+
、-
、*
、および/
などのプリミティブ演算子がCでどのように実装されているかを理解すると、次のスニペットが見つかりました 興味深い答え =。
// replaces the + operator
int add(int x, int y) {
while(x) {
int t = (x & y) <<1;
y ^= x;
x = t;
}
return y;
}
この関数は、+
が実際にバックグラウンドでどのように機能するかを示しているようです。しかし、それを理解するのは私にとって混乱しすぎです。このような操作は、コンパイラによって長い間生成されたアセンブリディレクティブを使用して行われると信じていました。
私の質問は:+
演算子は[〜#〜] most [〜#〜]実装に投稿されたコードとして実装されていますか?これは、2の補数または他の実装依存の機能を利用していますか?そして、誰かがそれがどのように機能するかを説明できれば、とても感謝しています。
うーん...たぶん、この質問はSOについて少し話題から外れているかもしれませんが、これらの演算子に目を通すのはいくらか良いと思います。
独創的であるために、C仕様ではhowの追加を実装するように指定していません。
しかし現実的には、CPUのWordサイズ以下の整数型の+
演算子はCPUの加算命令に直接変換され、より大きな整数型はいくつかの余分な命令とともに複数の加算命令に変換されますオーバーフローを処理するビット。
CPUは内部的に論理回路を使用して加算を実装し、ループ、ビットシフト、またはCの動作によく似たものは使用しません。
2ビットを追加すると、結果は次のようになります。(真理値表)
a | b | sum (a^b) | carry bit (a&b) (goes to next)
--+---+-----------+--------------------------------
0 | 0 | 0 | 0
0 | 1 | 1 | 0
1 | 0 | 1 | 0
1 | 1 | 0 | 1
したがって、ビット単位のxorを実行すると、キャリーなしで合計を取得できます。また、ビット単位で実行すると、キャリービットを取得できます。
マルチビット数a
およびb
のこの観測の拡張
a+b = sum_without_carry(a, b) + carry_bits(a, b) shifted by 1 bit left
= a^b + ((a&b) << 1)
b
が0
:
a+0 = a
したがって、アルゴリズムは次のように要約されます。
Add(a, b)
if b == 0
return a;
else
carry_bits = a & b;
sum_bits = a ^ b;
return Add(sum_bits, carry_bits << 1);
再帰を取り除き、それをループに変換する場合
Add(a, b)
while(b != 0) {
carry_bits = a & b;
sum_bits = a ^ b;
a = sum_bits;
b = carrry_bits << 1; // In next loop, add carry bits to a
}
return a;
上記のアルゴリズムを念頭に置いて、コードからの説明はより簡単になります。
int t = (x & y) << 1;
キャリービット。両方のオペランドの右側の1ビットが1の場合、キャリービットは1です。
y ^= x; // x is used now
キャリーなしの加算(キャリービットは無視されます)
x = t;
Xを再利用して、キャリーに設定します
while(x)
キャリービットがさらにある間に繰り返す
再帰的な実装(理解しやすい)は次のとおりです。
int add(int x, int y) {
return (y == 0) ? x : add(x ^ y, (x&y) << 1);
}
この関数は、バックグラウンドで+が実際にどのように機能するかを示しているようです
いいえ。通常(ほとんどの場合)整数の加算は、機械語命令の加算に変換されます。これは、ビット単位のxorおよびandを使用した代替実装を示しています。
この関数は、実際にバックグラウンドで+がどのように機能するかを示しているようです
いいえ。これは、ネイティブのadd
マシン命令に変換されます。この命令は、ALU
で実際にハードウェア加算器を使用しています。
コンピュータがどのように追加するのか疑問に思っているなら、ここに基本的な加算器があります。
コンピューター内のすべては、ほとんどがトランジスタでできている論理ゲートを使用して行われます。全加算器には半加算器があります。
論理ゲートと加算器の基本的なチュートリアルについては、 this を参照してください。ビデオは非常に役立ちますが、長くなります。
そのビデオでは、基本的な半加算器が示されています。簡単な説明が必要な場合は、次のとおりです。
半加算器は、与えられた2ビットを加算します。可能な組み合わせは次のとおりです。
- 0と0 = 0を追加
- 1と0 = 1を追加
- 1と1を追加= 10(バイナリ)
それでは、半加算器はどのように機能しますか?まあ、それは3つの論理ゲート、and
、xor
、およびnand
で構成されています。両方の入力が負の場合、nand
は正の電流を提供します。したがって、これは0と0の場合を解決します。xor
は正の出力を提供し、入力の1つは正で、その他の負の値は、1と0の問題を解決することを意味します。and
は、両方の入力が正の場合にのみ正の出力を与えるため、1と1の問題を解決します。半加算器ができました。ただし、ビットを追加することはできます。
次に、全加算器を作成します。全加算器は、半加算器を何度も呼び出すことで構成されます。今、これはキャリーを持っています。 1と1を加算すると、キャリー1が得られます。したがって、全加算器は、半加算器からキャリーを取得して保存し、別の引数として半加算器に渡します。
キャリーを渡す方法がわからない場合は、基本的にまず半加算器を使用してビットを追加し、次に合計とキャリーを追加します。これで、2ビットのキャリーが追加されました。したがって、追加する必要があるビットが終わるまで、これを何度も繰り返して、結果を取得します。
びっくりした?これが実際に起こる方法です。長いプロセスのように見えますが、コンピューターはナノ秒の何分の一か、より具体的には半クロックサイクルでそれを行います。 1クロックサイクルでも実行される場合があります。基本的に、コンピューターにはALU
(CPU
の大部分)、メモリ、バスなどがあります。
ロジックゲート、メモリ、ALUからコンピューターハードウェアを学び、コンピューターをシミュレートする場合は、このコースを見ることができます。このコースからすべてを学びました。 原則
電子証明書が必要ない場合は無料です。コースのパート2は今年の春に予定されています
Cは抽象マシンを使用して、Cコードの機能を記述します。そのため、どのように機能するかは指定されていません。たとえば、実際にCをスクリプト言語にコンパイルするC「コンパイラ」があります。
しかし、ほとんどのC実装では、マシン整数サイズよりも小さい2つの整数間の+
は、アセンブリ命令に変換されます(多くの手順の後)。 Assembly命令はマシンコードに変換され、実行可能ファイルに埋め込まれます。アセンブリは、マシンコードから「1ステップ削除」された言語であり、多数のパックバイナリよりも読みやすくすることを目的としています。
そのマシンコードは(多くの手順を経て)ターゲットハードウェアプラットフォームによって解釈され、CPUの命令デコーダーによって解釈されます。この命令デコーダーは命令を受け取り、それを信号に変換して「制御ライン」に沿って送信します。これらの信号は、レジスタおよびメモリからCPUを介してデータをルーティングします。CPUでは、演算ロジックユニットで値が加算されます。
算術論理装置には、個別の加算器と乗算器がある場合と、それらを一緒に混合する場合があります。
算術論理演算ユニットには、加算演算を実行して出力を生成する多数のトランジスタがあります。前記出力は、命令デコーダから生成された信号を介してルーティングされ、メモリまたはレジスタに保存されます。
算術論理装置と命令デコーダーの両方のトランジスターのレイアウト(および、私が説明した部分)は、工場のチップにエッチングされています。エッチングパターンは多くの場合、ハードウェア記述言語をコンパイルすることによって生成されます。ハードウェア記述言語は、何に何が接続されているかを抽象化し、トランジスタと相互接続ラインを生成します。
ハードウェア記述言語には、起こることを記述しないシフトとループを含めることができますin time(次々に)でなく、むしろin space-それは異なる間の接続を記述しますハードウェアの部品。このコードは、上記で投稿したコードと非常にあいまいに見える場合があります。
上記は多くの部品とレイヤーで光沢があり、不正確です。これは、私自身の無能からです(ハードウェアとコンパイラの両方を書いたことがありますが、どちらの専門家でもありません)。また、詳細についてはSO投稿ではなく、.
ここ は、SO 8ビット加算器に関する投稿です。 ここ は、SO以外の投稿です。加算器はHDLでoperator+
を使用するだけです(HDL自体は+
を理解し、低レベルの加算器コードを生成します)。
見つけたコードは、非常に原始的なコンピューターハードウェアmightが「追加」命令を実装する方法を説明しようとします。 thisメソッドがany CPUによって使用されないことを保証できるため、「可能性あり」と言います。その理由を説明します。
通常、10進数を使用し、それらを追加する方法を学習しました。2つの数値を追加するには、下2桁を追加します。結果が10未満の場合、結果を書き留めて次の桁位置に進みます。結果が10以上の場合、結果から10を引いたものを書き留め、次の桁に進み、購入して1を追加することを忘れないでください。たとえば、23 + 37、3 + 7 = 10を追加し、0を書き留めて、次の位置に1を追加することを忘れないでください。 10の位置で、(2 + 3)+ 1 = 6を追加して書き留めます。結果は60です。
2進数でもまったく同じことができます。違いは、数字が0と1のみであるため、可能な合計は0、1、2のみであるということです。32ビットの数値の場合、1桁の位置を次々に処理します。そして、それは本当に原始的なコンピューターハードウェアがそれを行う方法です。
このコードの動作は異なります。両方の数字が1の場合、2つの2進数の合計は2であることがわかります。したがって、両方の数字が1の場合、次の2進数位置に1を追加して0を書き留めます。ここで、両方の2進数は1(つまり&)であり、次の桁位置(<< 1)に移動します。次に、追加を行います:0 + 0 = 0、0 + 1 = 1、1 + 0 = 1、1 + 1は2ですが、0を書き留めます。それがexcludiveまたはoperatorの動作です。
ただし、次の桁位置で処理する必要があった1はすべて処理されていません。まだ追加する必要があります。そのため、コードはループを実行します。次の反復では、余分な1がすべて追加されます。
プロセッサがそのようにしないのはなぜですか?それはループであり、プロセッサはループを好まないため、低速です。最悪の場合、32回の反復が必要であるため、時間がかかります。数値0xffffffff(32個の1ビット)に1を追加すると、最初の反復でyのビット0がクリアされ、xが2に設定されます。 yを設定し、xを4に設定します。結果を得るには32回の反復が必要です。ただし、各反復ではxとyのすべてのビットを処理する必要があり、これには多くのハードウェアが必要です。
プリミティブプロセッサは、最低位から最高位まで、10進算術の方法と同じくらい迅速に処理を行います。また、32ステップもかかりますが、各ステップは2ビットと前のビット位置からの1つの値のみを処理するため、実装がはるかに簡単です。そして、原始的なコンピューターであっても、ループを実装することなくこれを行う余裕があります。
最新の高速で複雑なCPUは、「条件付き加算器」を使用します。特に、ビット数が多い場合(64ビット加算器など)、多くの時間を節約できます。
64ビット加算器は2つの部分で構成されます。最初に、最下位32ビット用の32ビット加算器。その32ビット加算器は、合計と「キャリー」(次のビット位置に1を追加する必要があることを示すインジケーター)を生成します。次に、上位32ビット用の2つの32ビット加算器:1つはx + yを加算し、もう1つはx + y + 1を加算します。3つの加算器はすべて並行して動作します。その後、最初の加算器がキャリーを生成すると、CPUは2つの結果x + yまたはx + y + 1のどちらが正しいものであるかを選択するだけで、完全な結果が得られます。したがって、64ビット加算器は32ビット加算器よりもわずかに長く、2倍長くはかかりません。
32ビット加算器部分は、複数の16ビット加算器を使用して条件付き加算器として実装され、16ビット加算器は条件付き加算器などです。
コンパイルされたCコードを実行できるほとんどすべての最新のプロセッサには、整数加算のサポートが組み込まれています。投稿したコードは、整数加算オペコードを実行せずに整数加算を実行する賢い方法ですが、通常は整数加算を実行する方法ではありません。実際、関数リンケージはおそらく何らかの形の整数加算を使用してスタックポインターを調整します。
投稿したコードは、xとyを追加するときに、共通のビットとxまたはyのいずれかに固有のビットに分解できるという観察に依存しています。
表現 x & y
(ビット単位のAND)は、xとyに共通のビットを提供します。表現 x ^ y
(ビットごとの排他的論理和)は、xまたはyのいずれかに一意のビットを提供します。
合計 x + y
は、共通の2倍のビット(xとyの両方がこれらのビットに寄与するため)とxまたはyに固有のビットの合計として書き換えることができます。
(x & y) << 1
は共通のビットの2倍です(1の左シフトは2を実質的に乗算します)。
x ^ y
は、xまたはyのいずれかに固有のビットです。
したがって、xを最初の値で置き換え、yを2番目の値で置き換えた場合、合計は変更されません。最初の値はビット単位の加算のキャリー、2番目はビット単位の加算の下位ビットと考えることができます。
このプロセスは、xがゼロになるまで続き、その時点でyが合計を保持します。
私の質問は次のとおりです。+演算子は、MOST実装に投稿されたコードとして実装されていますか?
実際の質問に答えましょう。すべての演算子は、一部の変換後に最終的にコードに変換される内部データ構造としてコンパイラーによって実装されます。個々のステートメントに対してコードを生成する現実のコンパイラーはほとんどないため、1回の追加でどのコードが生成されるかを言うことはできません。
コンパイラは、動作する限りコードを自由に生成できますあたかも実際の操作は標準に従って実行されました。しかし、実際に起こることは完全に異なるものになる可能性があります。
簡単な例:
static int
foo(int a, int b)
{
return a + b;
}
[...]
int a = foo(1, 17);
int b = foo(x, x);
some_other_function(a, b);
ここで追加の指示を生成する必要はありません。コンパイラがこれを次のように翻訳することは完全に合法です:
some_other_function(18, x * 2);
または、コンパイラは、関数foo
を連続して数回呼び出すことと、それが単純な算術演算であり、そのためのベクトル命令を生成することに気付くかもしれません。または、加算の結果が後で配列のインデックス付けに使用され、lea
命令が使用されます。
オペレーターが単独で使用されることはほとんどないため、オペレーターの実装方法について話すことはできません。
コードの内訳が他の人に役立つ場合は、例をご覧くださいx=2, y=6
:
x
はゼロではないため、y
への追加を開始します。
while(2) {
x & y = 2
なぜなら
x: 0 0 1 0 //2
y: 0 1 1 0 //6
x&y: 0 0 1 0 //2
2 <<1 = 4
なぜなら<< 1
は、すべてのビットを左にシフトします。
x&y: 0 0 1 0 //2
(x&y) <<1: 0 1 0 0 //4
要約すると、その結果を隠して、4
、t
で
int t = (x & y) <<1;
ここで bitwise XORy^=x
:
x: 0 0 1 0 //2
y: 0 1 1 0 //6
y^=x: 0 1 0 0 //4
そう x=2, y=4
。最後に、合計t+y
リセットしてx=t
そしてwhile
ループの先頭に戻ります:
x = t;
いつ t=0
(または、ループの開始時に、x=0
)、終了
return y;
興味深いことに、Atmega328Pプロセッサでは、avr-g ++コンパイラを使用して、次のコードは-1を減算して1を加算することを実装しています。
volatile char x;
int main ()
{
x = x + 1;
}
生成されたコード:
00000090 <main>:
volatile char x;
int main ()
{
x = x + 1;
90: 80 91 00 01 lds r24, 0x0100
94: 8f 5f subi r24, 0xFF ; 255
96: 80 93 00 01 sts 0x0100, r24
}
9a: 80 e0 ldi r24, 0x00 ; 0
9c: 90 e0 ldi r25, 0x00 ; 0
9e: 08 95 ret
特に、追加はsubi
命令(レジスタから定数を減算)によって行われることに注意してください。この場合、0xFFは実質的に-1です。
また興味深いのは、この特定のプロセッサにaddi
命令がないことです。これは、設計者が補数の減算を行うことがコンパイラライターによって適切に処理されると考えたことを意味します。
これは、2の補数または他の実装依存の機能を利用していますか?
コンパイラー作成者は、その特定のアーキテクチャーで可能な限り最も効率的な方法で、必要な効果(1つの数値を別の数値に加算)を実装しようとすると言うのはおそらく公平でしょう。それが補数の減算を必要とするのであれば、そうです。