FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

Gem5のチュートリアル "Learning Gem5"をやってみる(15. SimpleMemobjの実装を噛み砕く)

gem5を構成するSimObjectを追加するためのチュートリアルをやってみる。

www.gem5.org

SimpleMemobjを使った簡単なブリッジの実装が良く分からなかったので、もう一度よく読みなおしてみることにした。

  • gem5記事一覧インデックス

msyksphinz.hatenablog.com


単純なメモリ・オブジェクトの例

本節では、単純なメモリ・オブジェクトを作る。以下の図の通り、CPUからはicacheとdcacheを経由して接続されており、外部にリクエストを転送するためのmem_sideポートが宣言されている。

  • mem_side : メモリ・バスにリクエストを送るためのマスタ・ポート
  • dcache_port : CPUからのリクエストを受け付けるためのスレーブ・ポート
  • icache_port : CPUからのリクエストを受け付けるためのスレーブ・ポート

次の章simplecache-chapterでは、このオブジェクトをキャッシュにするためのロジックを追加するが、今回は単なるブリッジとしての役割を持たせるということにする。

大きな手順は以下のとおりである:

  1. Python上でSimObjectを宣言する。
    1. 必要なポートを定義する
  2. SConscriptファイルを作成し、クラスをビルド対象として登録する
  3. C++側のSimpleMemobjクラスを定義する
    1. ヘッダファイルを作成する
    2. CPU側のポート(スレーブ・ポート)を定義する
    3. メモリ・バス側のポート(マスタ・ポート)を定義する
    4. getPort()によるポートのインタフェースを作成する
  4. slaveとmasterのポートに必要な関数を実装する
    • getAddrRanges()
    • recvAtomic()
    • recvFunctional()
    • recvRangeChange()
  5. CPU側からリクエストを受け付ける際のコードを記述する
    • recvTimingReq()
    • recvRespRetry()
    • これらには、メモリ・バス側に転送できない場合のフロー制御の実装も含まれる
  6. メモリ・バス側からレスポンスを受け付ける際のコードを記述する
    • recvTimingResp()
    • recvReqRetry()
    • これらには、CPU側に転送できない場合のフロー制御の実装も含まれる

SimObjectを宣言する

まずはPython上で操作をするためのC++ラッパであるSimpleMemobjクラスを作成する。これまでと同様に、SimObjectを継承させて作成する。

from m5.params import *
from m5.proxy import *
from m5.SimObject import SimObject

class SimpleMemobj(SimObject):
    type = 'SimpleMemobj'
    cxx_header = "learning_gem5/part2/simple_memobj.hh"

    inst_port = SlavePort("CPU side port, receives requests")
    data_port = SlavePort("CPU side port, receives requests")
    mem_side = MasterPort("Memory side port, sends requests")

このオブジェクトはSimObjectを継承している。SimObjectクラスには、C++の実装で定義しなければならない純粋仮想関数getPortがある。

このオブジェクトのパラメータは3つのポートである。CPUが命令ポートとデータ・ポートに接続するための2つのポートと、メモリー・バスに接続するためのポートだ。これらのポートにはデフォルト値はなく、簡単な説明しかない。

このSimpleMemobjクラスには、3つのメンバ変数が追加されていることに注意する。それぞれが上記で説明したポートに相当している。

  • inst_port : CPU側のスレーブ・ポート
  • data_port : CPU側のスレーブ・ポート
  • mem_side : メモリ側のマスタ・ポート

これらのポートは、SimObjectクラスで継承すべきgetPort()を実装する際に覚えておく必要がある。

さらに、SimpleMemobjをビルド対象として追加するためのSConscriptを追加する必要があることも忘れないこと。

C++側でSimpleMemobjクラスを定義する

次に、PythonにラップしてもらうためのC++側でのSimpleMemobjのヘッダファイルを作成する。

まずはクラスの外観とコンストラクタのみを作成する。SimpleMemobjクラスは、SimObjectを継承する形で作成する。

