+演算子はCでどのように実装されていますか?


79

どのように原始的な事業者などの理解場合は+-*および/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の補数または他の実装に依存する機能を利用していますか?


60
ほとんどの実装はネイティブaddマシン命令を使用すると思います。これは、すべてのCPUが備えており、数クロックで動作するハードウェア加算器として実装されていると思います。
MikeCAT 2016

3
はい、+オペレーターは実装で定義された機能を利用する可能性が非常に高いです。これらは「機械語」と「CPU」と呼ばれます。あなたの質問は何ですか?式がどのように機械語に変換されるかを知りたい場合は、コンパイラの構築について読んでください。
このサイトには正直すぎる

コンパイラ+などaddによってアセンブリディレクティブに変換されるため、投稿したコードがまったく役に立たないということですか?
nalzok 2016

11
ほとんどの+操作は 、マシンコード命令のいくつかのバリアント(または組み合わせ)にコンパイルされaddます。あなたのコードは回旋と、すべての実際のシナリオでは役に立たないが、それがされてできる二項演算について教えするのに役立ちます。
Anders Tornblad 2016

11
Cがそれを行う方法ではありませんが(以下の回答を参照)、関連する回路が非常に低いレベルで追加できる方法に非常に近いものです。小さなバイナリ値(たとえば、3ビットまたは4ビットバイト)について紙とペンで作業してみて、どのように機能するかを確認してください。ここで、回路が電気パルスで同じことを行う方法を想像してみてください。ここで、ループではなく、すべてのビットを並列に実行することを想像してください。これで、1940年代のコンピューターを構築する準備が整いました:D
Jon Hanna

回答:


184

衒学的であるために、C仕様は加算がどのように実装されるかを指定していません。

ただし、現実的に+は、CPUのワードサイズ以下の整数型の演算子はCPUの加算命令に直接変換され、より大きい整数型はオーバーフローを処理するためにいくつかの追加ビットを含む複数の加算命令に変換されます。

CPUは内部で論理回路を使用して加算を実装し、ループ、ビットシフト、またはCの動作によく似たものは使用しません。


12
この答えは、並外れた明快さと単純さで提示されているため、優れています。私はそれが過度に衒学的であるとはまったく思いません、単に質問のための適切な量の衒学者です。
ジェレミーアンダーソン

5
@orlp実際には、CPUロジック回路はHDLからコンパイルでき、OPの提案とほぼ同様のループとビットシフトを使用して加算器を生成する可能性があります(ただし、漠然としています)。上記のループとビットシフトは、ハードウェアのレイアウトとそれらの接続方法を説明します。繰り返しになりますが、最上位のハードウェアでは、誰かが前述のループとビットシフトを展開したり、HDLを廃止して、加算器と同じくらいパフォーマンスが重要なもののために回路を手動でレイアウトしたりする可能性があります。
Yakk-Adam Nevraumont 2016

5
線形加算回路はそのCコードとまったく同じように動作しますが、ループはハードウェアで完全に展開されます(32回)。
usr

2
@usrは展開されるだけでなく、すべての「ステップ」が同時に発生します。
OrangeDog 2016

4
@OrangeDogは、単純なハードウェア加算器で、このCコードと同じようにキャリーが波打つため、パラレリズムが制限されます。高性能加算器は、これを減らすためにキャリー先見回路を使用する場合があります。
プラグウォッシュ2016

77

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を実行すると、キャリーなしで合計を取得できます。そして、ビット単位で実行し、キャリービットを取得できる場合。

この観測をマルチビット数に拡張しab

a+b = sum_without_carry(a, b) + carry_bits(a, b) shifted by 1 bit left
    = a^b + ((a&b) << 1)

一度b0

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を使用した代替実装を示しています。


5
これはベストアンサーのimoであり、他のすべての例では、通常は1つの命令に変換されると述べていますが、これはそれを行い、特定の関数について説明しています。
Nick Sweeting 2016年

@NickSweetingありがとう。質問は2つの方法で解釈される可能性があり、受け入れられた回答はOPが尋ねたかったことを正しく解釈したと思います。
Mohit Jain

25

この関数は、+が実際にバックグラウンドでどのように機能するかを示しているようです

いいえ。これはadd、実際にハードウェア加算器を使用しているネイティブマシン命令に変換されますALU

