FPGA開発日記

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

Gem5のチュートリアル "Learning Gem5"をやってみる(11. SimObjectへのパラメータの追加)

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

www.gem5.org

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

msyksphinz.hatenablog.com


gem5のPythonインターフェースの最も強力な部分の1つは、Pythonからgem5のC++オブジェクトにパラメータを渡す機能である。 この章では、前章の単純なHelloObjectをベースに、SimObjectのパラメータの種類とその使い方をいくつか検討する。

簡単なパラメータ

まず、HelloObjectにイベントを発生させる待ち時間と回数のパラメータを追加する。パラメータを追加するには、SimObject Pythonファイル(src/learning_gem5/part2/HelloObject.py)のHelloObjectクラスを修正する。パラメータは、PythonクラスにParam型を含む新しいステートメントを追加することで設定される。

例えば、以下のコードでは "Latency "パラメータであるtime_to_waitと、整数パラメータであるnumber_of_firesが設定されている。

class HelloObject(SimObject):
    type = 'HelloObject'
    cxx_header = "learning_gem5/part2/hello_object.hh"

    time_to_wait = Param.Latency("Time before firing the event")
    number_of_fires = Param.Int(1, "Number of times to fire the event before "
                                   "goodbye")

Param.<TypeName>TypeName型のパラメータを宣言している。一般的な型は整数ならInt浮動小数点数ならFloatなどである。これらの型は通常のPythonクラスのように動作する。

各パラメータ宣言は1つか2つの引数を取る。(上記のnumber_of_firesのように)2つのパラメーターが与えられた場合、最初の引数はパラメータのデフォルト値となる。この場合、Pythonの設定ファイルでnumber_of_firesに何も指定せずにHelloObjectインスタンス化すると、デフォルト値の1を取ることになる。

パラメータ宣言の2番目の引数は、パラメータの短い説明である。これはPython文字列でなければならない。パラメータ宣言のパラメータを1つだけ指定した場合は、それが説明文になる(time_to_waitの場合)。

gem5では、組み込み型以外の多くの複雑なパラメータ型もサポートしている。例えば、time_to_waitLatencyである。Latencyは文字列として時間としての値を受け取り、それをシミュレータのtickに変換する。例えば、デフォルトのtick rateが1ピコ秒(1秒間に1012 tick、または1THz)の場合、"1ns "は自動的に1000に変換される。他にも、PercentCyclesMemorySizeなどの便利なパラメータがある。

SimObjectファイルでこれらのパラメーターを宣言したら、コンストラクタでその値をC++クラスにコピーする必要がある。次のコードは、HelloObjectコンストラクタの変更を示している。

HelloObject::HelloObject(const HelloObjectParams &params) :
    SimObject(params),
    event(*this),
    myName(params.name),
    latency(params.time_to_wait),
    timesLeft(params.number_of_fires)
{
    DPRINTF(Hello, "Created the hello object with the name %s\n", myName);
}

ここでは、latencytimesLeftのデフォルト値にパラメータの値を使用している。さらに、パラメータ・オブジェクトの名前をメンバ変数myNameに格納し、後で使用できるようにしている。各パラメータのインスタンス化には、Pythonの設定ファイルから得られる名前を持つ。

しかし、ここで名前を代入するのは、paramsオブジェクトを使用する一例に過ぎない。すべてのSimObjectには、常に名前を返すname()関数がある。したがって、上記のように名前を保存する必要はない。

HelloObjectクラスの宣言に、名前用のメンバ変数を追加する。

class HelloObject : public SimObject
{
  private:
    void processEvent();

    EventWrapper event;

    const std::string myName;

    const Tick latency;

    int timesLeft;

  public:
    HelloObject(HelloObjectParams *p);

    void startup();
};

上記でgem5を実行すると、以下のエラーが発生する。

gem5 Simulator System.  http://gem5.org
gem5 is copyrighted software; use the --copyright option for details.

gem5 compiled Jan  4 2017 14:46:36
gem5 started Jan  4 2017 14:46:52
gem5 executing on chinook, pid 3422
command line: build/X86/gem5.opt --debug-flags=Hello configs/learning_gem5/part2/run_hello.py

Global frequency set at 1000000000000 ticks per second
fatal: hello.time_to_wait without default or user set value

これはtime_to_waitパラメータにデフォルト値がないためである。そのため、Pythonの設定ファイル(run_hello.py)を更新して、この値を指定する必要がある。