#include "mem/port.hh"
#include "params/SimpleMemobj.hh"
#include "sim/sim_object.hh"

class SimpleMemobj : public SimObject
{
  private:

  public:

    /** constructor
     */
    SimpleMemobj(SimpleMemobjParams *params);
};

slaveポート型を定義する

次に、2種類のポート(CPU側ポートとメモリー側ポート)のクラスを定義する必要がある。他のオブジェクトがこれらのクラスを使うことはないので、SimpleMemobjクラスの中でこれらのクラスを宣言する。

まず、スレーブポート(CPU側ポート)から始めよう。SlavePortクラスを継承する。以下は、SlavePortクラスのすべての純粋仮想関数をオーバーライドするために必要なコードである。

まず、スレーブ・ポート(CPU側のポート)から定義する。SlavePortクラスというのがあり、これを継承する形で定義する。SlavePortクラスの全ての純粋仮想関数をオーバ・ライドする必要がある。

// SimpleMemobjクラス内でCPUSidePortを宣言する
class CPUSidePort : public SlavePort
{
  private:
    SimpleMemobj *owner;

  public:
    CPUSidePort(const std::string& name, SimpleMemobj *owner) :
        SlavePort(name, owner), owner(owner)
    { }

    AddrRangeList getAddrRanges() const override;

  protected:
    // これらはSlavePortクラスで定義されている
    // 純粋仮想関数
    Tick recvAtomic(PacketPtr pkt) override { panic("recvAtomic unimpl."); }
    void recvFunctional(PacketPtr pkt) override;
    bool recvTimingReq(PacketPtr pkt) override;
    void recvRespRetry() override;
};

このオブジェクトでは、5つの関数を定義する必要がある。

  • getAddrRanges()
  • recvAtomic()
  • recvFunctional()
  • recvTimingReq()
  • recvRespRetry()

masterポート型を定義する

次に、マスタ・ポート・タイプを定義する必要がある。これは、CPU側からのリクエストをメモリ・システムの残りの部分に転送するメモリ側のポートになる。

同様に、SimpleMemobjクラスの下に定義し、MasterPortというクラスを継承することで作成する。

class MemSidePort : public MasterPort
{
  private:
    SimpleMemobj *owner;

  public:
    MemSidePort(const std::string& name, SimpleMemobj *owner) :
        MasterPort(name, owner), owner(owner)
    { }

  protected:
    // 純粋仮想関数で、必ずオーバライドしなければならない関数群
    bool recvTimingResp(PacketPtr pkt) override;
    void recvReqRetry() override;
    void recvRangeChange() override;
};

このクラスには、必ずオーバライドしなければならない純粋仮想関数が3つ用意されている。

  • recvTimingResp()
  • recvReqRetry()
  • recvRangeChange()

SimObjectインタフェースを定義する

ここまでで、2つの新しい型CPUSidePortMemSidePortを定義し、SimpleMemobjで使用する3つのポートを宣言できるようになった。SimObjectクラスの純粋仮想関数についても定義する必要がある。

これがgetPort()である。この関数はポートを通じてメモリオブジェクトを接続する初期化フェーズで実行される。

class SimpleMemobj : public SimObject
{
  private:

    <CPUSidePort クラスの定義>
    <MemSidePort クラスの定義>

    // 実際のCPUSidePort/MemSidePortのインスタンス
    CPUSidePort instPort;
    CPUSidePort dataPort;

    MemSidePort memPort;

  public:
    SimpleMemobj(SimpleMemobjParams *params);

    // 新たに実装されるgetPort()
    // これは必ずオーバライドしなければならない
    Port &getPort(const std::string &if_name,
                  PortID idx=InvalidPortID) override;
};

基本的なSimObject関数の実装

SimpleMemobjのコンストラクタの実装

SimpleMemobjのコンストラクタでは、単純にSimObjectコンストラクタが呼ばれるだけである。また、すべてのポートを初期化する必要がある。それぞれのポートのコンストラクタは2つの引数を持つ:

  • ポートの名前
  • その所持者のポインタ(this)