コンピューターがどのように加算するのか疑問に思っている場合は、ここに基本的な加算器があります。

コンピュータ内のすべては、ほとんどがトランジスタでできている論理ゲートを使用して行われます。全加算器には半加算器が含まれています。

論理ゲート、および加算器の基本的なチュートリアルについては、これを。ビデオは長いですが、非常に役立ちます。

そのビデオでは、基本的な半加算器が示されています。簡単な説明が必要な場合は、次のとおりです。

与えられた半加算器加算の2ビット。可能な組み合わせは次のとおりです。

  • 0と0 = 0を追加します
  • 1と0 = 1を追加します
  • 1と1を加算= 10(バイナリ)

では、半加算器はどのように機能するのでしょうか。まあ、それは、3つの論理ゲートで構成されandxorそしてnandnand 両方の入力手段、これは解く0と0の場合は、その結果、陰性である場合に正の電流を与えるxor手段、それが問題を解決するようにすることを、入力の正の出力のいずれかが正で与え、他の負1と0。and両方の入力が正の場合にのみ正の出力が得られるため、1と1の問題が解決されます。基本的に、半加算器が得られました。しかし、それでもビットを追加することしかできません。

次に、全加算器を作成します。全加算器は、半加算器を何度も呼び出すことで構成されます。今、これはキャリーを持っています。1と1を加算すると、キャリー1が得られます。つまり、全加算器は、半加算器からキャリーを取得して格納し、別の引数として半加算器に渡します。

キャリーを渡す方法がわからない場合は、基本的に最初に半加算器を使用してビットを加算し、次に合計とキャリーを加算します。これで、2ビットのキャリーが追加されました。したがって、追加する必要のあるビットが終わるまで、これを何度も繰り返して、結果を取得します。

びっくり?これが実際に起こる方法です。長いプロセスのように見えますが、コンピューターはそれをナノ秒の何分の1か、より具体的には半クロックサイクルで実行します。単一のクロックサイクルでも実行される場合があります。基本的に、コンピュータにはALU(の大部分CPU)、メモリ、バスなどがあります。

論理ゲート、メモリ、ALUからコンピューターのハードウェアを学び、コンピューターをシミュレートしたい場合は、このコースを見ることができます。このコースから、私はすべてを学びました。第一原理から現代のコンピューターを構築する

電子証明書が必要ない場合は無料です。コースのパート2は今年の春に予定されています


11
数ミリ秒?単一の追加の場合?
JAB 2016

2
2つの登録値による加算は、通常、1つのクロックで完了します。
コーディグレイ

5
@Tamoghna Chowdhury:ナノ秒の何分の1かを試してください。レジスタ追加は、最近のIntelプロセッサではIIRC 1クロックであるため、クロック速度は数GHzです...パイプライン処理やスーパースカラー実行などはカウントされません。
jamesqf 2016

このリップルキャリー加算器はレイテンシーを追加しすぎるため、ハードウェアにこのように実装されていません。
パイプ