root.hello = HelloObject(time_to_wait = '2us')

あるいは、time_to_waitをメンバ変数として指定することもできる。m5.instantiate()が呼ばれるまでC++オブジェクトは生成されないので、どちらのオプションを選んでも全く同じである。

root.hello = HelloObject()
root.hello.time_to_wait = '2us'

この単純なスクリプトの出力は、Helloデバッグフラグを実行した場合、次のようになる。

gem5 Simulator System.  http://gem5.org
gem5 is copyrighted software; use the --copyright option for details.

gem5 compiled Jan  4 2017 14:46:36
gem5 started Jan  4 2017 14:50:08
gem5 executing on chinook, pid 3455
command line: build/X86/gem5.opt --debug-flags=Hello configs/learning_gem5/part2/run_hello.py

Global frequency set at 1000000000000 ticks per second
      0: hello: Created the hello object with the name hello
Beginning simulation!
info: Entering event queue @ 0.  Starting simulation...
2000000: hello: Hello world! Processing the event! 0 left
2000000: hello: Done firing!
Exiting @ tick 18446744073709551615 because simulate() limit reached

コンフィギュレーションスクリプトを変更して、イベントを複数回発生させることもできる。

パラメータとしての他のSimObjects

他のSimObjectをパラメータとして指定することもできる。これを示すために、新しいSimObject、GoodbyeObjectを作成する。このオブジェクトは、他のSimObjectに”Goodbye”と言う単純な関数を持っている。もう少し面白くするために、GoodbyeObjectはメッセージを書き込むためのバッファと、メッセージを書き込むための限られた帯域幅を持っている。 まず、SConscriptファイルでSimObjectを宣言する:

Import('*')

SimObject('HelloObject.py', sim_objects=['HelloObject', 'GoodbyeObject'])
Source('hello_object.cc')
Source('goodbye_object.cc')

DebugFlag('Hello')

新しいSConscriptファイルはここからダウンロードできる。

次に、SimObject Pythonファイルで新しいSimObjectを宣言する必要がある。GoodbyeObjectHelloObjectと関係が深いので、同じファイルを使う。以下のコードをHelloObject.pyに追加する。

このオブジェクトには2つのパラメータがあり、両方ともデフォルト値になっている。最初のパラメータはバッファのサイズで、MemorySizeパラメータである。つ目はwrite_bandwidthで、バッファを埋めるスピードを指定する。バッファが一杯になると、シミュレーションは終了する。

class GoodbyeObject(SimObject):
    type = 'GoodbyeObject'
    cxx_header = "learning_gem5/part2/goodbye_object.hh"
    cxx_class = "gem5::GoodbyeObject"

    buffer_size = Param.MemorySize('1kB',
                                   "Size of buffer to fill with goodbye")
    write_bandwidth = Param.MemoryBandwidth('100MB/s', "Bandwidth to fill "
                                            "the buffer")

次に、GoodbyeObjectを実装する。

#ifndef __LEARNING_GEM5_GOODBYE_OBJECT_HH__
#define __LEARNING_GEM5_GOODBYE_OBJECT_HH__

#include <string>

#include "params/GoodbyeObject.hh"
#include "sim/sim_object.hh"

class GoodbyeObject : public SimObject
{
  private:
    void processEvent();

    /**
     * バッファを1反復分フィルする。バッファが満杯でない場合、
        * この関数は別のイベントをキューに入れ、フィルを継続する
     */
    void fillBuffer();

    EventWrapper<GoodbyeObject, &GoodbyeObject::processEvent> event;

    /// 1tickあたりに処理するバイト数
    float bandwidth;

        /// フィルする予定のバイトサイズ
    int bufferSize;

    /// メッセージを挿入するバッファ
    char *buffer;

    /// バッファに挿入するメッセージ
    std::string message;

    /// これまでに使用したバッファの量
    int bufferUsed;

  public:
    GoodbyeObject(GoodbyeObjectParams *p);
    ~GoodbyeObject();

    /**
     * Called by an outside object. Starts off the events to fill the buffer
     * with a goodbye message.
     *
     * @param name the name of the object we are saying goodbye to.
     */
    void sayGoodbye(std::string name);
};

#endif // __LEARNING_GEM5_GOODBYE_OBJECT_HH__
#include "learning_gem5/part2/goodbye_object.hh"

#include "base/trace.hh"
#include "debug/Hello.hh"
#include "sim/sim_exit.hh"

