gem5

返回主页
返回上一页

gem5_source_code
gem5_source_code lecture_1

返回主页
返回上一页

Lecture1

内存系统编程模型概述

gem5中各级Memory Objects通过ports互联。Ports为Memory Objects提供了接口。

Ports

主要包含三类:

  • Timing

    • 最重要的一类:唯一能够产生正确模拟结果的类;其它两类只用在特定的场合。

  • Atomic

    • 用于加速模拟和实现模拟的warm up。这个模式假设内存系统不会产生事件;它认为所有内存请求以单个long callchain(调用链)执行。

  • Functional

    • 更合适的叫法为Debugging Mode,在SE模式下较为常用。其能够从Host导入二进制可执行文件(或脚本)到process.cmd,以使得模拟系统可以访问。

 

Packets

Ports之间的通信传输以Packet为媒介,又由于Memory Objects通过Ports互联,因此也可以理解为Memory Objects以Packet进行交互。

  • Packet由MemReq构成,即内存请求对象。MemReq持有指示请求者、请求地址、请求类型的原始请求信息。

  • Packet也包含MemCmd,指示当前Packet的命令。这个命令在Packet的生命周期中可以变化。当命令满足(执行结束)时,Request会转向Response。常见的命令包括:

    • ReadReq (read request)

    • ReadResp (read response)

    • WriteReq (write request)

    • WriteResp (write response)

    • WritebackDirty ,WritebackClean (writeback requests)

  • Packet也保留了请求的数据,或者数据的指针

  • 尽管Packet最初用于传统cache用于追踪一致性的单元,但在gem5中,即使与一致性无关的Memory Objects(例如:DRAM控制器,CPU模型)以全部使用packet通信。

 

Ports Interface

早期版本的gem5的对于port的分类是主从(master-slave)两种。但是因为众所周知的原因,在现在的版本里面已经以memSidePort(s)以及cpuSidePort(s)代替。

注:cpuSidePort(s)以及memSidePort(s)最原始也是ResponsePort以及RequestPort

gem5里的Memory Object都至少需要有其中一种类型。

  • master通常是发送请求(接收响应)send requests (and receive response)

  • slave通常是接受请求 (发送响应) receive requests (and send responses)

上面就是一个简单的主从Memory Object之间的请求响应流程。如果自己实现一个Memory Object,就需要自己实现:sendTimingReq,RecvTimingReq,sendTimingResp,recvTimingResp,以及何时触发这些操作的一系列逻辑。

所有的port interface都需要一个PacketPtr做参数。RecvTimingReqrecvTimingResp这类请求会返回一个bool类型的值。

 

不管是主还是从,都有可能处于busy的状态。例如,slave如果busy,那么recvTimingReq就会向master返回falsemastersendTimingReq之后,接收到false,就会一直等直到recvReqRetry执行。recvReqRetry被调用才会重新调用sendTimingRetry

Master busy也是类似的。

 

Pybind & Declare the SimObject

Pybind

Pybind是一个用于将C++代码绑定到Python的开源库。它允许开发者通过简单的方式创建Python模块,将现有的C++代码暴露给Python解释器,使得这些C++代码可以像Python代码一样被调用和使用

 

Step 1

Memory Object类的编写第一步通常是定义一个Python类

例如

老版本gem5中的定义无需cxx_class,且对应的.cc / .hh文件无需嵌入gem5命名空间

 

Step 2

编写对应Memory Object的头文件

下面是简单的Memory Object实现,没有任何cache行为,单纯传递请求和响应。

  • 定义两种类型的Port

  • handleFunction(PacketPtr pkt) 的作用是处理函数调用相关的包,它通常用于模拟系统调用或其他 CPU 指令发出的特殊操作。在 gem5 中,许多系统调用或者设备驱动相关的功能可能通过这种函数实现。

    • 例如:

 

Step 3

接下来就需要实现Memory Object对应的功能。也就是实现.cc文件。

  • 首先需要通过构造类构造对应的Memory Object。注:对应参数SimpleMemobjParams *params会自动生成。

    • gem5 中使用了一种名为 参数化构造 的方法,通过 Python 脚本根据 .hh 文件生成参数类。这些脚本解析类的定义和其构造函数所需的参数,然后自动生成对应的参数类。

  • 接下来需要实现接口以获取port

    • 根据参数if_name获取port(在py文件中声明),如果不匹配则会交给父类。

  • 接下来分别实现CPUSidePortMemSidePort相应的功能

    • 值得注意的是,得区分谁是master,谁是slave。对于一个Memory Object来说,CPUSide是接收来自CPU的请求(发送响应),MemSide,是向内存侧发起请求(接收响应)。


 

  • 处理完CPUSidePortMemSidePort后需要编写处理请求和响应的一些函数。

  • 这几段代码里提到的blocked,是一个bool变量。

    • blocked构造时默认false,当前Memory Object正在等待response时设置为true

    • handleRequestblocked为真时不执行(返回失败)

    • handleResponse必须在blocked为真时才会执行

CallChain Analysis

