🎯

「Nand to Tetris」でメモリの仕組みを理解する - 1ビットの記憶から16K個のレジスタまで作ってみた

に公開

はじめに:なぜ今「Nand to Tetris」なのか

エンジニアとして働いていると、日々扱っているメモリやCPUといったコンピュータの構成要素について「実際どのような仕組みで動いているのだろうか?」という疑問を抱くことがあります。特に、メモリが不足してサービスが落ちたり、CPU使用率が急上昇したりするトラブルに遭遇すると、その根本的な動作原理を理解したいと感じていました。

そんな中、「Nand to Tetris」という学習プロジェクトに出会いました。このプロジェクトは、たった一つのNand論理ゲートから始めて、最終的にはテトリスが動作する完全なコンピュータシステムを構築するという、かなりやりがいのある学習体験です。

AI時代だからこそ、コンピュータの基礎原理をしっかり理解することの価値は高まっています。この記事では、特にメモリシステムの実装を通じて、普段当たり前に使っているコンピュータの仕組みがどれほど考え抜かれて作られているかを解説していきます。

「Nand to Tetris」とは何か

「Nand to Tetris」は、コンピュータシステムの理論と実装を学ぶための体系的な学習プログラムです。オライリー・ジャパンから『コンピュータシステムの理論と実装』として日本語版書籍も出版されており、世界中の大学でコンピュータサイエンスの教材として使用されています。

プロジェクトの構成

このプロジェクトの特徴は、その段階的で体系的なアプローチにあります。

このプロジェクトで気に入っているのは、各章が適切にモジュール化されていることです。学習者は前の章の実装詳細を完全に理解していなくても、その章で作るモジュールの「What(何ができるか)」を理解すれば次の章に進めるように設計されています。

この設計思想は、「抽象化と実装」というシステムエンジニアリングの重要な概念を体感的に学べるようになっています。

それでは、このプロジェクトの中でも特に興味深いメモリシステムの実装について詳しく見ていきましょう。

メモリの根本原理:なぜ時間という概念が必要なのか

プログラミングでは x = y + 17 のように変数に値を代入することが当たり前ですが、この「値を保持する」という行為は、実はコンピュータにとって非常に複雑な処理です。

組み合わせ回路と順序回路

コンピュータの回路には大きく分けて2つの種類があります。

組み合わせ回路は、NandゲートやAndゲート、Orゲートのように、入力信号を与えると即座に出力が得られる回路です。これらは「現在の入力」のみで出力が決まります。

一方で、データを記憶するためには順序回路が必要です。順序回路とは、現在の入力だけでなく「過去に処理した入出力」にも依存する回路のことです。

では、この時間をコンピュータはどのように表現するのでしょうか。

クロックサイクルによる時間のモデル化

コンピュータにおける時間は、「クロック」というバイナリ信号によってモデル化されます。このクロックは「tick」と「tock」を繰り返し、この一連の流れを「サイクル」と呼びます。

クロックサイクルを導入することで、2つの大きなメリットがあります。

例えば、Notゲートに1を入力しても、出力が0になるまでには微小な時間がかかります。しかし、1サイクルの時間をその遅延より意図的に長く設定することで、サイクル終了時には確実に正しい値が出力されています。

クロックサイクルとゲート動作のタイミング図
クロックサイクルにより、ゲートの処理遅延を吸収し、システム全体を同期させる仕組み

このように、時間の概念を導入することでメモリシステムが実現可能になります。

では、この時間の仕組みを使って、実際にどのようにしてデータを記憶するのでしょうか。まずは最も基本的な記憶装置から見ていきましょう。

フリップフロップ:記憶の最小単位

メモリシステムの基礎となるのがDフリップフロップ(DFF)回路です。これは1ビットの情報を保持する最小の記憶装置として機能します。

Dフリップフロップの仕組み

DFFの動作は非常にシンプルです。

入力: in    // 現在の入力値
出力: out   // 1クロック前の入力値

動作原理: out(t) = in(t-1)

この「前のサイクルの入力を今のサイクルで出力する」という動作により、情報の保持が実現されます。

実際の動作例を見てみましょう。

クロック時刻 入力(in) 出力(out)
T0 0 未定義
T1 1 0
T2 0 1
T3 1 0

出力が「過去の入力値」となっていることがわかります。これこそが「記憶」の本質です。

正直、最初にこの表を見たときは「たったこれだけ?」と思いました。でも、この単純な仕組みが私たちの使っているメモリの基礎になっていると知って、改めてコンピュータの奥深さを感じました。

DFFだけでは不十分な理由

しかし、DFFには重要な問題があります。常に新しい値で上書きしてしまうため、「いつ記憶するか」を制御できません。

実用的なメモリシステムでは「この値をずっと保持しておいて」という指示が必要です。

この問題を解決するために考え出されたのがBitチップです。DFFに制御機能を追加することで、より実用的なメモリを作ることができます。

