Learn gem5 #4 (Event-driven programming)

だいぶ空いてしまった。

Event-driven programming

ソースを編集。

--- ../backup/1/src/learning_gem5/part2/hello_object.hh 2024-04-03 22:02:25.385861133 +0900
+++ src/learning_gem5/part2/hello_object.hh     2024-04-03 22:04:29.235837762 +0900
@@ -9,8 +9,12 @@

 class HelloObject : public SimObject
 {
+  private:
+    void processEvent();
+    EventFunctionWrapper event;
   public:
     HelloObject(const HelloObjectParams &p);
+    void startup() override;
 };

 } // namespace gem5

--- ../backup/1/src/learning_gem5/part2/hello_object.cc 2024-03-24 16:17:08.087853809 +0900
+++ src/learning_gem5/part2/hello_object.cc     2024-04-03 22:06:04.745818640 +0900
@@ -8,9 +8,19 @@
 {

 HelloObject::HelloObject(const HelloObjectParams &params) :
-    SimObject(params)
+    SimObject(params), event([this]{processEvent();}, name())
 {
     DPRINTF(HelloExample, "Created the hello object\n");
 }

+void HelloObject::processEvent()
+{
+  DPRINTF(HelloExample, "Hello world! Processing the event!\n");
+}
+
+void HelloObject::startup()
+{
+  schedule(event, 100);
+}
+
 } // namespace gem5

実行する。

$ scons build/X86/gem5.opt -j20
$ build/X86/gem5.opt --debug-flags=HelloExample configs/learning_gem5/part2/run_hello.py

以下のメッセージが出る。

    100: hello: Hello world! Processing the event!

さらに変更を加える。

--- ../backup/2/src/learning_gem5/part2/hello_object.hh 2024-04-03 22:17:05.645691333 +0900
+++ src/learning_gem5/part2/hello_object.hh     2024-04-03 22:17:41.355683887 +0900
@@ -12,6 +12,8 @@
   private:
     void processEvent();
     EventFunctionWrapper event;
+    const Tick latency;
+    int timesLeft;
   public:
     HelloObject(const HelloObjectParams &p);
     void startup() override;

--- ../backup/2/src/learning_gem5/part2/hello_object.cc 2024-04-03 22:17:10.005690492 +0900
+++ src/learning_gem5/part2/hello_object.cc     2024-04-03 22:19:40.035660455 +0900
@@ -8,19 +8,27 @@
 {

 HelloObject::HelloObject(const HelloObjectParams &params) :
-    SimObject(params), event([this]{processEvent();}, name())
+    SimObject(params), event([this]{processEvent();}, name()),
+    latency(100), timesLeft(10)
 {
     DPRINTF(HelloExample, "Created the hello object\n");
 }

 void HelloObject::processEvent()
 {
-  DPRINTF(HelloExample, "Hello world! Processing the event!\n");
+  timesLeft--;
+  DPRINTF(HelloExample, "Hello world! Processing the event! %d left\n", timesLeft);
+
+  if (timesLeft <= 0) {
+    DPRINTF(HelloExample, "Done firing!\n");
+  } else {
+    schedule(event, curTick() + latency);
+  }
 }

 void HelloObject::startup()
 {
-  schedule(event, 100);
+  schedule(event, latency);
 }

 } // namespace gem5


実行する。

$ scons build/X86/gem5.opt -j20
$ build/X86/gem5.opt --debug-flags=HelloExample configs/learning_gem5/part2/run_hello.py

以下のメッセージが出る。

src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
    100: hello: Hello world! Processing the event! 9 left
    200: hello: Hello world! Processing the event! 8 left
    300: hello: Hello world! Processing the event! 7 left
    400: hello: Hello world! Processing the event! 6 left
    500: hello: Hello world! Processing the event! 5 left
    600: hello: Hello world! Processing the event! 4 left
    700: hello: Hello world! Processing the event! 3 left
    800: hello: Hello world! Processing the event! 2 left
    900: hello: Hello world! Processing the event! 1 left
   1000: hello: Hello world! Processing the event! 0 left
   1000: hello: Done firing!

Learn gem5 #3 (Creating a very simple SimObject, Debugging gem5)

Creating a very simple SimObject

https://www.gem5.org/documentation/learning_gem5/part2/helloobject/

とりあえず書いてあるとおりに作業を進める。
バックアップしてスクラッチから書いていく。

$ git checkout -b hello-simobject
$ mkdir -p backup/src/learning_gem5
$ mv src/learning_gem5/part2 backup/src/learning_gem5
$ mkdir -p backup/configs/learning_gem5
$ mv configs/learning_gem5/part2 backup/configs/learning_gem5
$ mkdir -p src/learning_gem5/part2
$ mkdir -p configs/learning_gem5/part2

src/learning_gem5/part2/HelloObject.py

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

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

src/learning_gem5/part2/hello_object.hh

#ifndef __LEARNING_GEM5_HELLO_OBJECT_HH__
#define __LEARNING_GEM5_HELLO_OBJECT_HH__

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

namespace gem5
{

class HelloObject : public SimObject
{
  public:
    HelloObject(const HelloObjectParams &p);
};

} // namespace gem5

#endif // __LEARNING_GEM5_HELLO_OBJECT_HH__

src/learning_gem5/part2/hello_object.cc

#include "learning_gem5/part2/hello_object.hh"

#include <iostream>

namespace gem5
{

HelloObject::HelloObject(const HelloObjectParams &params) :
    SimObject(params)
{
    std::cout << "Hello World! From a SimObject!" << std::endl;
}

} // namespace gem5

src/learning_gem5/part2/SConscript

Import('*')

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

なんと、再ビルドが必要!!キャッシュがあっても -j20 オプションで2分ぐらいかかる。

$ scons build/X86/gem5.opt -j20

次に構成スクリプトの作成
configs/learning_gem5/part2/run_hello.py

import m5
from m5.objects import *

root = Root(full_system = False)
root.hello = HelloObject()
m5.instantiate()

print("Beginning simulation!")
exit_event = m5.simulate()
print('Exiting @ tick {} because {}'
      .format(m5.curTick(), exit_event.getCause()))

実行

$ build/X86/gem5.opt configs/learning_gem5/part2/run_hello.py

実行できたようだ。

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

gem5 version 23.1.0.0
gem5 compiled Mar 24 2024 16:00:21
gem5 started Mar 24 2024 16:04:37
gem5 executing on DESKTOP-1L01RQ8, pid 3549
command line: build/X86/gem5.opt configs/learning_gem5/part2/run_hello.py

Global frequency set at 1000000000000 ticks per second
warn: No dot file generated. Please install pydot to generate the dot file and pdf.
Hello World! From a SimObject!
Beginning simulation!
src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
Exiting @ tick 18446744073709551615 because simulate() limit reached

Debugging gem5

Using debug flags

DRAMデバッグログを見る

$ build/X86/gem5.opt --debug-flags=DRAM configs/learning_gem5/part1/simple.py | head -n 50

CPUのデバッグログを見る。

$ build/X86/gem5.opt --debug-flags=Exec configs/learning_gem5/part1/simple.py | head -n 50

Execのデバッグフラグはいくつかのデバッグフラグの集合になっている。詳細は以下で確認できる。

$ build/X86/gem5.opt --debug-help

Adding a new debug flag

カスタムのデバッグフラグを追加できるらしい。

src/learning_gem5/part2/SConscript に以下を追加。

DebugFlag('HelloExample')

これで自動的にデバッグ用のヘッダファイルが追加される。これをインクルードする。
src/learning_gem5/part2/hello_object.cc

#include "base/trace.hh"
#include "debug/HelloExample.hh"
-    std::cout << "Hello World! From a SimObject!" << std::endl;
+    DPRINTF(HelloExample, "Created the hello object\n");

ビルドして実行。

$ scons build/X86/gem5.opt -j20
$ build/X86/gem5.opt --debug-flags=HelloExample configs/learning_gem5/part2/run_hello.py

実行できた。

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

gem5 version 23.1.0.0
gem5 compiled Mar 24 2024 16:17:31
gem5 started Mar 24 2024 16:19:16
gem5 executing on DESKTOP-1L01RQ8, pid 3713
command line: build/X86/gem5.opt --debug-flags=HelloExample configs/learning_gem5/part2/run_hello.py

Global frequency set at 1000000000000 ticks per second
warn: No dot file generated. Please install pydot to generate the dot file and pdf.
      0: hello: Created the hello object
Beginning simulation!
src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
Exiting @ tick 18446744073709551615 because simulate() limit reached

Debug output