リップルキャリー加算器は、速度が遅すぎるため、CPUで何十年も使用されていません。代わりに、単一のクロックサイクル(または一部のIntelのダブルクロックALUの場合は半サイクル)でジョブを実行できる、より複雑な加算器を使用します。(まあ、ほとんどのCPUはそれを使用しません。ローエンドの組み込みCPUは、トランジスタ数
マーク

15

Cは、抽象マシンを使用して、Cコードの機能を記述します。したがって、それがどのように機能するかは指定されていません。たとえば、実際にCをスクリプト言語にコンパイルするC「コンパイラ」があります。

ただし、ほとんどのC実装で+は、マシンの整数サイズよりも小さい2つの整数の間で、(多くのステップの後)アセンブリ命令に変換されます。アセンブリ命令はマシンコードに変換され、実行可能ファイルに埋め込まれます。アセンブリは、マシンコードから「1ステップ削除」された言語であり、パックされたバイナリの束よりも読みやすくすることを目的としています。

そのマシンコード(多くのステップの後)は、ターゲットハードウェアプラットフォームによって解釈され、CPU上の命令デコーダーによって解釈されます。この命令デコーダは、命令を受け取り、それを信号に変換して「制御ライン」に沿って送信します。これらの信号は、レジスタとメモリからCPUを介してデータをルーティングします。CPUでは、値が算術論理演算ユニットで加算されることがよくあります。

算術論理演算装置は、別々の加算器と乗算器を持っている場合もあれば、それらを一緒に混合している場合もあります。

算術論理演算装置には、加算演算を実行して出力を生成するトランジスタが多数あります。前記出力は、命令デコーダから生成された信号を介してルーティングされ、メモリまたはレジスタに格納される。

算術論理演算装置と命令デコーダーの両方の前述のトランジスターのレイアウト(および私が光沢を付けた部分)は、プラントのチップにエッチングされています。エッチングパターンは、多くの場合、ハードウェア記述言語をコンパイルすることによって生成されます。ハードウェア記述言語は、何が何にどのように動作するかを抽象化し、トランジスタと相互接続ラインを生成します。

ハードウェア記述言語には、時間内に(次々に)発生することを記述するのではなく、空間で発生することを記述するシフトとループを含めることができます。これは、ハードウェアのさまざまな部分間の接続を記述します。上記のコードは、上記で投稿したコードと非常に漠然と似ている場合があります。

上記は多くの部分と層に光沢があり、不正確さが含まれています。これは、私自身の無能さ(ハードウェアとコンパイラの両方を書いたが、どちらも専門家ではない)と、完全な詳細には1、2年のキャリアが必要であり、SOの投稿ではないためです。

これは、8ビット加算器に関するSOの投稿です。 これはSO以外の投稿operator+で、HDLで使用している加算器の一部に注目してください。(HDL自体+が低レベルの加算器コードを理解して生成します)。


14

コンパイルされた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が合計を保持します。


14

あなたが見つけたコードは、非常に原始的なコンピュータハードウェアどのように「追加」命令を実装するかを説明しようとしています。この方法がどの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を書き留めます。これがtの計算です。すべての場所が検出されます。ここで、両方の2進数は1(つまり&)であり、次の桁の位置(<< 1)に移動します。次に、加算を行います。0+0 = 0、0 + 1 = 1、1 + 0 = 1、1 + 1は2ですが、0を書き留めます。これが排他または演算子が行うことです。

しかし、次の桁の位置で処理しなければならなかったすべての1は処理されていません。それらはまだ追加する必要があります。これが、コードがループを実行する理由です。次の反復では、余分な1がすべて追加されます。

なぜプロセッサがそのようにしないのですか?それはループであり、プロセッサはループを嫌い、遅いからです。最悪の場合、32回の反復が必要になるため低速です。数値0xffffffff(32 1ビット)に1を加算すると、最初の反復でyのビット0がクリアされ、xが2に設定されます。2回目の反復でビット1がクリアされます。 yの値を設定し、xを4に設定します。結果を得るには32回の反復が必要です。ただし、各反復でxとyのすべてのビットを処理する必要があるため、多くのハードウェアが必要になります。

プリミティブプロセッサは、10進演算と同じように、最低位置から最高位置まですばやく処理します。また、32ステップかかりますが、各ステップは2ビットと前のビット位置からの1つの値のみを処理するため、実装がはるかに簡単です。そして、原始的なコンピュータでさえ、ループを実装する必要なしにこれを行う余裕があります。

最新の高速で複雑なCPUは、「条件付き合計加算器」を使用します。特に、64ビット加算器などのビット数が多い場合は、時間を大幅に節約できます。

64ビット加算器は2つの部分で構成されています。1つは、下位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ビット加算器は条件付き合計加算器などです。


13

私の質問は:+演算子は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命令が使用されること。

演算子が単独で使用されることはほとんどないため、演算子がどのように実装されているかについて話すことはできません。


11

コードの内訳が他の誰かを助ける場合は、例を見て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;

次に、ビット単位のXORを 適用しますy^=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;

1
キャリービットを隠しておく理由についてはすでに十分な説明があったので、この回答を投稿して、コードがどのように機能しているを示します
user1717828 2016

11

興味深いことに、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の補数または他の実装に依存する機能を利用していますか?

コンパイラー作成者は、その特定のアーキテクチャーで可能な限り最も効率的な方法で、必要な効果を実装しようとする(ある番号を別の番号に追加する)と言っても過言ではありません。それが補数を引く必要があるなら、そうです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.