開発日記

Erlangをダラダラ書きます。

CISCが生まれた背景を知りたいか.うん知りたい.

CISCが生まれた背景

時代がCPUの設計思想に大きく影響を与えている.

主に3つの要望があったと思われる.

1. メモリサイズを小さくしたい!そして消費電力とコストを下げたい!

  • 昔はメモリが高価でできるだけサイズを小さくしたかった.CISCではこの要望に応えるために,可変長命令を採用した.可変長であることで,命令の利用頻度に応じて機械語コードの長さをチューニングし,コードのビット数を削減できた.嬉しい.

2. メモリアクセス数を減らしたい!そして高速に動作させたい!

  • 昔はメモリが低速でフォンノイマンボトルネックが酷かった.キャッシュメモリも一般的ではなくCPUクロックをあげても高速動作には繋がらなかった.CISCではこの要望に応えるために,複雑な機能を予めサブルーチン化してCPU内に組み込み1命令で処理を実行できるようにすることで,必要な命令数を減らした.命令の数が減れば当然命令をフェッチする回数も減り,メモリが低速なシステムでも高パフォーマンスが出せるようになった.嬉しい.

3. アセンブラ記述を楽にしたい!ひたすら楽したい!

  • 昔はプログラムをアセンブラで直接記述するのが一般的で苦労することが多く,覚えることはできるだけ減らしたかった.CISCはこの要望に応えるために,レジスタや即値やメモリへのアクセスを同じ命令で扱えるようにし,命令ごとにオペランドを覚えたりする必要性を無くした.嬉しい.

  • 上記は,『命令がすべてのアドレッシングモードを利用できる』ようにしたとも言える.ここでもアドレッシングモードは広義のアドレッシングモードを指す.(下記に補足説明)また,これは命令の直交性が高いとも表現できる.(ちなみに,命令の直交性を上げようとするなら,可変長命令が自然な選択肢となる.なぜなら,レジスタへのアクセスとメモリへのアクセスで利用するビット数に差がありすぎるため,固定長だと無駄が多い設計となるから)

補足説明・アドレッシングモード

  • 広義の意味:操作対象をオペランドによって指定する,その指定方法

    • 例えば、命令がすべてのアドレッシングモードを利用できるとは、『add レジスタ, メモリ』や『add レジスタ, レジスタ』など同じadd命令で全ての対象(レジスタ・メモリ・即値)を扱えることを指す。
    • RISCでは、命令ごとに扱えるオペランドを制限している。そうすることで固定長命令になり、命令の読み込みと解釈を分けることができるようにしている。
  • 狭義の意味:メモリアクセスの指定方法

    • レジスタ間接(後述のインデックス付きレジスタ間接で,ベースをゼロレジスタにしたら良いのでわざわざレジスタ間接の命令は容易しなくていい)
    • ディスプレートメント付きレジスタ間接(PC相対・GP相対 etc,構造体アクセスとかに便利.構造体のポインタにメンバのオフセットを代入するだけで良く,わざわざレジスタを使うまでもない)
    • インデックス付きレジスタ間接(配列のアクセスなどに利用.配列の先頭アドレスをベースレジスタ,アクセス先までのオフセットをインデックスレジスタに格納)

その後どうなったか...

  • RISCの流れが生まれた.理由は,高クロックなCPUが欲しい!という要望があったからである.クロックをあげるためには動作をパイプラインにすると良い.このパイプラインの動作に『可変長命令』が邪魔をしてきた.可変長がゆえに命令メモリから命令をフェッチする際,次の命令がどこまでか見当がつかない.つまり,命令の読み込みと解釈を同時に実施する必要性が生まれ,『命令の読み込み』と『命令の解釈』を2段の別ステージに分けることができなくなってしまった.またシンプルに可変長命令の判断は遅延が生じル.高クロックを達成するためには可変長命令を諦めざるおえなくなった...

  • さらに,複雑な機能を予めサブルーチン化してCPU内に組み込むというのも,邪魔になった.なぜなら複雑なサブルーチンは処理がいつ終わるか予測できないため,ストールが発生しパイプライン処理が詰まってしまうからだ.ゆえにサブルーチンをCPUに組み込むことは諦めざるおえなくなった... (メモリ効率と速度はいつだってトレードオフ,とほほ)

  • さらにさらに,高級言語コンパイラの登場でアセンブラでプログラミングする必要性もなくなった...