Bitチップ:制御可能な1ビットメモリ

Bitチップは、DFFに「いつ記憶するか」の制御機能を追加したものです。

制御機能付きメモリの設計

入力: in, load
出力: out

動作原理:
if (load(t-1) == 1) then out(t) = in(t-1)  // 新しい値を記憶
else out(t) = out(t-1)                      // 前の値を保持

この仕組みにより、load=1のときだけ新しい値を記憶し、load=0のときは以前の値を保持し続けます。

実装の仕組み:フィードバックループ

Bitチップの実装には、DFFとMuxチップを組み合わせたフィードバック構造を使用します。

CHIP Bit {
    IN in, load;
    OUT out;

    PARTS:
    Mux(a=dffout, b=in, sel=load, out=muxOut);
    DFF(in=muxOut, out=dffout, out=out);
}

面白いのは、DFFの出力を再び自分の入力に戻すフィードバックループです。

  • load=1のとき:Muxが新しい入力(in)を選択 → DFFが新しい値を記憶
  • load=0のとき:Muxが前の値(dffout)を選択 → 同じ値がループして保持される

Bitチップのフィードバックループ構造
DFFとMuxを組み合わせたフィードバックループ。load信号により「記憶」と「保持」を切り替える

このフィードバック機構により、デジタル回路で「永続的な記憶」が実現されています。初めてこの仕組みを理解したとき、シンプルな回路の組み合わせで記憶という複雑な機能が実現できることに感動を覚えました。

さて、1ビットの記憶ができるようになりましたが、実際のコンピュータではもっと大きなデータを扱う必要があります。次は複数ビットをまとめて扱う方法を考えてみましょう。

Registerチップ:実用的なデータ単位への拡張

コンピュータで扱うデータは通常、1ビットではなく複数ビットです。数値、アドレス、命令などは16ビットや32ビットで構成されています。そこで、Bitチップを16個組み合わせたRegisterチップが必要になります。

16ビット並列処理の設計

Registerチップは、16個のBitチップを並列に配置し、1本のload信号で一括制御します。

CHIP Register {
    IN in[16], load;
    OUT out[16];

    PARTS:
    Bit(in=in[0], load=load, out=out[0]);
    Bit(in=in[1], load=load, out=out[1]);
    // ... 16個のBitチップを並列配置
    Bit(in=in[15], load=load, out=out[15]);
}

この設計により、16ビットのデータを1つの単位として効率的に管理できます。すべてのビットが同期して動作するため、データの整合性も保たれます。

これで16ビットのデータを1つの単位として扱えるようになりました。

しかし、まだ問題があります。コンピュータには多数のデータを保存する必要がありますが、Registerは1つの値しか保存できません。複数のデータを扱うには「どこに保存するか」を指定する仕組みが必要になります。これがアドレッシングという技術です。

RAM8チップ:アドレッシングの基礎実装

実用的なメモリシステムでは、複数の記憶領域を持ち、「どこに書き込むか」「どこから読み出すか」を指定できる必要があります。これを実現するのがRAM(Random Access Memory)です。

アドレッシングの基本概念

RAM8チップは8個のRegisterを持ち、3ビットのアドレスで場所を指定します。

入力: in[16], load, address[3]
出力: out[16]

アドレス指定:
address=000 → Register0
address=001 → Register1
...
address=111 → Register7

3ビットで2³=8通りの選択が可能なため、8個のRegisterを識別できます。

回路実装の仕組み

RAM8チップの実装には3つの主要コンポーネントが必要です。

実装コードで確認してみましょう。

CHIP RAM8 {
    IN in[16], load, address[3];
    OUT out[16];

    PARTS:
    // load信号を8本に分配
    DMux8Way(in=load, sel=address, 
             a=load0, b=load1, c=load2, d=load3,
             e=load4, f=load5, g=load6, h=load7);

    // 8個のRegisterに分配されたload信号を接続
    Register(in=in, load=load0, out=out0);
    Register(in=in, load=load1, out=out1);
    // ... 8個すべてのRegister
    Register(in=in, load=load7, out=out7);

    // 出力選択
    Mux8Way16(a=out0, b=out1, c=out2, d=out3,
              e=out4, f=out5, g=out6, h=out7,
              sel=address, out=out);
}

動作プロセスの詳細

アドレス011(3番目)に値1234を書き込む例で確認しましょう。

  1. 書き込み制御: DMux8Wayがaddress=011によりload3=1、他を0に設定
  2. データ書き込み: 全Registerに1234が入力されるが、Register3のみが書き込み実行
  3. データ読み出し: Mux8Way16がaddress=011によりRegister3の出力を選択

この3段階のプロセスにより、指定したアドレスへの正確な読み書きが実現されます。

実際にRAM8チップを実装してみると、8個のRegisterすべてに同じデータが入力されているのに、load信号によって選択された1つだけが書き込まれるという仕組みが、なんだか不思議で面白く感じました。最初は無駄に思えたのですが、これがコンピュータの効率的なメモリ管理の基本になっているんですね。