これらはヘッダファイルに定義したものを使用する。名前はどのような文字列でも良いが、利便性のためにPythonSimObjectファイルの名前と同一にする。

また、blocked変数をfalseとする(これについては前述で説明がないためどのような用途なのか不明)。

#include "learning_gem5/part2/simple_memobj.hh"
#include "debug/SimpleMemobj.hh"

SimpleMemobj::SimpleMemobj(SimpleMemobjParams *params) :
    SimObject(params),
    instPort(params->name + ".inst_port", this),
    dataPort(params->name + ".data_port", this),
    memPort (params->name + ".mem_side",  this), 
        blocked(false)
{
}

getPort()の実装

getPort()関数の目的は、オブジェクトから、ポートの名前を使ってその内部のポートインスタンスを呼び出すためのものである。

実装は極めて単純で、引数のif_nameを比較してmem_sideという名前であるかをチェックする。もしそうであれば、memPortオブジェクトを返す。名前がinst_portであれば、instPortを返し、data_portであればdataPortを返す。そうでなければ、新たに上位のgetPort()を返すことになる。

Port &
SimpleMemobj::getPort(const std::string &if_name, PortID idx)
{
    panic_if(idx != InvalidPortID, "This object doesn't support vector ports");

    // This is the name from the Python SimObject declaration (SimpleMemobj.py)
    if (if_name == "mem_side") {
        return memPort;
    } else if (if_name == "inst_port") {
        return instPort;
    } else if (if_name == "data_port") {
        return dataPort;
    } else {
        // pass it along to our super class
        return SimObject::getPort(if_name, idx);
    }
}

slaveポートとmasterポート関数の実装

上記で放置していた、オーバ・ライドしなければならない関数を実装していく。

  • getAddrRanges()
    • 該当するSimObjectがターゲットとしているメモリアドレスの領域のリストを返す
    • Convenience typedef for a collection of address ranges.
  • recvAtomic()
  • recvFunctional()
    • これは良く分からない。実際のRD/WR操作を行うための関数?
  • recvTimingReq()
  • recvRespRetry()
  • recvTimingResp()
  • recvReqRetry()
  • recvRangeChange()

slaveとmasterポートの実装はシンプルである。ほとんどの場合、ポートの関数はメインメモリオブジェクト(SimpleMemobj)にフォワードするだけである。

2つのシンプルな関数から始める:getAddrRanges()recvFunctional()は、単純にSimpleMemobjのものを呼び出すだけである

AddrRangeList
SimpleMemobj::CPUSidePort::getAddrRanges() const
{
        // SimpleMemobjのgetAddrRanges()を呼び出す
    return owner->getAddrRanges();
}

void
SimpleMemobj::CPUSidePort::recvFunctional(PacketPtr pkt)
{
        // SimpleMemobjのhandleFunctionalを呼び出す
    return owner->handleFunctional(pkt);
}

SimpleMemobjgetAddrRanges()は、メモリ・バス側のgetAddrRanges()を呼び出す。

また、SimpleMemobjhandleFunctional()は、メモリ・バス側のsendFunctional()を呼び出す。

つまり、どちらとも単純にスレーブからマスターへパケットをフォワードしているだけである。

void
SimpleMemobj::handleFunctional(PacketPtr pkt)
{
    memPort.sendFunctional(pkt);
}

AddrRangeList
SimpleMemobj::getAddrRanges() const
{
    DPRINTF(SimpleMemobj, "Sending new ranges\n");
    return memPort.getAddrRanges();
}

MemSidePortでは、recvRangeChangeを実装する必要があり、SimpleMemobjを通じてスレーブポートに転送を行う。

SimpleMemobjを通じて、instPortdataPortに対してsendRangeChange()を呼び出す。

void
SimpleMemobj::MemSidePort::recvRangeChange()
{
    owner->sendRangeChange();
}
void
SimpleMemobj::sendRangeChange()
{
    instPort.sendRangeChange();
    dataPort.sendRangeChange();
}