コンピュータは0と1で動いている
大学院生の時に初めて手に取ったPythonの入門書、新入社員として受講したJava研修、入社後に勉強を始めた基本情報技術者試験。これらプログラミング関連の書籍や講義の冒頭で、必ずと言っていいほど繰り返されている決まり文句(クリシェ)が、「コンピュータは0と1の世界でできている」というものだ。
最初のうちは「へぇ~それはすごい。考えた人はなかなかに頭がいいな」程度の感想を抱くくらいで、さっさと本題のページへと指を進めていた。しかし、あまりにも繰り返し出てくるので、そのうち違う感想が自分の中に芽生え始めた。「いや、本当にそうなっているのか?」。
本記事は「コンピュータが本当に0と1の世界で成り立っているのか?」「成り立っているとしたらどのようなからくりなのか?」という疑問に対し、現在の自分の理解をまとめたものである。「0と1の計算(2進数の演算)だけで、いかにして汎用コンピュータという広範な機能を実現するのか」。この疑問に、ある程度の「腹落ち感」を与えてくれたのが、Nand2Tetrisの愛称で知られる名著『コンピュータシステムの理論と実装』(原題:The Elements of Computing Systems)である。
汎用コンピュータの多機能性はCPUによって達成されている。よって、CPUを0と1の計算で構築できることを示せれば、私の疑問は解消されるはずだ。したがって本記事では、Nand2Tetrisの第1章から第5章までの内容をベースとして、いかにして基本論理ゲートからCPUが作られるか、その要点をまとめる。なお、CPUやその構成要素(ALU、RAM、論理ゲート)が実際にどのようにハードウェアとして物理実装されるかについては立ち入らない(興味がある人はぜひ本書を読んで、実装の課題に着手してほしい)。
1. 物理現象を「論理」に変える:論理ゲート
まずは論理ゲートから見ていこう。2進数の基本的な論理演算(「AかつB」や「AまたはB」など)は、論理ゲートとして物理的に実装される。
そもそも論理ゲートとは、抽象的な概念である「ブール関数」を物理世界に再現した装置のことだ。例えばAndゲートは「入力がすべて1のとき1を返す」という関数を、Orゲートは「入力の少なくとも1つが1であれば1を返す」という関数を再現している。
このようなブール関数を物理的に実装するためには、次の2つの性質が必要となる。
- 物理量から2つの区別可能な状態を作り出す性質(スイッチング): これはブール関数における「0と1」を明確に定義するために必要となる。
- 入力の状態に応じて出力を決定する性質: これは関数としての論理的な振る舞いを再現するために必要となる。
逆に言えば、これらの性質さえ満たしていれば、物理的な媒体は何でも構わない。磁石(N極/S極)、光(有/無)、生体分子(特定の化学物質の有/無)、水圧・空気圧(流れの有/無)、さらには量子(スピンの上/下)なども利用可能である。ただ、現代のほとんどのコンピュータでは、実装の容易さから電圧(電流の有/無)を採用しているに過ぎない。
2. 最強の部品「Nand」ですべてを作る
このような論理ゲートは、互いに組み合わせることで、より複雑なブール関数を再現する「複合ゲート」を作ることができる。
たとえば、XOR(排他的論理和)というゲートは、And、Or、Notといった基本ゲートを組み合わせることで構築可能だ。そして驚くべきことに、これらすべての論理ゲートは、Nand(Not-And)という単一の論理ゲートのみを組み合わせて作ることができる。
したがって、Nandゲートさえ物理的に実装できてしまえば、原理的にはあらゆるブール関数を物理的に実装できることになるのである。
3. 「計算」の正体:足し算はどう実現されるか
ここまでで、電気という物理現象を利用してブール関数を実装できること、そしてNandゲートさえあればすべての論理演算が可能になることを見てきた。これにより、電圧のON/OFFを用いた論理演算の世界は完成したと言える。
しかし、これだけでは汎用コンピュータへの道はまだ程遠い。なにせ現状では、「1 + 1」といった基礎的な算術演算すらできないのだから。
算術演算は加算器(Adder)というチップによって実現される。加算器とは、2つのnビット入力を受け取り、それらの和を出力する論理ゲートであり、これまで見てきた論理ゲートの組み合わせによって構築することができる。この加算器の登場によって、ついに2進数の「加算」が可能になった。
次に2進数の減算についてだが、これに専用のチップ(減算器)は不要だ。2の補数という概念を利用することで、加算器をそのまま流用できるからである。詳細は割愛するが、2の補数表現を使えば、-5のような負の整数をバイナリコードで表現できるようになる。これにより、2 - 1という減算を、2 + (-1)という加算として処理できるのだ。
さらに言えば、乗算も「足し算の繰り返し」として表現できるため、原理的には加算器さえあれば実現可能である(実際の高速なCPUでは専用の乗算回路を持つことも多いが、理論的な最小構成としては必須ではない)。
つまり、デジタルコンピュータが実行するあらゆる複雑な数値計算は、元を正せば2進数の加算に還元することができると言えるのだ。
4. 万能計算機への第一歩:ALU(算術論理演算器)
さて、ここまでで0と1による論理演算と、基本的な算術演算が可能となった。
しかし現時点では、これらは個別の複合ゲートとして実装されているに過ぎない。特定の演算をしたい場合は、それぞれのゲートに個別にデータを流す必要がある。つまり、加算をしたいときは加算器用の入力(x_add, y_add)に、And演算をしたいときはAndゲート用の入力(x_and, y_and)に情報を入力する…といった具合に、演算機能の数だけ入力配線を用意しなければならないのだ。
このような「配線まみれ」の状況は、「入力データは全演算で共通化し、『どの演算を実施するか』を別のバイナリコードで指示する」という設計のチップを構築することで解決できる。そのために生まれたチップこそが、算術論理演算器(ALU)である。
今回扱うシンプルなALUの仕組みは以下のとおりだ。 ALUは x と y という2つの16ビット整数を入力として受け取る。それに加えて、zx, nx, zy, ny, f, no という6つの1ビット入力(制御ビット)を受け取る。
このALUは、最小限の機能を持つ基本的なモデルだが、2つの入力 x, y に対して以下の18通りの関数を切り替えて実行できる。
| xとyを使う | xを使う | yを使う | xもyも使わない | |
|---|---|---|---|---|
| 算術演算 | x+y, x-y, y-x | x, -x, x+1, x-1 | y, -y, y+1, y-1 | 0, 1, -1 |
| 論理演算 | x&y, x|y | !x | !y |
ここで面白いのは、「加算ボタン」や「ANDボタン」のような、特定の演算に1対1で対応するビットが存在するわけではない点だ。これら6つの制御ビットのON/OFFの組み合わせ(ビットパターン)によって、どの計算を行うかが決定される仕組みになっている。
例えば、ある特定のビットパターンを制御入力として与えるとALUは x + y を実行し、また別の組み合わせを与えると、同じ入力 x, y に対して x & y を実行する。 このように、入力データとは別に「何をせよ」という命令コード(OpCode)のようなビット列を与えることで、単一のチップでありながらマルチな計算機能を実現しているのである。
5. コンピュータに「時間」を与える:クロックと同期
これまで紹介してきたチップは、「入力に対して即座に演算結果を出力する」という理想的な前提で話を進めてきた。例えば、ALUに「5」と「2」と「加算命令」を入力すれば、瞬時に「7」が出力されるかのように扱ってきた。
しかし現実には、主に2つの理由により出力は遅延する。 ひとつは、電気信号がチップの入力ピンまで到達するのにかかる物理的な移動時間。もうひとつは、チップ内部でゲートがスイッチングを行い、計算を完了させるまでにかかる処理時間だ。 そのため現実の世界では、ALUの出力が正しい「7」に安定するまでには僅かながら時間がかかるし、安定するまでの過渡期には、計算途中の意味のない信号(ゴミ)が出力されてしまうこともある。
この問題を解決するために、コンピュータアーキテクチャでは時間の進行を連続的ではなく、サイクル(Cycle)と呼ばれる一定の長さの区間に分割して管理している。
このサイクルの基準となるのが、クロック(Clock)である。クロックは、Tick(0の状態)とTock(1の状態)という2つの状態を一定間隔で行き来する信号(クロック信号)を生成し続ける。このTickとTockを合わせた1回分の周期が「1サイクル」となる。
これにより、時間は「サイクル1」「サイクル2」…と離散的に扱われることになる。「データの保存」や「次の計算の開始」といった重要な変化は、サイクルの変わり目(エッジ)でのみ行い、サイクルの中途半端な時間は「待機時間」とみなすのだ。
先ほどのALUの例で言えば、入力信号が届いて計算が完了し、出力が「7」に安定するまでの時間が「1サイクル」の中に収まってさえいればいい。そうすれば、次のサイクルの始まりで確実に正しい「7」という結果だけを拾うことができ、計算途中のゴミデータを無視できるからだ。
こうして、現実には連続的で不安定な物理世界を、サイクル単位の整然とした「離散的な時間」に変換することが、安定したコンピュータを作るための鍵であることがわかった。では、この仕組みは具体的にどのハードウェアで実現されるのだろうか?
6. 「記憶」の仕組み:フリップフロップとメモリ
クロック信号によってコンピュータ内の時間が離散化される仕組みは前述のとおりだ。では、これは実際にどのような回路として実装されるのだろうか?
これまで見てきたチップ(NandやALUなど)はすべて、入力信号を与えると(遅延はあれど)即座に出力が決まるものであった。このようなチップは組み合わせ回路(Combinational Chip)と呼ばれる。
一方、「記憶」を実現するためには、入力値の組み合わせのみに反応するのではなく、「過去の入力」を保持しておく必要がある。このように、現在の入力だけでなく、以前(過去)の状態にも依存して出力が決まるのが順序回路(Sequential Chip)である。
この順序回路の最小単位となるのが、Dフリップフロップ(Data Flip-Flop; DFF)だ。 DFFの機能はシンプルで、「1つ前のサイクルで受け取った入力を、今のサイクルで出力する」というものだ(out[t] = in[t-1])。この「1サイクル分だけ値を保持する」という性質が、記憶の根本となる。 レジスタやRAMといったメモリデバイスは、このDFFを大量に並べることで構築されている。
コンピュータ内のDFFやメモリデバイスは、すべてマスタークロックに接続されている。マスタークロックはいわばオーケストラの指揮者のような存在だ。システム全体に『せーの!』と合図(クロック信号)を送ることで、何億個ものメモリ素子を一斉に更新(同期)させる。 これにより、コンピュータ全体で「今の状態」と「次の状態」が明確に区別され、秩序立った動作が可能になるのである。
7. 脳を作る:CPUの構成
汎用コンピュータが万能な道具たり得るのは、CPUという頭脳を持っているからだ。このCPUを構成する中心的なパーツが、計算を担うALUと、記憶を担うレジスタである。
役割は単純だ。 ALUは、制御ビットの指示に従って、「足し算」や「論理積」などの演算をひたすら実行する。 レジスタは、ALUが計算した結果を一時的に保存したり、あるいは「次にどんな演算をするか」という命令自体を保持したりするために使われる。
つまり、「計算する機能(ALU)」と「値を覚える機能(レジスタ)」を組み合わせることで、決められた手順(アルゴリズム)に従ってデータを次々と処理し続ける装置、すなわちCPUを作ることが可能になるのだ。
8. 命を吹き込む:プログラムカウンタと実行サイクル
ALUとレジスタがあれば、計算と記憶はできる。しかし、これだけではまだ「人間が毎回スイッチを切り替えて命令を与える計算機」に過ぎない。 汎用コンピュータが画期的なのは、メモリに書き込まれたプログラム(命令のリスト)を読み込み、自動的に次々と処理を実行し続ける点にある。
この自律的な動作を実現するための最後の重要なパーツが、プログラムカウンタ(PC)である。 PCは、「次に実行すべき命令がメモリの何番地にあるか」というアドレス(住所)を保持し続ける特殊なレジスタだ。
コンピュータが動作している間、ハードウェアは以下のサイクルを延々と、猛烈な速さで繰り返している。
- フェッチ(Fetch): PCが指し示すメモリのアドレスから、命令(0と1のビット列)を読み込む。
- デコード(Decode): 読み込んだ命令を解釈する。「足し算」なのか、「メモリへの保存」なのか、あるいは「次の命令へのジャンプ」なのかを判断し、ALUやレジスタに制御信号を送る。
- エグゼキュート(Execute): 実際にALUが計算を行ったり、レジスタの値が書き換わったりする。
- PCの更新: 次の命令に進むため、PCの値を更新する。基本的には「+1」して次の行に進むが、もし「ジャンプ命令(if文やループ)」であれば、指定されたアドレスにPCの値を書き換える。
このサイクルこそが、コンピュータの心臓の鼓動である。 Nand2Tetrisでは、ALU、レジスタ、そしてこのPCを組み合わせることで、最終的に「Hack」と呼ばれるシンプルながらも完全なコンピュータアーキテクチャを完成させる。
おわりに:ブラックボックスの中身はシンプルだった
冒頭の疑問に戻ろう。「コンピュータは本当に0と1の世界で成り立っているのか?」
答えは「Yes」だ。しかし、より正確に表現するならば、「0と1という単純な物理状態を、論理の力で積み重ねることで、無限の機能を作り出している」と言えるだろう。
- 物理層では、電圧のON/OFFが「0と1」を作る。
- 論理層では、Nandゲートがその0と1を操作して「計算」の最小単位を作る。
- アーキテクチャ層では、ALUやレジスタが連携して「命令」を実行する。
私たちが普段触れているPythonやJavaのコードは、この積み重ねの遥か上層に位置している。しかし、その足元を掘り下げていけば、そこにあるのは決して魔法ではない。先人たちが築き上げた、堅牢で論理的な構造物なのだ。
もちろん、「中身がどうなっているか」を知らなくても、プログラムは書けるしシステムは作れる。 けれど、この低レイヤの世界を知った今、エラーログの向こう側や処理遅延の裏側で、あのALUやPCが必死に0と1を操作している姿が、ありありと想像できるようになった。その感覚は、エンジニアとしての自分を、以前より少しだけ強くしてくれたように思う。
もし、この記事を読んで「自分もCPUを作ってみたい」と少しでも思ったなら、ぜひ『コンピュータシステムの理論と実装』を手に取ってみてほしい。そこには、0と1から世界を構築する確かな手触りが待っているはずだ。