DPRINTFは以下を出力する。

  • 現在時刻の tick 値
  • DPRINTF呼び出し元のSimObject名。通常Pythonでの変数名だが、SimObjectのname()関数の戻り値を出力。
  • 書式化済みメッセージ文字列

デバッグ出力先は --debug-file オプションで制御できる。デフォルトは stdout。

Using functions other than DPRINTF

DPRINTF以外にもデバッグ関数はいろいろある。リファレンスを参照。
https://doxygen.gem5.org/release/current/base_2trace_8hh.html

opt, debug ビルド以外はデバッグ出力は無効化されるのでデバッグ出力をしたい場合は gem5.opt, gem5.debug ファイルを実行する。

Learn gem5 #2 (Creating a simple configuration script~Extending gem5 for ARM)

Creating a simple configuration script

https://www.gem5.org/documentation/learning_gem5/part1/simple_config/

まずは動かしてみる。

$ mkdir -p configs/tutorial/part1/
$ cp configs/learning_gem5/part1/simple.py configs/tutorial/part1/
$ build/X86/gem5.opt configs/tutorial/part1/simple.py

動くようだ。

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

gem5 version 23.1.0.0
gem5 compiled Mar 16 2024 23:40:41
gem5 started Mar 23 2024 13:54:24
gem5 executing on DESKTOP-1L01RQ8, pid 18841
command line: build/X86/gem5.opt configs/tutorial/part1/simple.py

Global frequency set at 1000000000000 ticks per second
warn: No dot file generated. Please install pydot to generate the dot file and pdf.
src/mem/dram_interface.cc:690: warn: DRAM device capacity (8192 Mbytes) does not match the address range assigned (512 Mbytes)
src/base/statistics.hh:279: warn: One of the stats is a legacy stat. Legacy stat is a stat that does not belong to any statistics::Group. Legacy stat is deprecated.
system.remote_gdb: Listening for connections on port 7000
Beginning simulation!
src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
Hello world!
Exiting @ tick 499308000 because exiting with last active thread context

このとき、m5outというログディレクトリができているので、念のためバックアップしておく。

$ mv m5out m5out.1

記事の内容については、Pythonのコードを追っていけばわかる話が多く、割愛する。
要するに、下図のようなシステムを構築している。

注記事項としては、PIOと割り込みを接続する必要があるのはx86の要件で、ほかのCPUでは省略できる点ぐらいであろうか。
あとは、SEモードとFSモードの話もしているが、今後詳細な説明が出てくると予想・・・

Adding cache to the configuration script

前回のシステムにキャッシュを付けるという例題になっている。

キャッシュのモデル化には2つの手法がある。

  • クラシックキャッシュ
  • Ruby

なぜ2つあるかというと、もともとgem5はm5とGEMSが組み合わさったものであり、クラシックキャッシュはm5由来のものであり、RubyはGEMS由来のものであるからである。キャッシュコヒーレンスプロトコルの違いが重要な場合はRubyを使うべきだが、それ以外の場合はクラシックキャッシュで十分である。

実際の実装についてはPythonコードを追っていけば記事の内容もわかるレベルだと思われる。
動かしてみる。

$ cp configs/learning_gem5/part1/caches.py configs/tutorial/part1/
$ cp configs/learning_gem5/part1/two_level.py configs/tutorial/part1/
$ build/X86/gem5.opt configs/tutorial/part1/two_level.py --l2_size='1MB' --l1d_size='128kB'

動いた。

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

gem5 version 23.1.0.0
gem5 compiled Mar 16 2024 23:40:41
gem5 started Mar 23 2024 14:07:50
gem5 executing on DESKTOP-1L01RQ8, pid 18885
command line: build/X86/gem5.opt configs/tutorial/part1/two_level.py --l2_size=1MB --l1d_size=128kB

Global frequency set at 1000000000000 ticks per second
warn: No dot file generated. Please install pydot to generate the dot file and pdf.
src/mem/dram_interface.cc:690: warn: DRAM device capacity (8192 Mbytes) does not match the address range assigned (512 Mbytes)
src/base/statistics.hh:279: warn: One of the stats is a legacy stat. Legacy stat is a stat that does not belong to any statistics::Group. Legacy stat is deprecated.
system.remote_gdb: Listening for connections on port 7000
Beginning simulation!
src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
Hello world!
Exiting @ tick 57774000 because exiting with last active thread context

キャッシュを追加したことでシミュレーション時間が 499308000 から 57774000 に減っている。

念のためログディレクトリをバックアップしておく。

$ mv m5out m5out.2

Understanding gem5 statistics and output

gem5実行完了後、m5outディレクトリに以下のファイルができる。

  • config.ini: シミュレーション中に作られた全SimObjectのリストとそのパラメータ値
  • config.json: config.iniのJSON形式
  • stats.txt: シミュレーション中に登録された全m5統計のテキスト表現

config.ini

SimObject名は[system.membus]のように[]で囲われたテキストで表現。
よくわからないが、1 tick は 1ps に相当し、クロックを1GHzに設定したので、1クロックサイクルは 1000 ticks に設定されたらしい。

[system.clk_domain]
clock=1000

stats.txt

詳細はgem5 wiki siteに書いてあると言っているがリンク切れになっている。。。
代表的なものとして、以下がある。

  • simSeconds シミュレーション時間(秒)
  • simInsts CPUで実行された命令数
  • hostInstRate シミュレーションのパフォーマンス(1秒当たりの命令実行数)

SimObjectの統計も出力されている。syscall数、キャッシュやtranslation buffer(?)の統計など。ファイルの後半はメモリコントローラの統計も出ている。それぞれのコンポーネントのreadバイト数や平均帯域など。

Using the default configuration scripts

今まではスクラッチから構成スクリプトを書いていたが、デフォルトの構成スクリプトを使うこともできる。

ディレクトリ構成

configs/ 以下にいろいろ入っている。

boot/

FSモードで使用される rcS ファイルが入っている。これらのファイルはLinux起動後にシミュレータによってロードされ、シェルによって実行される。これらのほとんどはFSモード実行中にベンチマークを制御するために使われる。hack_back_ckpt.rcS などのユーティリティ関数もあり、これらはFSシミュレーションの章で詳しく説明される。

common/

このディレクトリはシミュレーションシステムを作る際に使われるたくさんのヘルパースクリプト・ヘルパー関数が入っている。たとえば、Caches.pyは以前のチュートリアルで作ったcaches.pyやcaches_opts.pyに似たものである。

Options.pyはコマンドラインで設定されるさまざまなオプションが含まれる。CPU数、システムクロックなどなど。使いたいコマンドラインパラメータがあるときにすでに含まれていないか事前に確認すると良い。

CacheConfig.py はクラシックメモリシステムのキャッシュのオプションと機能が含まれる。

MemConfig.py はメモリシステム設定のためのヘルパー関数が含まれる。

FSConfig.py は様々なシステムをFSシミュレーションするための必須な関数が含まれる。FSシミュレーションについては独立した章で議論する。

Simulation.pyはgem5を動かしたり設定するための様々なヘルパー関数を含む。チェックポイントの保存や回復など。examples/以下の構成ファイルはこのファイルを使う。このファイルは非常に複雑だが、使うことで柔軟性を高める。

dram/

DRAMテストのスクリプト

examples/

例題となる構成スクリプトが入っている。特に se.py と fs.py は有用であり、次のセクションで解説する。

learning_gem5/

learning_gem5 book のスクリプト

network/

HeteroGarnet(?)ネットワークのスクリプト
補注:どうやらOn-chipインタコネクトやNoC関連のものらしい?

nvm/

NVMインタフェースの例

ruby/

Rubyで作られたキャッシュコヒーレンスプロトコル。詳細はRubyの章で解説。

splash2/

splash2ベンチマークスイートのスクリプト

topologies/

Rubyキャッシュ階層のトポロジ実装。詳細はRubyの章。

Using se.py and fs.py

ここではse.py, fs.pyの共通オプションを見ていく。オプションの多くは Options.py の addCommonOptions で登録される。全オプションを見るには --help を使ったり、スクリプトのコードを読めばいい。

以下を実行してみる。

$ build/X86/gem5.opt configs/example/se.py --cmd=tests/test-progs/hello/bin/x86/linux/hello

予想外の出力が出てきた・・・。将来的に廃止されるらしい?

fatal: The 'configs/example/se.py' script has been deprecated. It can be found in 'configs/deprecated/example' if required. Its usage should be avoided as it will be removed in future releases of gem5.

とりあえず、スクリプトのパスを変えて実行してみる。

$ build/X86/gem5.opt configs/deprecated/example/se.py --cmd=tests/test-progs/hello/bin/x86/linux/hello