RISCはパイプライン処理を効率的に動作させるために

❌ 可変長命令

❌ 命令の直交性

❌ 複雑な処理のサブルーチン化

⭕️ 固定長命令(即値フィールドも必要な量に抑える)

⭕️ アドレッシングモードを必要なものに限定

⭕️ 処理の単純化(空いたスペースにキャッシュメモリや多くのレジスタ

⭕️ ロード・ストアアーキテクチャキャッシュメモリを効率的に動作させるため,アクセスを1つのステージ限定させる.1命令1アクセスに限定する.)

⭕️ 多数の汎用レジスタレジスタはアクセス速度を予測できる.予測できるとコンパイラが最適化しやすくなる.)

他にもRISCは,3オペランドのものが多かったり,遅延スロットを持つ場合がある.


  • ただ今は,RISCCISCと明確に区別はできない.(可変長命令でも内部はRISCのように動作したりしてる)
  • 今は,すべてがRISCともいえる.(表面上の命令セットがCISCライクなだけ)

上記に関して,間違っていたらご指摘してくださると幸いです.

参考文献

久々の投稿.最近はアセンブラにハマっている.

アセンブラにハマっている.

坂井先生の「大熱血!アセンブラ入門」を読み始めた.

説明が丁寧なうえ従来とは異なるアプローチでアセンブラを読み解いていてとても面白い.

この本では,目的ベースで話が進んでいく.まずC言語のコードを読み,何をするコード何か理解する.そしてそのコードの実行ファイルを逆アセンブルした結果を眺める.

目的ベースでアセンブラを読むため,アセンブラの気持ちを伺いながら読むことができる.この流れが個人的にしっくりきていて読んでいて楽しい.

これからアセンブラの世界にどっぷり浸かりそうな気がしている. 自分はRISC-Vのコアを触ることが多いので,RISC-Vのアセンブラを他のRISC-Vライクなコアと比べてみるのもとても楽しそうだ.

本のページ数は1000ページ越えととても多いが,ゆっくり読み進めようと思う.

今はPart1のChapter6を読んでいるのだが,そこで「命令エイリアス」について学んだ.

命令エイリアス

命令エイリアスというのは,アセンブラの命令に別名を名付けた命令のことを指す.

例えば,MIPSでは,レジスタのコピーを行うmove命令が実はエイリアス命令で,中身はaddu命令だったりする.

これはRISCライクなアーキテクチャアセンブラではよくあることで,メリットがある.

メリット

  • 命令数を削減できる.RISCライクなアーキテクチャは固定命令長を採用していることが多い.したがって機械語コードに含められるオペコードの数に限りがある.同じような命令はエイリアスとして同じように扱うことで,カバーできる命令を増やすことができる.

  • 回路面積を減らせる.命令を増やすとその分の独立した回路が必要になるため回路面積が大きくなる.回路面積を減らせると消費電力も減るし小型化できるため良いことが多い.

上記で紹介したレジスタ間の値のコピーだけでなく,即値の代入やnop命令など,エイリアス命令として実装できる命令は意外と多くある.

ちょっと疑問なこと

  • レジスタ間のコピーをエイリアスで実現する方法は,本で学んだ限り2つある.1つは「OR演算」を利用する方法.もう1つは,「加算命令とゼロレジスタ」を利用する方法である.本の中ではPowerPCはOR演算を利用しており,MIPSではゼロレジスタを利用していた.この2つの方法はアセンブラ依存で変わるらしいのだが,実際のアセンブラの開発者はどういう理由でエイリアスのぱたーんを採用しているのだろうか? 気まぐれで採用されるのだろうか?

