2021/7/5エンジニア

    [コンピュータ・アーキテクチャ]機械仕掛けの脳(前編)

    執筆: H.T

    はじめに

    お久しぶりです。低レイヤー好きのH.Tです。

    最近は蒸し暑くて、ろくに外にも出られませんね。

    そんな時こそ、コンピュータに思いを巡らせるというのはいかがでしょうか。ということで、今回はコンピュータの中心的なパーツであるCPUについて語りたいと思います。

    皆様は、CPUに対してどのようなイメージをお持ちでしょうか。昔の私は、「すごく複雑な機械で、なにやら難しいことをしている」という漠然としたイメージを持っていました。

    結論から言ってしまうと、現代のCPUはとても複雑な構成をしていますが、それは単純な要素を複雑に組み合わせた結果に過ぎません。つまり、一つ一つを確実に理解していけば、大して難しいものではありません。

    前回は、CPUの構成要素の一つである加算器について解説しました。電圧の昇降で表現される論理演算によって、どのように計算がなされるか、何となく想像がついたかと思います。

    今回からは、加算器も含めたCPUの全体像について、数回に分けてお送りします。最終的にはCircuitVerseを使って単純な8bit CPUを作ってみたいと思います。

    図: 実装予定の8bitCPU

    CPUは文字通りコンピュータの中心的なパーツ。

    CPUを語ること、それ即ちコンピュータを語ること。です。

    本稿は序章として、コンピュータ誕生の歴史から大まかな発展を辿り、CPU製作の第一歩として仕様を策定していきます。

    最後まで読んでいただければ、CPUとは、コンピュータとはなにか、明確なイメージが出来るようになると思います!

    是非、最後までお付き合いくださいませ。

    コンピュータができるまで

    前述の通り、現代のコンピュータはとても複雑な構成をしているため、いきなり中身から入ると理解に時間がかかります。少し視点を変えて、まずはコンピュータの歴史をたどり、古くて原始的な計算機がいかにして現代コンピュータになっていったかを見てみましょう。

    コンピュータの歴史をたどると、17世紀には機械式計算機というものがありました。これは、ダイヤルで数字を入力し、ガチャガチャ回すと計算ができるというカッコイイ機械です。機能としては、前回作成した加算器と同様で、ある入力の演算結果を出力する、いわば歯車でできた電卓です。

    例えば、ドイツの偉大な数学者、ゴットフリート・ライプニッツが作った手回し式計算機は、四則演算を行うことができました。このような機械式計算機は、形は変われど、日本でも1970年頃まで流通していたそうです。

    コンピュータに近い形としては、同じく17世紀、チャールズ・バベッジの解析機関と呼ばれる機械があります。様々な事情により完成には至りませんでしたが、今までの機械式計算機と違い、パンチカードに計算手順を記述することで、複雑な計算を自動的に行えるというものでした。

    ただの電卓のようなものから、複雑な計算手順をプログラミングできる汎用計算機へ進化しようとした、世界初の設計と言われています。おまけに手回しでなく蒸気で動くということで、もし完成していたら、もの凄いインパクトがあったことでしょう。完全にスチームパンクの世界です。

    パンチカードを使った計算機は、タビュレーティングマシン(作表機)という名称で1890年あたりから注目を浴び始めます。当時、人口増加で困り果てていた米国の国勢調査で利用されたそうです。パンチカードの優れた点は、命令の記述だけでなく、データの保存にも利用できるという点でした。

    そして20世紀、真空管が誕生します。現代ではほとんど見かけることのない代物ですが、当時としてはものすごく画期的な発明でした。この真空管、何ができるのかというと、流れる電流量を電圧によって制御することができます。勘の鋭い方は気付かれたかもしれませんが、これは前回登場したトランジスタと類似した機能です。そう、真空管が消えてしまったのは、上位互換であるトランジスタが登場したからなのです。

    さて、前回の記事によれば、トランジスタをスイッチとして利用することで、AND/OR/NOTの論理演算を行う回路が作れるのでした。電圧をかけた時だけ電気が流れ、かけていない時は通さないという特性を生かしたものです。そして論理演算を行うための理論も既に完成していました。あとは理論と物理を紐付けるだけです。

    真空管を使ったコンピュータは1940年あたりから登場し始めました。ENIACは、最初期のコンピュータとして有名なマシンの一つです。

    プログラム内蔵方式

    戦時中に弾道計算を行う目的で開発されたENIACには、様々な問題がありました。躯体は巨大で、桁違いに電力を消費します。最大の欠点は、別の計算を行うためのプログラム変更の煩雑さで、これは数日を費やすような大変な作業でした。これでは、「汎用」コンピュータとしてはあまりに使いづらいでしょう。やはり、コンピュータたるもの、ある時は弾道計算、ある時は暗号解読と、様々な計算アルゴリズムを瞬時に切り替えて実行したいものです。

    そんな要望に答えたのが、「プログラム内蔵方式」でした。

    ENIACのような初期のコンピュータは、プログラム変更のために配線を入れ替えたり、スイッチをオン・オフしたりといった、ハードウェア的な変更作業が必要でした。

    新らしいプログラム内蔵方式では、コンピュータに大きな記憶装置をもたせて、これにプログラムを書き込みます。要は、記憶装置内のプログラムを入れ替えるだけで様々な処理を実行できるという発想です。

    ちなみにプログラムとは、計算手順を記述した命令の集まりのことで、コンピュータはこれを参照することで様々な動作を行うことができます。

    例えば、プログラムの中に以下のような命令文があったとしましょう。

    「記憶装置の0番地に入っている値と、1番地に入っている値を足して、2番地に格納する」

    コンピュータはこれを読み取り、この通りの動作を実行します。

    このときに重要なのが、制御装置です。制御装置は、受け取った命令どおりの操作を実行するために、コンピュータ内の各装置に指示を出します。上の例で考えると、コンピュータは実際に次のような動作をします。

    1. 制御装置は、演算装置と呼ばれるコンピュータ内の計算機に指示を出し、「足し算モード」にする
    2. 制御装置は、記憶装置に指示を出し、0番地と1番地の値を演算装置に送る
    3. 演算装置は記憶装置から送られてきた2つの値を足して、結果を出力する
    4. 出力された結果は制御装置の指示により、記憶装置の3番地に格納される

    ここで、「制御装置」「演算装置」「記憶装置」という登場人物が出てきましたので、役割を整理しておきましょう。

    • 演算装置… 与えられた2つの値に対して計算を行い、1つの結果を出力する
    • 記憶装置… プログラムやデータを保存したり、出力したりする
    • 制御装置… 命令通りの動作を行えるよう、演算装置や記憶装置に指示を出す

    CPUは、この内の演算装置と制御装置を内包した部品であり、コンピュータの頭脳のような存在と言えます。

    制御装置

    少しずつコンピュータの中身が明らかになってきましたが、まだもやもやしていると思います。そもそも制御装置は命令をどうやって理解しているのでしょうか。

    命令を単純化することで、コンピュータにもわかりやすくしてみましょう。

    「記憶装置の0番地にある値と、1番地にある値を足し算して、2番地に格納する」

    このような命令は次のように単純化できます。

    足し算, 2番地, 0番地, 1番地

    さらに、予め加算機能について00という名前を割り振っておけば、数字の羅列だけですべてを表せます。

    0, 2, 0, 1

    最後に、コンピュータは2進数でものを考えるので、下のように変換してみましょう。

    0000 0010 0000 0001

    どうでしょう。コンピュータでも理解しやすい命令になった気がしませんか?

    はじめに言ったとおり、コンピュータは単純な要素を複雑に組み合わせた結果に過ぎません。これだけやってあげないと、単純なハードウェアは命令を正しく解読できないわけです。

    命令が単純すぎると思われるかもしれませんが、実際のプログラムもこれと似たような命令をたくさん組み合わせて作られています。ただし、このような単純な命令を使いこなすのは人間にとってとても大変な作業ですので、実際には「プログラミング言語」と呼ばれる、より人の言語に近い形式で記述した後、「コンパイラ」というソフトウェアが、機械にも理解できる単純な命令に分解します。

    仕様策定

    それでは、実際に仮想的なCPUを作ってみましょう。まずはCPUに与える命令の仕様を策定します。

    上の例のように、CPUに与える命令は主に「命令種別コード」「宛先アドレス」「被演算子1」「被演算子2」の四種類に別れます。ここでは、命令の長さを16bitとし、以下の様なフィールドで分けて考えます。

    ピッタリ16bitになりました。命令種別コードが4bitですので、このCPUは最大2の4乗、16個の命令をサポートすることができます。

    また、宛先アドレス、被演算子1、被演算子2がそれぞれ宛先レジスタ、ソースレジスタ1、ソースレジスタ2となっています。レジスタというのは、CPUの内部にある高速な記憶装置のことです。今回はアドレスフィールドが3bitなので、8つのレジスタをCPU内に配置します。これまで、「0番地」「1番地」などと表現していたものは、実は8つのレジスタの番号を指定していたのです。

    「CPUは制御装置や演算装置だけでなく、記憶装置も内包しているの?」と思われたあなた、鋭いです。答えは、そうとも言えますが、あくまでメインの記憶装置(主記憶装置と言います)はCPU外部にあることが多いです。レジスタはSRAMと呼ばれる記憶装置で構成されており、最速のメモリなのですが、コストが高く容量は小さいです。対して、主記憶装置を構成するDRAMは、SRAMより遅いですが、コストが安く大容量を確保できます。

    コンピュータ全体の動きを見てみると、主記憶装置にあるプログラムやデータを、必要な分だけCPUのレジスタにコピーして計算を行います。なぜこのような二度手間な動作をするのでしょうか。それは単純に、主記憶装置が遅いからです。だからといって、高コストなSRAMをたくさん作るのは現実的ではありません。

    最後の拡張用と書かれた3bitのフィールドは、今回利用しません。特に用途が無いので、命令を拡張したくなったときに利用するフィールドと考えてください…。

    では、4bitの命令種別コードに、必要な命令を割り当てていきましょう。

    コード命令種別詳細
    0000add足し算
    0001sub引き算
    0010mul掛け算
    0011xorXOR
    0100andAND
    0101orOR
    0110slビットを左にシフト
    0111srビットを右にシフト
    1000addiレジスタの値と指定した値を足し算
    1001subiレジスタの値と指定した値を引き算
    1010lw記憶装置の値をレジスタへ読み込み
    1011swレジスタの値を記憶装置へ書き込み
    1100
    1101
    1110beq条件分岐
    1111j指定行の命令へジャンプ

    2つ余りましたが、最低限の命令を策定できました。

    無駄に高機能な割に割り算ができないという歪な構成ですが、除算器を実装するのは面倒なので許してください。命令に空きがあるので、興味の有る方は実装してみても面白いかもしれません。

    ところで、先程命令の形式を下のように定めましたが、すべての命令がこの形式で良いのでしょうか。

    たとえば、addi、subiはそれぞれ、レジスタの値と、即値(命令中に埋め込まれた値)という2つの値を被演算子として演算を行う命令です。ですが、この命令形式では即値を扱うのに3bitのフィールドしかありません。これでは、0から7までの数字しか表せないのです。

    そこで、即値を取り扱うaddi、subi、lw、sw、beq命令に関しては、次のような形式を定めます。

    これで、即値用に6bitのフィールドを確保できました。

    最後にj命令です。

    j命令は、プログラムの指定した行へジャンプする機能を持っています。通常、プログラムは1行目から順番に実行されますが、同じ命令を繰り返し実行したい場合や、離れた命令を実行したい場合にこのような命令が必要になります。

    ジャンプ先のアドレスだけを指定できれば良いので、次のように定めましょう。(なんと、12bitという広い範囲でジャンプできるようになりました。)

    このように、ある程度柔軟に命令形式をサポートすることで、16bitという限られた命令長を最大限活用できるようになります。サポートする命令形式が少ないほど、ハードウェアの設計が容易になりますが、1命令で表現できる幅が少なくなります。逆に、多くの命令形式をサポートすると、表現幅は広がりますが、ハードウェア設計が困難になります。

    本稿で作成した「命令形式」や「命令種別・コード」といった仕様を、命令セットアーキテクチャ(ISA)と呼びます。

    広く世に出ているISAとしては、Intel社のx64や、Arm社のA64(AArch64)等があります。これらの仕様書はインターネットで誰でも入手できるようになっています。

    ともあれ、これで役者は揃いました。あとは実装するだけです!

    まとめ

    • プログラム内蔵方式では、大容量の主記憶装置にプログラムやプログラムの実行に必要なデータを格納することで、プログラムの書き換えを容易にした。
    • プログラムの実行には演算装置、制御装置、記憶装置がひつようであり、この内演算装置と制御装置はCPUに含まれる。
    • SRAMは高速だがコストが高く、DRAMは低速だがコストが低い。
      お互いの欠点をカバーするために記憶装置を階層構造にした。
    • コンピュータは単純な命令しか実行できないため、命令を単純化し、役割ごとにわかりやすくフィールドに分けた。
    • 命令の種類ごとに複数の命令形式を作ることで、限られた命令長を最大限有効活用しようとした。

    次回はいよいよ、CircuitVerseでこのCPUを実装していきたいと思います。

    おたのしみに。

    参考書籍

    David A. Patterson & John L. Hennessy,『コンピュータの構成と設計』.第5版.日経BP.2014

    中野 明『IT全史 / 情報技術の250年を読む』.祥伝社.2017

    関連リンク