実行できたみたいだ。

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

gem5 version 23.1.0.0
gem5 compiled Mar 16 2024 23:40:41
gem5 started Mar 23 2024 17:39:43
gem5 executing on DESKTOP-1L01RQ8, pid 18954
command line: build/X86/gem5.opt configs/deprecated/example/se.py --cmd=tests/test-progs/hello/bin/x86/linux/hello

warn: The se.py script is deprecated. It will be removed in future releases of  gem5.
Global frequency set at 1000000000000 ticks per second
warn: No dot file generated. Please install pydot to generate the dot file and pdf.
src/mem/dram_interface.cc:690: warn: DRAM device capacity (8192 Mbytes) does not match the address range assigned (512 Mbytes)
src/base/statistics.hh:279: warn: One of the stats is a legacy stat. Legacy stat is a stat that does not belong to any statistics::Group. Legacy stat is deprecated.
system.remote_gdb: Listening for connections on port 7000
**** REAL SIMULATION ****
src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
Hello world!
Exiting @ tick 5943000 because exiting with last active thread context

しかし、このチュートリアルの作者的には全く面白くないらしい。アトミックCPU、アトミックメモリアクセスがデフォルトなので実際のタイミング情報が得られないらしい。このことは m5out/config.ini を見ると確認できる。オプションを変えて実行してみる。

$ build/X86/gem5.opt configs/deprecated/example/se.py --cmd=tests/test-progs/hello/bin/x86/linux/hello --cpu-type=TimingSimpleCPU --l1d_size=64kB --l1i_size=16kB
gem5 Simulator System.  https://www.gem5.org
gem5 is copyrighted software; use the --copyright option for details.

gem5 version 23.1.0.0
gem5 compiled Mar 16 2024 23:40:41
gem5 started Mar 23 2024 17:42:16
gem5 executing on DESKTOP-1L01RQ8, pid 18955
command line: build/X86/gem5.opt configs/deprecated/example/se.py --cmd=tests/test-progs/hello/bin/x86/linux/hello --cpu-type=TimingSimpleCPU --l1d_size=64kB --l1i_size=16kB

warn: The se.py script is deprecated. It will be removed in future releases of  gem5.
Global frequency set at 1000000000000 ticks per second
warn: No dot file generated. Please install pydot to generate the dot file and pdf.
src/mem/dram_interface.cc:690: warn: DRAM device capacity (8192 Mbytes) does not match the address range assigned (512 Mbytes)
src/base/statistics.hh:279: warn: One of the stats is a legacy stat. Legacy stat is a stat that does not belong to any statistics::Group. Legacy stat is deprecated.
system.remote_gdb: Listening for connections on port 7000
**** REAL SIMULATION ****
src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
Hello world!
Exiting @ tick 454646000 because exiting with last active thread context

m5out/config.ini のcacheを見てみるとキャッシュが作成されてないことがわかる。キャッシュサイズを指定したところでキャッシュを使用することを指定しないとだめらしい。--caches オプションを追加する。

$ build/X86/gem5.opt configs/deprecated/example/se.py --cmd=tests/test-progs/hello/bin/x86/linux/hello --cpu-type=TimingSimpleCPU --l1d_size=64kB --l1i_size=16kB --caches
gem5 Simulator System.  https://www.gem5.org
gem5 is copyrighted software; use the --copyright option for details.

gem5 version 23.1.0.0
gem5 compiled Mar 16 2024 23:40:41
gem5 started Mar 23 2024 17:44:53
gem5 executing on DESKTOP-1L01RQ8, pid 18956
command line: build/X86/gem5.opt configs/deprecated/example/se.py --cmd=tests/test-progs/hello/bin/x86/linux/hello --cpu-type=TimingSimpleCPU --l1d_size=64kB --l1i_size=16kB --caches

warn: The se.py script is deprecated. It will be removed in future releases of  gem5.
Global frequency set at 1000000000000 ticks per second
warn: No dot file generated. Please install pydot to generate the dot file and pdf.
src/mem/dram_interface.cc:690: warn: DRAM device capacity (8192 Mbytes) does not match the address range assigned (512 Mbytes)
src/base/statistics.hh:279: warn: One of the stats is a legacy stat. Legacy stat is a stat that does not belong to any statistics::Group. Legacy stat is deprecated.
system.remote_gdb: Listening for connections on port 7000
**** REAL SIMULATION ****
src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
Hello world!
Exiting @ tick 31680000 because exiting with last active thread context

キャッシュが入ったことで実行時間が早くなり、454646000 ticks から 31680000 に減った。このように、常に config.ini を確認することは重要である。

Some common options se.py and fs.py

 --help で全オプションを確認できる。重要なオプションは以下の通り。

--cpu-type=CPU_TYPE
--sys-clock=SYS_CLOCK
--cpu-clock=CPU_CLOCK
--mem-type=MEM_TYPE
--caches
--l2cache
--ruby
-m TICKS, --abs-max-tick=TICKS
-I MAXINSTS, --maxinsts=MAXINSTS
-c CMD, --cmd=CMD
-o OPTIONS, --options=OPTIONS
--output=OUTPUT
--errout=ERROUT

Extending gem5 for ARM

まずは指示通りにビルド。ビルドには30分弱かかる。。。。

$ mkdir -p cpu_tests/benchmarks/bin/arm
$ cd cpu_tests/benchmarks/bin/arm
$ wget dist.gem5.org/dist/v22-0/test-progs/cpu-tests/bin/arm/Bubblesort
$ wget dist.gem5.org/dist/v22-0/test-progs/cpu-tests/bin/arm/FloatMM
$ cd -
$ scons build/ARM/gem5.opt -j 20

Modifying simple.py to run ARM Binaries

$ cp configs/tutorial/part1/simple.py configs/tutorial/part1/simple_arm.py
$ vi configs/tutorial/part1/simple_arm.py

コメントアウトする。

#system.cpu.interrupts[0].pio = system.membus.mem_side_ports
#system.cpu.interrupts[0].int_requestor = system.membus.cpu_side_ports
#system.cpu.interrupts[0].int_responder = system.membus.mem_side_ports

バイナリの指定。binary変数はほかでも使われてるので、cmd.processの方を直接書き換えるとエラーが出る。

binary = os.path.join(
    thispath,
    "../../../",
    #"tests/test-progs/hello/bin/x86/linux/hello",
    "tests/test-progs/hello/bin/arm/linux/hello",
)

実行する。

build/ARM/gem5.opt configs/tutorial/part1/simple_arm.py

なんかエラーが出る。

NameError: name 'X86TimingSimpleCPU' is not defined

CPUの指定を書き換えて再度実行。。。

#system.cpu = X86TimingSimpleCPU()
system.cpu = ArmTimingSimpleCPU()
gem5 Simulator System.  https://www.gem5.org
gem5 is copyrighted software; use the --copyright option for details.

gem5 version 23.1.0.0
gem5 compiled Mar 23 2024 18:16:56
gem5 started Mar 23 2024 18:33:38
gem5 executing on DESKTOP-1L01RQ8, pid 5640
command line: build/ARM/gem5.opt configs/tutorial/part1/simple_arm.py

Global frequency set at 1000000000000 ticks per second
warn: No dot file generated. Please install pydot to generate the dot file and pdf.
src/mem/dram_interface.cc:690: warn: DRAM device capacity (8192 Mbytes) does not match the address range assigned (512 Mbytes)
src/base/statistics.hh:279: warn: One of the stats is a legacy stat. Legacy stat is a stat that does not belong to any statistics::Group. Legacy stat is deprecated.
system.remote_gdb: Listening for connections on port 7000
Beginning simulation!
src/sim/simulate.cc:199: info: Entering event queue @ 0.  Starting simulation...
Hello world!
Exiting @ tick 374532000 because exiting with last active thread context

なんかWarningが出るが、とりあえずは実行できてるっぽい。

ARM Full System Simulation

動かしてみたもののうまくいかなかった。

$ cd util/term/
$ make
$ cd -
$ mkdir -p fs_image
$ cd fs_image
$ wget http://dist.gem5.org/dist/v22-0/arm/aarch-system-20220707.tar.bz2
$ tar jxvf aarch-system-20220707.tar.bz2
$ cd -

絶対パスの指定

$ export IMG_ROOT=/absolute/path/to/fs_images

実行。何を指定したらいいのかわからないので、適当に指定した。