以第一次处理请求为例

这里不包含失败重试的部分,整体的调用链相对还是简单的。基本就是CPUSidePort接收请求,Memory Object进行处理,通过MemSidePort向MemSide发送请求。等到响应到达MemSidePort,Memory Object处理响应。随后响应CPUSide的请求(发送响应)。

当请求(响应)接受多的时候,就有可能出现retry。在代码中,似乎一些函数并不是在这个文件中实现的,比如sendRetryReq,sendTimingReq等等,有一些奇怪。

这里还会出现blockedPacket,首次赋值在MemSidePort::sendPacket。因为有可能请求的MemSide处于忙状态,因此MemSidePort会收到sendTimingReqfalse返回值。此时,packet会被暂存在blockedPacket里。当MemSide结束忙碌状态,向这个MemSidePort发送重试请求时,blockedPacket会被置为nullptr,同时重新发送刚刚暂存的packet。

涉及到sendPacket都需要首先确保blockedPacket是空的,这是因为sendPacket是一个有可能遇到忙碌而失败的操作,因此需要存下每一次的Packet。阻塞式的Memory Object,每一个Port都有一个blockedPacket

 

Simple Cache

cache在gem5中也是一个Memory Object,这里是一个简单的cache object

Step 1

第一步依然是需要定义一个相应的python文件

  • VectorResponsePort

    • 在gem5中,VectorResponsePort 是一种用于处理向量响应的端口,通常与多个请求响应相关联。它允许一个组件接收来自多个请求的响应,并能够以一种高效的方式将这些响应传递给相应的请求发起者。

  • system

    • 这是模拟的系统,cache是其中的一部分

 

Step 2

接下来就是定义相应的头文件

  • 数据转发几乎和之前的简单的Memory Object一致,但是作为cache,它需要响应多核CPU请求;需要有缓存的驱逐等等。

 

Step 3

之后就是对具体逻辑的实现,也就是对头文件的实现

前面的代码与之前的Memory Object几乎一致,基本不需要过多解释。下面这个与之前略有不同。额外多了一层判断。毕竟还是一个阻塞式的cache,端口存了一个请求或者还需要重试,就不能够响应请求。

MemSide这边几乎没有变化。

接下来就是SimpleCache变化新增比较多的部分了

cachehandleRequest就是处理CPU对内存的请求,而这个请求可能在cache当中,也可能不在,但还是会访问cache。而访问cache是需要时间的。因此事件的发送就需要结合访问延迟来进行调度。

这里是Simple Cache是一个阻塞式的cache,因此一次只允许一个请求执行,因此只需要保留一个port id。

cache需要处理的响应是什么呢?首先得知道cache为什么会需要接收响应。因为它发起了请求。为什么它会发起请求?因为Cache Miss了。

所以处理响应的第一步就是把packet插到cache里(假设插入是在关键路径之外,也不会导致任何的延迟)。

 

Cache需要响应不同的CPU请求,因此需要根据等待响应的CPU的端口号来发送数据包。必须要始终记得对Cache的请求可能来自不同的CPU。

accessTiming

单独摘出来的函数,是Simple Memory Object所没有的。

这段代码是 SimpleCache 类中的 accessTiming 方法,负责处理对缓存的访问请求,决定是命中还是未命中,并相应地处理数据包

accessFunctional

负责处理对缓存的功能性访问请求

获取请求的地址 -> 在cache哈希表(k:v=Addr,data)里找 -> 然后根据读还是写进行相应操作

insert

这段代码负责将响应数据包插入到缓存中。如果缓存已满,它会随机选择一个块进行驱逐,并将其数据写回内存。

详细介绍上述的随机剔除代码

  • bucket:用来存储随机选择的桶的索引。

  • bucket_size:用来存储所选桶中的条目数量。

  • random_mt.random(0, (int)cacheStore.bucket_count() - 1)

    • cacheStore 中获取桶的总数量(bucket_count()),然后随机生成一个从 0bucket_count() - 1 的整数。这个整数表示桶的索引。

    • random_mt 是一个随机数生成器

  • while ((bucket_size = cacheStore.bucket_size(bucket)) == 0)

    • bucket_size = cacheStore.bucket_size(bucket) 获取当前随机选择的桶中条目的数量

    • 如果 bucket_size0,说明该桶是空的,进入 while 循环,继续生成新的桶索引,直到找到一个非空的桶

    • 这确保了我们最终选择的桶中至少有一个block可以驱逐

  • auto block = std::next(cacheStore.begin(bucket), random_mt.random(0, bucket_size - 1));

    • cacheStore.begin(bucket):获取选定桶的迭代器(指向该桶的第一个条目)

    • random_mt.random(0, bucket_size - 1): 随机生成一个从 0bucket_size - 1 的整数,表示在当前桶中的随机条目的索引

    • std::next(...)

      • std::next 函数接受一个迭代器和一个偏移量,返回指向桶中指定位置的迭代器。

      • 这段代码通过 std::next 从桶的开始位置移动到随机选择的条目,获取对应的块(block