ここまでで8個の記憶場所を持つRAMができました。しかし、実際のコンピュータではもっと大容量のメモリが必要です。RAM8チップを使ってさらに大きなメモリを作るにはどうすればよいでしょうか。

階層的メモリ拡張:小さなRAMから大容量メモリへ

RAM8チップができても、実用的なコンピュータには更に大容量のメモリが必要です。ここでNand to Tetrisプロジェクトの面白い設計が活かされます。

アドレス分割による階層化

より大きなメモリは、アドレスビットを「上位」と「下位」に分割する階層的な手法で構築します。

例えば、RAM64チップの場合:

6ビットアドレス = 000000 ~ 111111

分割:
address[3..5] (上位3ビット) → どのRAM8を選ぶか (8通り)
address[0..2] (下位3ビット) → RAM8内のどのRegisterを選ぶか (8通り)

結果: 8 × 8 = 64個のRegisterにアクセス可能

統一された拡張パターン

この階層化手法により、以下のような一貫した拡張が可能になります。

Register (1個) 
    ↓ ×8個組み合わせ
RAM8 (8個, 3ビットアドレス)
    ↓ ×8個組み合わせ  
RAM64 (64個, 6ビットアドレス)
    ↓ ×8個組み合わせ
RAM512 (512個, 9ビットアドレス)
    ↓ ×8個組み合わせ
RAM4K (4096個, 12ビットアドレス)
    ↓ ×4個組み合わせ
RAM16K (16384個のレジスタ, 14ビットアドレス)

階層化の利点

この設計手法にはいくつかの良い点があります。

例えば、RAM512を設計する際は、RAM64チップの内部実装を理解する必要がありません。RAM64チップが「64個のRegisterを6ビットアドレスで選択できる」という仕様だけ理解していれば十分です。

この「抽象化と実装の分離」は、現代のソフトウェア開発でも重要な設計原則として広く活用されています。

ここまで、DFFから始まって16K個のレジスタを持つRAMまでの実装を見てきました。最後に、このような学習体験から何が得られるのかを考えてみたいと思います。

学習体験の価値:理論と実践の融合

ここまでメモリシステムの実装を通じて見てきたように、Nand to Tetrisプロジェクトは単なる理論学習ではありません。実際に手を動かしてコンピュータの各要素を構築することで、深い理解が得られます。

実践的な学習効果

このプロジェクトの価値は、以下の3つの能力を同時に身につけられることです。

  • システム思考: 小さな要素から複雑なシステムを段階的に構築する設計手法
  • 抽象化の理解: ハードウェアからソフトウェアまでの各レイヤーの役割と関係性
  • 制約下での創造性: 限られた要素(Nandゲート)から目標を達成する問題解決力

個人的には、このプロジェクトを通じて「複雑なものも単純な要素の組み合わせでできている」ということを実感できたのが大きな収穫でした。普段のシステム設計でも、この考え方が役に立っています。

現代エンジニアへの意義

AI技術が発達した現代でも、コンピュータサイエンスの基礎理解は重要です。ChatGPTが書いたコードを適切に活用するにも、その土台となるコンピュータの動作原理を知ることが欠かせません。

メモリリークの解決、並行処理の設計、クラウドインフラの最適化など、実務の課題により的確に対処できるようになります。

それでは最後に、この記事で見てきたメモリシステムの学習体験を振り返ってみましょう。

まとめ

Nand to Tetrisプロジェクトは、一粒のNand論理ゲートから始まり、最終的にはテトリスが動作する完全なコンピュータシステムを構築する学習プロジェクトです。

この記事では、メモリシステムの段階的実装を通じて、コンピュータの基礎的な仕組みを見てきました。時間概念の導入から、フリップフロップ、Bitチップ、Registerチップ、そしてRAMチップまで、一歩ずつ積み上げることでメモリシステムの本質を理解できました。

このプロジェクトで本当に良いなと思うのは、抽象化と実装の分離、モジュラー設計、システム思考といった、現代のソフトウェア開発でも重要な概念を実体験として学べることです。AI時代だからこそ、コンピュータサイエンスの基礎をしっかり理解することで、より効果的なシステム設計や問題解決ができるエンジニアになれるはずです。

興味を持った方は、ぜひ実際にNand to Tetrisプロジェクトに挑戦してみてください。私も最初は「本当にNandゲートだけでコンピュータが作れるの?」と半信半疑でしたが、実際に手を動かしてみると、一つ一つの小さな積み重ねがやがて大きなシステムになっていく過程が本当に面白いです。途中でつまずくこともありますが、それも含めて学びになるはずです。

「聞いたことは忘れる。見たことは思い出す。体験したことは身に付く」— 孔子

参考リンク・関連資料

公式サイト・書籍

オンライン学習リソース

関連記事・解説

GitHubで編集を提案

Discussion