GoodbyeObject::GoodbyeObject(const GoodbyeObjectParams &params) :
    SimObject(params), event(*this), bandwidth(params.write_bandwidth),
    bufferSize(params.buffer_size), buffer(nullptr), bufferUsed(0)
{
    buffer = new char[bufferSize];
    DPRINTF(Hello, "Created the goodbye object\n");
}

GoodbyeObject::~GoodbyeObject()
{
    delete[] buffer;
}

void
GoodbyeObject::processEvent()
{
    DPRINTF(Hello, "Processing the event!\n");
    fillBuffer();
}

void
GoodbyeObject::sayGoodbye(std::string other_name)
{
    DPRINTF(Hello, "Saying goodbye to %s\n", other_name);

    message = "Goodbye " + other_name + "!! ";

    fillBuffer();
}

void
GoodbyeObject::fillBuffer()
{
    // There better be a message
    assert(message.length() > 0);

    // Copy from the message to the buffer per byte.
    int bytes_copied = 0;
    for (auto it = message.begin();
         it < message.end() && bufferUsed < bufferSize - 1;
         it++, bufferUsed++, bytes_copied++) {
        // Copy the character into the buffer
        buffer[bufferUsed] = *it;
    }

    if (bufferUsed < bufferSize - 1) {
        // Wait for the next copy for as long as it would have taken
        DPRINTF(Hello, "Scheduling another fillBuffer in %d ticks\n",
                bandwidth * bytes_copied);
        schedule(event, curTick() + bandwidth * bytes_copied);
    } else {
        DPRINTF(Hello, "Goodbye done copying!\n");
        // Be sure to take into account the time for the last bytes
        exitSimLoop(buffer, 0, curTick() + bandwidth * bytes_copied);
    }
}

このGoodbyeObjectへのインターフェースは、文字列をパラメータとして受け取る関数sayGoodbyeというシンプルなものだ。この関数が呼ばれると、シミュレーターはメッセージを構築し、メンバ変数に保存する。それから、バッファへの充填を開始する。

限られた帯域幅をモデル化するために、メッセージをバッファに書き込むたびに、メッセージの書き込みにかかる待ち時間を一時停止する。この一時停止をモデル化するために、単純なイベントを使用する。

SimObject宣言でMemoryBandwidthパラメータを使用したので、帯域幅変数は自動的に1バイトあたりのティックに変換され、待ち時間の計算は単純に帯域幅×バッファに書き込むバイト数となる。

最後に、バッファがいっぱいになったら、シミュレーションを終了する関数exitSimLoopを呼び出す。1つ目はPythonの設定スクリプトに返すメッセージ(exit_event.getCause())、2つ目は終了コード、3つ目はいつ終了するかである。

HelloObjectのパラメータとしてGoodbyeObjectを追加する

まず、GoodbyeObjectをHelloObjectのパラメータとして追加する。これには、ParamTypeNameにSimObjectのクラス名を指定するだけだ。通常のパラメータと同じように、デフォルトを指定することもできるし、指定しないこともできる。

class HelloObject(SimObject):
    type = 'HelloObject'
    cxx_header = "learning_gem5/part2/hello_object.hh"

    time_to_wait = Param.Latency("Time before firing the event")
    number_of_fires = Param.Int(1, "Number of times to fire the event before "
                                   "goodbye")

    goodbye_object = Param.GoodbyeObject("A goodbye object")

次に、GoodbyeObjectへの参照をHelloObjectクラスに追加する。hello_object.hhファイルの先頭にgoodbye_object.hhをインクルードするのを忘れないように!

#include <string>

#include "learning_gem5/part2/goodbye_object.hh"
#include "params/HelloObject.hh"
#include "sim/sim_object.hh"

class HelloObject : public SimObject
{
  private:
    void processEvent();

    EventWrapper event;

    /// Pointer to the corresponding GoodbyeObject. Set via Python
    GoodbyeObject* goodbye;

    /// The name of this object in the Python config file
    const std::string myName;

    /// Latency between calling the event (in ticks)
    const Tick latency;

    /// Number of times left to fire the event before goodbye
    int timesLeft;

  public:
    HelloObject(const HelloObjectParams &p);

    void startup();
};