$ build/ARM/gem5.opt configs/example/arm/fs_bigLITTLE.py --caches --bootloader="$IMG_ROOT/binaries/boot.arm" --ke
rnel="$IMG_ROOT/binaries/vmlinux.arm" --disk="$IMG_ROOT/disks/m5_exit.squashfs.arm" --bootscript=configs/boot/bbench-gb.rcS

なんか動いてはいるっぽいが、よくわからない・・・。おそらく動かし方が間違っているのだろう。
今のところARMのFSシミュレーションについてモチベーションがないため実験はここまでにしておく。

Learn gem5 #1 (Introduction~Building gem5)

今日からは gem5 を触ってみる。
https://en.wikipedia.org/wiki/Gem5
www.gem5.org

Wikipedia によると、gem5 は m5 (CPUシミュレーションフレームワーク)と GEMS (メモリタイミングシミュレータ) が合体したものであり、複数の実行モードを持つイベント駆動型のシミュレータのようだ。
gem5 - Wikipedia

今回この日記で gem5 を取り上げるモチベーションは、

  1. SystemC と同様に抽象度の高いレベルのシミュレーションができる。
  2. SystemCモジュールをgem5で動かしたり、逆にgem5モジュールをSystemCで動かすこともできる。

https://old.gem5.org/wiki/images/a/a5/Summit2017_systemc.pdf

SystemCは gem5 version 20.0 で正式に取り込まれているようだ。
https://arxiv.org/pdf/2007.03152.pdf

Github上では ext/systemc 以下にSystemC 2.3.1 が丸ごと取り込まれている。
BoostをC++11で置き換えるなどの修正が入っているらしい。
https://github.com/gem5/gem5/tree/stable/ext/systemc

Learn gem5を最初から見ていき、今日はインストールまでを行う。
長いので要所要所を訳していく。

Introduction

https://www.gem5.org/documentation/learning_gem5/introduction/

What is gem5

gem5はモジュール型離散イベント駆動コンピュータシステムシミュレーションプラットフォームである。すなわち、

  1. gem5コンポーネントの再配置・パラメータ化・拡張・置換が必要に応じて容易にできる。
  2. 一連のイベントの時間経過のシミュレーションができる。
  3. 一つもしくは複数のコンピュータシステムを様々な方法でシミュレーションするために使う
  4. 単なるシミュレータにとどまらない。既製のコンポーネントを使用して独自のシミュレーションシステムを構築できるシミュレータプラットフォームである。

gem5は主にC++Pythonで記述されており、ほとんどのコンポーネントBSDライセンスで提供されている。
コンピュータシステムをデバイスとOSを含んだフルシステムモード(FSモード)でシミュレーションしたり、ユーザスペースプログラムをsyscall エミュレーションモード(SEモード)でシステムサービスをシミュレータから提供してシミュレーションすることもできる。
様々なレベルでAlpha, ARM, MIPS, Power, SPARC, RISC-V, x86_64バイナリを2つの単純なCPIモデル、OoOモデル、in orderパイプラインモデルで実行できる。
メモリシステムは柔軟に構築でき、キャッシュ、クロスバー、あるいはさらなる柔軟なメモリシステムモデリングのためにRubyシミュレータも使うことができる。

Building gem5

https://www.gem5.org/documentation/learning_gem5/part1/building/

Requirements for gem5

どうやら pyenv 環境ではうまくインストールできないので、あらかじめ pyenv を削除する(.bashrcから pyenv init などを削除)。
その後、必要そうなものをapt installする。

$ sudo apt install build-essential git m4 scons zlib1g zlib1g-dev libprotobuf-dev protobuf-compiler libprotoc-dev libgoogle-perftools-dev python3 python3-dev
$ sudo apt install libboost-all-dev

Getting the code

$ git clone https://github.com/gem5/gem5
$ cd gem5
$ pip3 install -r requirements.txt
$ python3 `which scons` build/X86/gem5.opt -j9
scons: Reading SConscript files ...

You're missing the pre-commit/commit-msg hooks. These hook help to ensure your
code follows gem5's style rules on git commit and your commit messages follow
our commit message requirements. This script will now install these hooks in
your .git/hooks/ directory.
Press enter to continue, or ctrl-c to abort:

エンターを入力するとビルドが始まる。ビルドは途方もなく時間がかかる。。。
いくつかWarningが出ているが、いったん無視する。

scons: done building targets.
*** Summary of Warnings ***
Warning: Header file <capstone/capstone.h> not found.
         This host has no capstone library installed.
Warning: Can not enable KVM, host seems to lack KVM support
Warning: Couldn't find HDF5 C++ libraries. Disabling HDF5 support.

実行ができるかどうかだけ確認する。どうやらちゃんとビルドできているようだ。

$ build/X86/gem5.opt
gem5 Simulator System.  https://www.gem5.org
gem5 is copyrighted software; use the --copyright option for details.

gem5 version 23.1.0.0
gem5 compiled Mar 16 2024 23:40:41
gem5 started Mar 16 2024 23:44:44
gem5 executing on DESKTOP-1L01RQ8, pid 18613
command line: build/X86/gem5.opt
続きを読む

Getting Started with TLM-2.0 (Tutorial 3 - Routing Methods through Interconnect Components)

www.doulos.com

Introduction

ここではインターコネクトコンポーネントを通じてトランザクションが伝搬していくのを見ていく。すなわち、ルーターがイニシエータといくつかのターゲットメモリの間に配置される。
ルータートランザクション、DMI、デバッグトランザクションをターゲットに転送する必要があり、関数呼び出しやトランザクションがイニシエータに戻っていくリターンパスも管理する必要がある。
ここでは2つの課題に取り組む必要がある。
・1つのターゲットソケットと複数のイニシエータソケットを持つインターコネクトコンポーネントをどのようにモデリングするか?
ルーターを介してfoward/backwardトランザクションでアドレスをどのように処理するか?

An Interconnect Component

イニシエータとターゲットメモリはTutorial 2の例ととても似ているが、ここではそれらの間にルーターを追加する。ルーターはTLM-2インタコネクトコンポーネントとしてモデリングされる。つまり、そのコンポーネントではターゲットソケットから入ってくるトランザクションをイニシエータソケットに出力する。この例では4つのイニシエータソケットが4つのメモリインスタンスに接続される。

トップ階層の接続は以下の通り。

SC_MODULE(Top)
{
  Initiator* initiator;
  Router<4>* router;
  Memory*    memory[4];

  SC_CTOR(Top)
  {
    initiator = new Initiator("initiator");
    router    = new Router<4>("router");
    for (int i = 0; i < 4; i++)
    {
      char txt[20];
      sprintf(txt, "memory_%d", i);
      memory[i]   = new Memory(txt);
    }

    initiator->socket.bind( router->target_socket );
    for (int i = 0; i < 4; i++)
      router->initiator_socket[i]->bind( memory[i]->socket );
  }
};

この例ではイニシエータのソケットはルーターのターゲットソケットにバインドされ、ルーターのそれぞれ4つのイニシエータソケットは別々のターゲットメモリのソケットにバインドされる。initiator-to-tagetソケットはそれぞれpoint-to-point接続である。1つのイニシエータソケットを複数のターゲットソケットにバインドすることはできないし、逆方向についても同様である。(ただし、実際のところはできる。そのためにはmulti-passthroughソケットと呼ばれる別の便利なソケットを使用する必要がある。)

それぞれのメモリはイニシエータからは異なるアドレススペースに配置される。そのため、ルータートランザクションに埋め込まれたアドレスに依存して、メモリのローカルアドレスへのアドレス変換をしつつ、適したメモリにトランザクションをルーティングしなければならない。このアドレス変換処理は通常転送だけでなくDMIやデバッグ転送でも行う必要がある。

ルーターは一つのシンプルターゲットソケットのインスタンスを持ち、4つのシンプルイニシエータソケットを持ち、それらはブロッキング転送をサポートする。

template<unsigned int N_TARGETS>
struct Router: sc_module
{
  tlm_utils::simple_target_socket<Router>
                                        target_socket;
  tlm_utils::simple_initiator_socket_tagged<Router>
                                        initiator_socket[N_TARGETS];
  ...
};

ターゲット数はテンプレート引数によって指定可能となっている。すべてのソケットはデフォルトで32ビット幅でベースプロトコルを使う。このルーターは汎用ペイロードトランザクションしかルーティングできない(Extensionには対応しない)。
また、イニシエータソケットは simple_initiator_socket_tagged を使用する。タグ付きソケットを使うことでどのソケットからメソッド呼び出しが行われたかがわかるようになり、イニシエータソケットが複数ある場合には必須のものである。以下がルータのコンストラクタである。