この疑問に答えられるだけの知識が今の自分にはないため,ここに一旦疑問として残しておく.

もしわかる方がいらっしゃったら教えていただきたいです.

今日はこの辺で.

scala勉強

最近サボりがちだった.

abstract class

抽象メソッドの場合は振る舞いのみを定義し,実装は具象にお任せ〜

どういうメリットがあるのかいまいち分かってない.

具象ではoverrideで上書き処理する.

case class

  • イミュータブル
  • 値で比較

本来のclassは参照比較

これもどんなメリットがあるかいまいちわかっていない..誰か教えて〜

参考

qiita.com

Scala Hello

package Hello

class Hello {
    private val name: String = "YMD"

    def Hello(num: Int): Unit = {
        for (i <- 1 to num) {
            println("Hello " + name)
        }
    }

    def Name: String = {
        name
    }
}

object Hello {
    def main(args: Array[String]) = {

        val hello = new Hello()

        hello.Hello(3)

        println(hello.Name)
    }
}

コンピュータの超基本

プログラム

  • 計算の手順を示したもの

  • メモリ上にある機械語(バイナリ)

プログラム用語

  • オペコード(opcode)... 演算命令を指定

  • オペランド(operand)... 演算命令に用いる演算子

ISA

  • バイナリと演算命令の対応表

コンピュータ構成要素

  • CPU(メモリから命令を取り出して計算)

  • メモリ(命令列とデータを保持,単一の巨大な配列,アドレスとデータ)

CPU構成要素

  • Functional Unit

  • Register

  • Program counter

CPUの動作

  1. PCに保持しているアドレスでメモリにアクセス
  2. 読んだ命令に応じて処理
  3. PCを更新
  4. 1に戻る

たったこれだけ.

CPU具体的な動作

  1. 命令からメモリを読み出し(Fetch)
  2. 読み出したバイナリを解読(Decode)
  3. 解読結果を用いて演算(Execute)
  4. 必要であればメモリアクセス(Memory access
  5. 必要であればレジスタアクセス(Write back)

なぜレジスタがある?

メモリより高速に動作できるから.

レジスタは必須の存在ではない.

パルス幅変調(PWM)

幅が変わる信号

  • 信号が高い時間の割合(デューティサイクル)を変えることで変調

  • 割合25%(12サイクル中3サイクルHigh)

  • 割合50%(10サイクル中5サイクルHigh)

応用

  • ローパスフィルタを加えるとデジタル・アナログ変換回路になる

Chisel テスト

PeekPokeTester

用意するのは3つのファイル
  • DUT

  • テスト回路

  • テストオブジェクト

テストの流れ
  1. テスト対象のモジュール(DUT)をインスタンス

  2. テスト回路を準備(入力ポートに値をセットしたり...)

  3. テスト回路を駆動するテストオブジェクト準備

  4. sbtでテストオブジェクトを実行

便利なテスト関数
  • poke() ... 入力値を設定

  • peek() ... 出力値を読み出し

  • step() ... クロックサイクルを進める

  • println() ... 出力値を表示

  • expect() ... 期待値と比較

ScalaTest

ScalaJavaのテストツール

ユニットテスト(単体テスト)が便利になるっぽい

ちょっと特殊な書き方をする(またいつか紹介)

PeekPokeTester(Chiselテスト)ScalaTestでラップできる

ラップできるとsbt "testOnly testnameのように実行できる

波形表示

デジタル回路デバッグの古典的アプローチ

入力に値を入れてクロックを進めるだけのテスト回路を作成

ScalaTestとDriver.executeを用いて波形ファイル(.vcdファイル)を生成

波形の表示には「GTKWave」・「ModelSim」が使用可能

テスト回路ではfor文を活用して全ての入力信号を生成すれば良い.

printfデバッグ

ハードウェアモジュール定義の中のどこでも書ける

プログラムの実行中に気になる変数をチェック

出力はクロックの立ち上がりに実行