次に、HelloObjectのコンストラクタとprocessイベント関数を更新する必要がある。また、コンストラクタにgoodbyeポインタが有効かどうかのチェックを追加する。Pythonの特殊なSimObjectであるNULLを使うことで、パラメータを通してSimObjectとしてNULLポインタを渡すことができる。これはこのオブジェクトが受け入れるようにコード化されていないため、このような場合はパニックになるべきである。

#include "learning_gem5/part2/hello_object.hh"

#include "debug/Hello.hh"

HelloObject::HelloObject(HelloObjectParams &params) :
    SimObject(params),
    event(*this),
    goodbye(params.goodbye_object),
    myName(params.name),
    latency(params.time_to_wait),
    timesLeft(params.number_of_fires)
{
    DPRINTF(Hello, "Created the hello object with the name %s\n", myName);
    panic_if(!goodbye, "HelloObject must have a non-null GoodbyeObject");
}

パラメータで指定した数のイベントを処理したら、GoodbyeObjectsayGoodbye関数を呼び出そう。

void
HelloObject::processEvent()
{
    timesLeft--;
    DPRINTF(Hello, "Hello world! Processing the event! %d left\n", timesLeft);

    if (timesLeft <= 0) {
        DPRINTF(Hello, "Done firing!\n");
        goodbye->sayGoodbye(myName);
    } else {
        schedule(event, curTick() + latency);
    }
}

configスクリプトのアップデート

最後に、GoodbyeObjectをコンフィグスクリプトに追加する必要がある。新しいコンフィグスクリプトhello_goodbye.pyを作成し、helloオブジェクトとgoodbyeオブジェクトの両方をインスタンス化する。例えば、以下のようなスクリプトが考えられる。

import m5
from m5.objects import *

root = Root(full_system = False)

root.hello = HelloObject(time_to_wait = '2us', number_of_fires = 5)
root.hello.goodbye_object = GoodbyeObject(buffer_size='100B')

m5.instantiate()

print("Beginning simulation!")
exit_event = m5.simulate()
print('Exiting @ tick %i because %s' % (m5.curTick(), exit_event.getCause()))

このスクリプトを実行すると、以下の出力が生成される。

gem5 Simulator System.  http://gem5.org
gem5 is copyrighted software; use the --copyright option for details.

gem5 compiled Jan  4 2017 15:17:14
gem5 started Jan  4 2017 15:18:41
gem5 executing on chinook, pid 3838
command line: build/X86/gem5.opt --debug-flags=HelloExample configs/learning_gem5/part2/hello_goodbye.py

Global frequency set at 1000000000000 ticks per second
      0: hello.goodbye_object: Created the goodbye object
      0: hello: Created the hello object
Beginning simulation!
info: Entering event queue @ 0.  Starting simulation...
2000000: hello: Hello world! Processing the event! 4 left
4000000: hello: Hello world! Processing the event! 3 left
6000000: hello: Hello world! Processing the event! 2 left
8000000: hello: Hello world! Processing the event! 1 left
10000000: hello: Hello world! Processing the event! 0 left
10000000: hello: Done firing!
10000000: hello.goodbye_object: Saying goodbye to hello
10000000: hello.goodbye_object: Scheduling another fillBuffer in 152592 ticks
10152592: hello.goodbye_object: Processing the event!
10152592: hello.goodbye_object: Scheduling another fillBuffer in 152592 ticks
10305184: hello.goodbye_object: Processing the event!
10305184: hello.goodbye_object: Scheduling another fillBuffer in 152592 ticks
10457776: hello.goodbye_object: Processing the event!
10457776: hello.goodbye_object: Scheduling another fillBuffer in 152592 ticks
10610368: hello.goodbye_object: Processing the event!
10610368: hello.goodbye_object: Scheduling another fillBuffer in 152592 ticks
10762960: hello.goodbye_object: Processing the event!
10762960: hello.goodbye_object: Scheduling another fillBuffer in 152592 ticks
10915552: hello.goodbye_object: Processing the event!
10915552: hello.goodbye_object: Goodbye done copying!
Exiting @ tick 10944163 because Goodbye hello!! Goodbye hello!! Goodbye hello!! Goodbye hello!! Goodbye hello!! Goodbye hello!! Goo

これら2つのSimObjectのパラメータを変更し、全体の実行時間(Exiting @ tick 10944163)がどのように変化するかを見ることができる。これらのテストを実行するには、ターミナルへの出力が少なくなるようにデバッグフラグを外すとよい。

次の章では、より複雑で有用なSimObjectを作成し、最後に単純なブロッキングユニプロセッサキャッシュを実装する。