SC_CTOR(Router)
: target_socket("target_socket")
{
  target_socket.register_b_transport(    this, &Router::b_transport);
  target_socket.register_get_direct_mem_ptr(
                                         this, &Router::get_direct_mem_ptr);
  target_socket.register_transport_dbg(  this, &Router::transport_dbg);

  for (unsigned int i = 0; i < N_TARGETS; i++)
  {
    char txt[20];
    sprintf(txt, "socket_%d", i);
    initiator_socket[i] = 
        new tlm_utils::simple_initiator_socket_tagged<Router>(txt);

    initiator_socket[i]->register_invalidate_direct_mem_ptr(
        this, &Router::invalidate_direct_mem_ptr, i);
  }
}

それぞれのインタフェースメソッドの実装はターゲットソケットかイニシエータソケットかの適したソケットに登録される。
イニシエータソケットへの登録メソッドの最後の引数が整数タグになっていることに着目してほしい。このタグはコールバック時に invalidate_direct_mem_ptr メソッドの引数として渡される。次に、ブロッキング転送インタフェース、DMI、デバッグ転送インタフェースを見ていく。

Routing the b_transport method

ブロッキング転送メソッドはfoward方向の際にのみ渡される。したがって、ターゲットから返される b_transport 呼び出しのハンドリングについては考慮しなくてよい。以下はルーターの b_transport の実装である。

virtual void b_transport( tlm::tlm_generic_payload& trans, sc_time& delay )
{
  sc_dt::uint64 address = trans.get_address();
  sc_dt::uint64 masked_address;
  unsigned int target_nr = decode_address( address, masked_address);

  trans.set_address( masked_address );

  ( *initiator_socket[target_nr] )->b_transport( trans, delay );
}

ルーターはアドレス属性を調べ、どのソケットにトランザクションを受け渡すか決定します。そのための変数として target_nr を使っている。この例では以下のアドレスデコーディング関数を使っている。

inline unsigned int decode_address( sc_dt::uint64 address,
                                    sc_dt::uint64& masked_address )
{
  unsigned int target_nr = static_cast<unsigned int>( (address >> 8) & 0x3 );
  masked_address = address & 0xFF;
  return target_nr;
}

inline sc_dt::uint64 compose_address( unsigned int target_nr,
                                      sc_dt::uint64 address)
{
  return (target_nr << 8) | (address & 0xFF);
}

ルーターは汎用ペイロードトランザクションのアドレス属性をマスクしたアドレス、すなわちターゲットメモリのローカルアドレスで上書きする。
アドレス属性はインターコネクトコンポーネント内で修正することが許される数少ない属性の一つである。ほかの書き換え可能な属性としてはDMIヒントとExtensionがある。インターコネクトとターゲットはほとんどの属性値をリードオンリーとして扱う必要がある。

b_transportの最後ではトランザクションを適切なイニシエータソケットに転送している。ターゲットモジュールのb_transportはイニシエータモジュールのスレッドプロセスのコンテキストで実行される。関数終了時は関数コールチェーン全体が巻き戻りイニシエータに戻されることになる。

Routing DMI and Debug Transactions

DMIとデバッグ転送のルーティングで重要な原則は、通常転送と全く同じアドレス変換を行うことであり、その変換はfoward/backward双方で行うことが必要である。まずはfoward DMIインタフェースから見ていく。

virtual bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans,
                                tlm::tlm_dmi& dmi_data)
{
  sc_dt::uint64 masked_address;
  unsigned int target_nr = decode_address( trans.get_address(),
                                           masked_address );
  trans.set_address( masked_address );

  bool status = ( *initiator_socket[target_nr] )->
                   get_direct_mem_ptr( trans, dmi_data );
  ...

コードは上述の b_transport の実装と似ている。要求された DMI アドレスはターゲットでのアドレス空間に変換され、トランザクションは適切なターゲットにルーティングされる。

ターゲットはアドレス範囲を含むDMIデータオブジェクトによってDMI要求に返答するが、そのオブジェクトについてもイニシエータでのアドレス空間に変換する必要がある。

  dmi_data.set_start_address(
    compose_address( target_nr, dmi_data.get_start_address() ));
  dmi_data.set_end_address  (
    compose_address( target_nr, dmi_data.get_end_address() ));

  return status;
}

DMIデータオブジェクトに組み込まれている開始アドレスと終了アドレスは上書きされる。重要な点はここでは逆アドレス変換が行われている点である。ターゲットでDMIポインターを無効化する際もこのbackwardパスと同じ処理が必要である。

virtual void invalidate_direct_mem_ptr(int id,
                                       sc_dt::uint64 start_range,
                                       sc_dt::uint64 end_range)
{
  sc_dt::uint64 bw_start_range = compose_address( id, start_range );
  sc_dt::uint64 bw_end_range   = compose_address( id, end_range );
  target_socket->invalidate_direct_mem_ptr(bw_start_range, bw_end_range);
}

ここでの invalidate_direct_mem_ptr 実装はタグ付きソケットからのコールバックの例となっている。
ソケットはどのソケットからメソッド呼び出しが行われたか判別するための int id引数を追加している。
ターゲットと同じIDを設定しているため、このメソッドで逆アドレス変換が実行できる。

最後にデバッグ転送メソッドのルーティングを見ていくが、今までの例と同様の実装となっている。

virtual unsigned int transport_dbg(tlm::tlm_generic_payload& trans)
{
  sc_dt::uint64 masked_address;
  unsigned int target_nr = decode_address( trans.get_address(), 
                                           masked_address );
  trans.set_address( masked_address );

  return ( *initiator_socket[target_nr] )->transport_dbg( trans );
}

補注:
Tutorial 4, 5, 6もあるが、サンプルコードのみで解説がないため、本日記での紹介はいったんここで終わりとする。

Getting Started with TLM-2.0 (Tutorial 2 - Response Status, DMI, and Debug Transport)

www.doulos.com

2つ目のチュートリアルでは汎用ペイロードの返答ステータス、デバッグメモリ、デバッグ転送インタフェースを見ていく。

The Response Status of the Generic Payload

Example 1ではターゲットがトランザクションを実行できなかった場合、単純にSystemCレポートハンドラーである SC_REPORT_ERROR()を呼び出して諦めた。
これは許容できるが、より構造的なエラーハンドリングの方法がある。すなわち、汎用ペイロードの返答ステータスである。
返答ステータスはトランザクションオブジェクトの一部であり、トランザクション完了時にイニシエータによってチェックできる。

返答ステータスのデフォルト値は TLM_INCOMPLETE_RESPONSE であり、トランザクションがターゲットに到達していないかターゲットによってトランザクションが実行されていないことを表している。
トランザクションが成功した場合、ターゲットは返答ステータスとして TLM_OK_RESPONSE を設定する。
もしトランザクションが失敗した場合、ターゲットは定義済みエラーから選ぶことができる。

virtual void b_transport( tlm::tlm_generic_payload& trans, sc_time& delay )
{
  tlm::tlm_command cmd = trans.get_command();
  sc_dt::uint64    adr = trans.get_address() / 4;
  unsigned char*   ptr = trans.get_data_ptr();
  unsigned int     len = trans.get_data_length();
  unsigned char*   byt = trans.get_byte_enable_ptr();
  unsigned int     wid = trans.get_streaming_width();

  if (adr >= sc_dt::uint64(SIZE)) {
    trans.set_response_status( tlm::TLM_ADDRESS_ERROR_RESPONSE );
    return;
  }

アドレスエラーはアドレスが範囲外であったり、アドレス値が原因でトランザクションが失敗した場合に使用される。

  if (byt != 0) {
    trans.set_response_status( tlm::TLM_BYTE_ENABLE_ERROR_RESPONSE );
    return;
  }

バイトイネーブルエラーはバイトイネーブル値によってターゲットでエラーが生じた場合やターゲットがバイトイネーブルを全くサポートしていない場合に使用される。

  if (len > 4 || wid < len) {
    trans.set_response_status( tlm::TLM_BURST_ERROR_RESPONSE );
    return;
  }

バーストエラーはデータ長あるいはストリーミング幅が原因でターゲットでエラーが生じた場合や、ターゲットがバースト転送をまったく対応しない場合に使用される。

5つ目のエラー TLM_GENERIC_ERROR_RESPONSE はトランザクション中に生じたいかなるエラーにも使用できる。

イニシエータがトランザクション完了後に返答エラーをチェックするのに役立つメソッドを汎用ペイロードは提供する。

socket->b_transport( *trans, delay );

if ( trans->is_response_error() )
{
  char txt[100];
  sprintf(txt, "Error from b_transport, response status = %s",
          trans->get_response_string().c_str());
  SC_REPORT_ERROR("TLM-2", txt);
}

is_response_ok、is_response_error を使うと明示的に値をチェックするのを避けることができ便利である。
get_response_string は返答を文字列で返す。エラーメッセージを出力する際に便利である。

Using the Direct Memory Interface

Direct Memory Interface (DMI) を使う目的は、イニシエータに直接ターゲット内のメモリ領域のポインタを与えることでシミュレーションをスピードアップすることである。したがって、トランスポートインタフェース経由のread/writeトランザクションはバイパスされる。
DMIはforward/backwardインタフェースの両方を使う。fowardパスはイニシエータソケットからターゲットソケットへの関数呼び出しのことであり、backwardパスはその逆方向である。forward DMIインタフェースではイニシエータがDirect Memory Pointerをターゲットに要求し、backward DMIインタフェースではターゲットが以前イニシエータに与えたDMI pointerを無効化する。

新しいDMIポインタを取得するメソッドから見ていく。このメソッドは get_direct_mem_ptr で、イニシエータから foward pathで呼び出され、ターゲットで実装される。メモリ(ターゲット)は simple_target_socket を使うので、b_transportと同様にターゲットはソケットに実装を登録しなければならない。そうでなければ、シンプルソケットは何も実行しないデフォルト実装を割り当てる。

socket.register_get_direct_mem_ptr(this, &Memory::get_direct_mem_ptr);

実装は以下のとおりである。

virtual bool get_direct_mem_ptr(tlm::tlm_generic_payload& trans,
                                tlm::tlm_dmi& dmi_data)
{
  dmi_data.allow_read_write();

イニシエータは特定アドレス・特定アクセスモード(R/W/RW)のDMIポインタを要求する。
ターゲットは要求を許可するかどうかを決定する。この例ではターゲットはどんなリクエストに対しても許可する。
ターゲットは DMI データオブジェクトに対して与えるアクセスの詳細を設定する必要がある。

  dmi_data.set_dmi_ptr( reinterpret_cast( &mem[0] ) );
  dmi_data.set_start_address( 0 );
  dmi_data.set_end_address( SIZE*4-1 );
  dmi_data.set_read_latency( LATENCY );
  dmi_data.set_write_latency( LATENCY );

  return true;
}

dmi_ptr は実際のダイレクトメモリポインターである。これは要求アドレスと必ずしも対応する必要がない。なぜならば、ターゲットはどのDMI領域を与えるかは自由だからである。ターゲットが許可する領域が大きいことは望ましくない。start_address と end_address はターゲットから見たDMI領域の境界を表し、例題ではメモリの全域を設定している。
read_latency と write_latency はメモリアクセスのタイミングパラメータの見積もりであり、モデルのタイミング制度に応じてイニシエータ側で使用されたり無視されたりする。

get_direct_mem_ptr は DMIポインタを供給できる場合は true を返し、そうでなければ false を返す。

ターゲットにはまだもう一つ考えられる改善がある。ターゲットは汎用ペイロードのDMIヒント属性を使ってDMIをサポートできるかどうかをイニシエータに伝達することができる。これによりイニシエータのシミュレーション速度を向上できる。なぜならば、get_direct_mem_ptr を繰り返し呼び出して fail が返り続けることは無駄だからである。したがって、我々の例では b_transport メソッドは以下のようにDMI ヒントを設定している。

trans.set_dmi_allowed(true);

ここからはイニシエータがDMIヒントをどのように使うかを見ていく。トランザクション完了後、イニシエータは汎用ペイロード内のDMIヒントをチェックする。もし、ヒントが設定されている場合、イニシエータはターゲットにDMIポインタを要求する。

tlm::tlm_generic_payload* trans = new tlm::tlm_generic_payload;
...
socket->b_transport( *trans, delay  );

if ( trans->is_dmi_allowed() )
{
  dmi_ptr_valid = socket->get_direct_mem_ptr( *trans, dmi_data );
}

イニシエータは b_transport を呼び出し、DMIヒントをチェックし、get_direct_mem_ptr を呼び出して dmi_ptr_valid フラグを設定し、DMIポインタが有効であることを示す。
イニシエータは全く同じトランザクションオブジェクトを転送にもDMIにも使用することに留意すること。これにより、シミュレーション効率が改善する。
その後、イニシエータはDMIポインタを使えるようになり、トランスポートインタフェースをバイパスできる。

if (dmi_ptr_valid)
{
  if ( cmd == tlm::TLM_READ_COMMAND )
  {
    assert( dmi_data.is_read_allowed() );
    memcpy(&data, dmi_data.get_dmi_ptr() + i, 4);
    wait( dmi_data.get_read_latency() );
  }
  else if ( cmd == tlm::TLM_WRITE_COMMAND )
  {
    assert( dmi_data.is_write_allowed() );
    memcpy(dmi_data.get_dmi_ptr() + i, &data, 4);
    wait( dmi_data.get_write_latency() );
  }
}
else
{
  ...
  socket->b_transport(*trans, delay );
  ...
  if ( trans->is_dmi_allowed() )
    ...
}

上のコードでは DMI read_latency と write_latency を使っていることに留意すること。
イニシエータが DMI を使っているとき、dmi_data オブジェクト経由でレイテンシ情報を受け取る。

これで forward DMI interfaceに関する説明を終える。ここからはbackward interfaceについて述べる。
イニシエータは invalidate_direct_mem_ptr を実装する必要がある。
この関数はターゲットからの要求に応じて既存のポインタを消去し、また、イニシエータソケットに登録される必要がある。

socket.register_invalidate_direct_mem_ptr(
       this, &Initiator::invalidate_direct_mem_ptr);
...

virtual void invalidate_direct_mem_ptr(sc_dt::uint64 start_range,
                                       sc_dt::uint64 end_range)
{
  dmi_ptr_valid = false;
}

この例ではイニシエータはダイレクトメモリ領域の境界を無視し、単純に DMI ポインタを無効化している。

Using the Debug Transport Interface

デバッグ転送インタフェースの目的はイニシエータが副作用なしに・シミュレーション時間経過なしにターゲットのメモリを読み書きできるようにすることである。
DMIとデバッグ転送インタフェースは似ているが、その意図は全く異なる。
DMIは通常のトランザクションのシミュレーション速度を改善するためのものであり、デバッグ転送インタフェースはデバッグ用途である。

ここではイニシエータからターゲットへのfoward pathのデバッグ転送インタフェースを使う。
ターゲットは transport_dbg を実装する必要があり、シンプルターゲットソケットの場合ソケットにメソッドを登録する必要がある。
そうしない場合、DMIと同様、シンプルソケットは何もしないデフォルト実装を供給する。

socket.register_transport_dbg(this, &Memory::transport_dbg);
...

virtual unsigned int transport_dbg(tlm::tlm_generic_payload& trans)
{
  tlm::tlm_command cmd = trans.get_command();
  sc_dt::uint64    adr = trans.get_address() / 4;
  unsigned char*   ptr = trans.get_data_ptr();
  unsigned int     len = trans.get_data_length();

  unsigned int num_bytes = (len < SIZE - adr) ? len : SIZE - adr;

  if ( cmd == tlm::TLM_READ_COMMAND )
    memcpy(ptr, &mem[adr], num_bytes);
  else if ( cmd == tlm::TLM_WRITE_COMMAND )
    memcpy(&mem[adr], ptr, num_bytes);

  return num_bytes;
}

デバッグ転送インタフェースはこれまでと同様 tlm_generic_payload を使う。
転送インタフェースと比較して、デバッグ転送インタフェースは制限された属性セットを使う:コマンド、アドレス、データポインタ、データ長。
上の例のように、transport_dbg メソッドは与えらえたデータ長のすべてを読み書きする必要はなく、可能な限りの長さを読み書きし、その長さを返す。
このメソッドの実装は意図的に単純にしている。トランザクション内のポインタを使ってデータコピーする以外のことをしてはならない。そうでないとデバッグ転送の目的を達成できない。

最後に、イニシエータが transport_dbg を呼び出してメモリの内容をダンプする例を見る。

trans->set_address(0);
trans->set_read();
trans->set_data_length(128);

unsigned char* data = new unsigned char[128];
trans->set_data_ptr(data);

unsigned int n_bytes = socket->transport_dbg( *trans );

for (unsigned int i = 0; i < n_bytes; i += 4)
{
  cout << "mem[" << i << "] = "
       << *(reinterpret_cast<unsigned int*>( &data[i] )) << endl;
}

Getting Started with TLM-2.0 (Tutorial 1 - Sockets, Generic Payload, Blocking Transport)

今日からはGetting Started with TLM-2.0を見ていく。

www.doulos.com

長いので重要なところだけ訳していく。

Modeling Concepts

TLMはプロセス間通信にフォーカスしている。
OSCI TLM-2.0では特に、オンチップのメモリマップドバスのモデリングにフォーカスしている。
ここでは、読者がSystemCのモジュール、ポート、プロセス、チャネル、インタフェース、イベントに詳しいことを想定する。
TLM-2.0では異なるモジュールのプロセス間通信をするために、portとexport経由でのインタフェースメソッドコールを使う。

Initiators, Targets, and Sockets

イニシエータとは、新しいトランザクションを開始するモジュールのことである(要するにmaster)。
ターゲットとはほかのモジュールによって開始したトランザクションに応答するモジュールのことである(要するにslave)。
トランザクションとはデータ構造であり(C++オブジェクト)、イニシエータ-ターゲット間を関数呼び出しによって渡される。
モジュールはイニシエータであると同時にターゲットにもなることができ、それはアービタールーター、バスなどのモデリングにおいて典型的である。

イニシエータとターゲット間でトランザクションを通すために、TLM-2.0ではソケットを使う。
イニシエータはイニシエータソケットを通じてトランザクションを送り、ターゲットはターゲットソケットを通じてトランザクションを受け取る。
トランザクションを変更せずに転送するモジュールはインターコネクトと呼ばれる。
インターコネクトはイニシエータソケットとターゲットソケットの両方を持つ。

SystemCのコードを見ていく。tlm.hをインクルードする必要がある。このケースではさらにtlm_utilsから2つのソケットに関するヘッダファイルをインクルードしている。

#define SC_INCLUDE_DYNAMIC_PROCESSES

#include "systemc"
using namespace sc_core;
using namespace sc_dt;
using namespace std;

#include "tlm.h"
#include "tlm_utils/simple_initiator_socket.h"
#include "tlm_utils/simple_target_socket.h"

OSCIシミュレータを使う場合、一部のTLM-2.0機能を使う(特にシンプルソケット)際に SC_INCLUDE_DYNAMIC_PROCESSES マクロを定義する必要がある。シンプルソケットではダイナミックプロセスを生成するためである。

イニシエータとターゲットモジュールを定義する。

struct Initiator: sc_module
{...};

struct Memory: sc_module
{...};

接続する。

SC_MODULE(Top)
{
  Initiator *initiator;
  Memory    *memory;

  SC_CTOR(Top)
  {
    initiator = new Initiator("initiator");
    memory    = new Memory   ("memory");

    initiator->socket.bind( memory->socket );
  }
};

トップレベルモジュールではイニシエータとメモリをインスタンシエーションする。イニシエータのイニシエータソケットをターゲットメモリのターゲットソケットにバインドする。ソケットは双方向通信に必要なすべてをカプセル化している。あるイニシエータソケットは常にあるターゲットソケットにバインドされる。イニシエータソケットはsc_portの派生クラスであり、ターゲットソケットはsc_exportの派生クラスである。バインド関数は一度の関数呼び出しでport-export接続を双方向に行う。これがソケットの便利な点である。

OSCIシミュレータで動かすにはsc_main関数が必要。

int sc_main(int argc, char* argv[])
{
  Top top("top");
  sc_start();
  return 0;
}

イニシエータとメモリモジュールではソケットが宣言されてコンストラクトを明示的に行う必要がある。

struct Initiator: sc_module
{
  tlm_utils::simple_initiator_socket<Initiator> socket;

  SC_CTOR(Initiator) : socket("socket")
  {
    ...
};

struct Memory: sc_module
{
  tlm_utils::simple_target_socket<Memory> socket;

  SC_CTOR(Memory) : socket("socket")
  {
    ...
};

すべてのTLM-2.0ライブラリはtlmもしくはtlm_utils名前空間に属する。
simple_initiator_socket<Initiator>、simple_target_socket<Memory>のテンプレート引数は親モジュールを指定する。
シンプルソケットは tlm_initiator_socket や tlm_target_socket の派生クラスであるが、ここではその詳細を知る必要はない。

この例ではイニシエータはブロッキング転送を使ってターゲットメモリと通信する。そのためには b_transport メソッドを実装する必要がある。
シンプルターゲットソケットを使う場合、以下のように、ターゲットでソケットに対してコールバックメソッドを登録する。

socket.register_b_transport(this, &Memory::b_transport);

ターゲットがすべきことは b_transport メソッドを実装することだけであり、以下で述べる。

補注:ここまでのコードをまとめると以下のような感じになる(コンパイル可能)。
newは令和の時代では使うのは微妙なので実体をTopに置いている。

#define SC_INCLUDE_DYNAMIC_PROCESSES

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

using namespace sc_core;
using namespace sc_dt;
using namespace std;

SC_MODULE(Initiator) {
  tlm_utils::simple_initiator_socket<Initiator> socket;
  SC_CTOR(Initiator) : socket("socket") {
  }
};

SC_MODULE(Memory) {
  tlm_utils::simple_target_socket<Memory> socket;
  SC_CTOR(Memory) : socket("socket") {
    //socket.register_b_transport(this, &Memory::b_transport);
  }
};

SC_MODULE(Top) {
  Initiator initiator;
  Memory memory;
  SC_CTOR(Top) : initiator("initiator"), memory("memory") {
    initiator.socket.bind(memory.socket);
  }
};

int sc_main(int, char**) {
  Top top("top");
  sc_start();
  return 0;
}

The Generic Payload and Blocking Transport

ソケットクラスのデフォルト(=テンプレートクラス引数で特に指定しない場合)のトランザクションタイプとして、tlm_generic_payload が使われる。
汎用ペイロードはTLMの相互運用性を達成するという意味でTLM2.0標準のカギとなるものである。
汎用ペイロードは2つの密接に関連した目的がある。
1つ目の目的は、汎用目的のトランザクションタイプとして抽象メモリマップドバスのモデリングに使うことであり、特定のバスプロトコルの正確な詳細に関心がないときに既存のモデルとすぐさま接続できる。
2つ目の目的は、汎用ペイロードは様々な特定のプロトコルの詳細レベルをモデリングする際に基礎として(補注:基底クラスとしての意味か?)使用することができる。同じ汎用ペイロードタイプを基礎に作られた異なるプロトコル同士の橋渡しが比較的簡単にできる。

例題のイニシエータモジュールはスレッドプロセスを持ち汎用ペイロードトランザクションのストリームを生成する。

SC_CTOR(Initiator) : socket("socket")
{
  SC_THREAD(thread_process);
}

void thread_process()
{
  tlm::tlm_generic_payload* trans = new tlm::tlm_generic_payload;
  sc_time delay = sc_time(10, SC_NS);

  for (int i = 32; i < 96; i += 4)
  {
    ...
    socket->b_transport( *trans, delay );
    ...
  }
}

トランザクションは b_transport メソッドによりソケット経由で送られる。
トランザクションは参照渡しで渡され、返り値はない。
イニシエータはトランザクションのstorage破棄に責任がある。
b_transport 呼び出しではタイミングアノテーションも運ぶ。
タイミングアノテーションは現トランザクションが完了後、現シミュレーション時刻に加算される必要がある。
タイミングアノテーションはb_transport 呼び出し時もreturn時も有効である。

tlm::tlm_command cmd = static_cast(rand() % 2);
if (cmd == tlm::TLM_WRITE_COMMAND) data = 0xFF000000 | i;

trans->set_command( cmd );
trans->set_address( i );
trans->set_data_ptr( reinterpret_cast<unsigned char*>(&data) );
trans->set_data_length( 4 );
trans->set_streaming_width( 4 );
trans->set_byte_enable_ptr( 0 );
trans->set_dmi_allowed( false );
trans->set_response_status( tlm::TLM_INCOMPLETE_RESPONSE );

socket->b_transport( *trans, delay );

汎用ペイロードは標準的な属性を持つ。コマンド、アドレス、データ、バイトイネーブル、ストリーミング幅、返答ステータスである。
汎用ペイロードはDMIヒント・拡張もともに運ぶ。
それぞれの属性はデフォルト値を持つが、10のうち8つは明示的に設定することが推奨される。
なぜなら、トランザクションオブジェクトはプールから再利用されることが多いからである。

コマンドはread/writeの2種類がある。
アドレスはデータの読み込み・書き込みが行われる区間の最下位アドレスである。
データポインタはイニシエータ内のデータバッファを指し示し、データ長はデータのバイト数である。
実際のデータコピーはターゲット内で実行される。

ストリーミング幅は要するにバス幅のことである(超訳
ストリームでないトランザクションではストリーミング幅はデータ長と同じとなる。
デフォルト値は0だが、0はインタフェースメソッドコール経由で送られる場合許容されない。データポインタとデータ長も同様。

バイトイネーブルポインタは0であれば使用しないことを示す。バイトイネーブル長属性も存在するが、バイトイネーブルポインタが0の場合は無視されるためここでは設定しない。

set_dmi_allowed メソッドは DMI ヒントを設定する。これは常に falseを設定すべきである。
DMIヒントはターゲットにより設定されることもあり、Direct Memory Interfaceをサポートするかどうかを示す。

返答ステータスは常に TLM_INCOMPLETE_RESPONSE を設定すべきである。本当の返答ステータスはターゲットにより設定される。

ここで触れられていない10個目の属性は拡張機能の配列である。拡張機能については今後のチュートリアルで議論される。デフォルトではイニシエータでもターゲットでも無視される。

ブロッキング転送メソッドはターゲットメモリ内で実装される。まず、6属性が汎用ペイロードから取り出される。

virtual void b_transport( tlm::tlm_generic_payload& trans, sc_time& delay )
{
  tlm::tlm_command cmd = trans.get_command();
  sc_dt::uint64    adr = trans.get_address() / 4;
  unsigned char*   ptr = trans.get_data_ptr();
  unsigned int     len = trans.get_data_length();
  unsigned char*   byt = trans.get_byte_enable_ptr();
  unsigned int     wid = trans.get_streaming_width();

次に、ターゲットがサポートしていない機能をイニシエータが使おうとしてないかをチェックする。
ここではバイトイネーブル、ストリーミング幅、バースト長の確認をしている。
また、アドレスが範囲外かどうかもチェックする。
トランザクションが実行できない場合は、SystemCレポートハンドラーを呼んでエラー生成する。

  if (adr >= sc_dt::uint64(SIZE) || byt != 0 || len > 4 || wid < len)
    SC_REPORT_ERROR("TLM-2", 
                "Target does not support given generic payload transaction");

ターゲットはread/writeコマンドをデータコピーにより実装する。エンディアンに関しては汎用ペイロードはホストコンピュータと同じエンディアンを使う。
ターゲットメモリがホストのエンディアンと同じ限り、データコピーはmemcpyにより実現できる。

  if ( cmd == tlm::TLM_READ_COMMAND )
    memcpy(ptr, &mem[adr], len);
  else if ( cmd == tlm::TLM_WRITE_COMMAND )
    memcpy(&mem[adr], ptr, len);

最後にブロッキング転送は返答ステータスをトランザクションに設定し、トランザクションの完了成功を示す。
設定しない場合、トランザクションが未完了を示すことになる。

  trans.set_response_status( tlm::TLM_OK_RESPONSE );

Timing Annotation

ブロッキング転送メソッドでは機能のみモデリングし、時間についてはモデリングしないため、単にdelay引数は無視し、変更しないままイニシエータに返す。
b_transport 呼び出し後は、イニシエータは返答ステータスをチェックする。

if (trans->is_response_error() )
  SC_REPORT_ERROR("TLM-2", "Response error from b_transport");

ここで、イニシエータはアノテートされた時間を認識する必要がある。
このモデルは機能のみに関心があるため、delayを無限に累算し続けてもよい。
このアイディアでは、シミュレーションモデルは機能をフルスピードでシミュレーションし、消費時間は「内緒で」累算し続けることになる。
このコーディングスタイルはTLM-2.0標準ではLoosly-Timedと呼ばれる。
しかしながら、モデルが実際にするのはb_transport呼び出しで返ってきたdelayに従ってwaitすることである。
これはシミュレーション実行速度を遅くする。なぜならコンテキストスイッチングがトランザクション毎に発生するからである。
しかし、単純な例題ではこれで十分だし、シミュレーションログも解釈が容易である。

補注:全体のコードはここで参照できる。
https://github.com/marcelosousa/llvmvf/blob/master/tests/systemc/tlm/tlm2_getting_started_1.cpp

私が書き直したコードは以下。

#define SC_INCLUDE_DYNAMIC_PROCESSES

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>

using namespace sc_core;
using namespace sc_dt;
using namespace std;

SC_MODULE(Initiator) {
  tlm_utils::simple_initiator_socket<Initiator> socket;
  int data;
  SC_CTOR(Initiator) : socket("socket"), data(0) {
    SC_THREAD(thread_process);
  }
  void thread_process() {
    tlm::tlm_generic_payload trans;
    sc_time delay = sc_time(10, SC_NS);
    for (int i = 32; i < 96; i += 4) {
      tlm::tlm_command cmd = static_cast<tlm::tlm_command>(rand() % 2);
      if (cmd == tlm::TLM_WRITE_COMMAND) data = 0xFF000000 | i;
      trans.set_command(cmd);
      trans.set_address(i);
      trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
      trans.set_data_length(4);
      trans.set_streaming_width(4);
      trans.set_byte_enable_ptr(0);
      trans.set_dmi_allowed(false);
      trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
      cout << sc_time_stamp() << ": [initiator] cmd=" << cmd << " addr=" << i;
      if (cmd) cout << " data=" << data;
      cout << endl;
      socket->b_transport(trans, delay);
      if (trans.is_response_error()) {
        SC_REPORT_ERROR("TLM-2", "Response error from b_transport");
      }
      wait(delay);
    }
  }
};

SC_MODULE(Memory) {
  tlm_utils::simple_target_socket<Memory> socket;
  static constexpr int SIZE = 256;
  int mem[SIZE];
  SC_CTOR(Memory) : socket("socket") {
    socket.register_b_transport(this, &Memory::b_transport);
  }
  virtual void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    tlm::tlm_command cmd = trans.get_command();
    sc_dt::uint64    adr = trans.get_address() / 4;
    unsigned char*   ptr = trans.get_data_ptr();
    unsigned int     len = trans.get_data_length();
    unsigned char*   byt = trans.get_byte_enable_ptr();
    unsigned int     wid = trans.get_streaming_width();
    if (adr >= sc_dt::uint64(SIZE) || byt != 0 || len > 4 || wid < len) {
      SC_REPORT_ERROR("TLM-2", "Target does not support given generic payload transaction");
    }
    if (cmd == tlm::TLM_READ_COMMAND) {
      memcpy(ptr, &mem[adr], len);
    } else if (cmd == tlm::TLM_WRITE_COMMAND) {
      memcpy(&mem[adr], ptr, len);
    }
    cout << sc_time_stamp() << ": [memory] cmd=" << cmd << " addr=" << adr * 4;
    if (cmd) cout << " data=" << *reinterpret_cast<int*>(ptr);
    cout << endl;
    trans.set_response_status(tlm::TLM_OK_RESPONSE);
  }
};

SC_MODULE(Top) {
  Initiator initiator;
  Memory memory;
  SC_CTOR(Top) : initiator("initiator"), memory("memory") {
    initiator.socket.bind(memory.socket);
  }
};

int sc_main(int, char**) {
  Top top("top");
  sc_start();
  return 0;
}

Interoperability and the Base Protocol

まとめると、TLM-2.0は標準的なAPIを使ったモデル間の相互運用性を確かにし、ユーティリティクラスによって生産性の向上と一貫したコーディングスタイルを提供するものである。相互運用性のキーは

  1. 標準のイニシエータソケット・ターゲットソケットを使う。
  2. 汎用ペイロードを使う。
  3. ベースプロトコルを使う。

このチュートリアルではまだベースプロトコルの詳細については触れられていないが、シンプルソケットと b_transport メソッドで暗に使用されている。ベースプロトコルは汎用ペイロードの使い方のルールを指定する。これはのちのチュートリアルで詳細を示す。

ブロッキング転送インタフェースは1回の関数呼び出しでトランザクションが完了するときに使う。
トランザクション要求がb_transport呼び出しで渡され、返答はb_transportからのreturn時に運ばれる。
この例では、一つのトランザクションオブジェクトが呼び出し毎に再利用される。トランザクションオブジェクトのstorageは最初にイニシエータ内ですべて割り当てられる。この方式で動くのは、同時に1つのトランザクションのみが渡されるためである。

この例題の別の特徴としては、ブロッキング転送メソッドがブロックしない点である。つまり、waitを呼び出さない点である。
b_transport内でwaitを呼ぶこともできた。しかしながら、原理的にはイニシエータ内の複数のスレッドから同じソケットを介してb_transportへの複数の並行した呼び出すこともでき、これはタイミングアノテーションと競合する可能性がある。
この状況はベースプロトコルのルールでは許容される。