目錄
- DramSys 代碼分析
- 1 Configuration結構體構造
- 1.1 `from_path` 函數詳解
- 1.2 構造過程總結
- 這種設計的好處
- 2 Simulator 例化過程
- 2.1 instantiateInitiator
DramSys 代碼分析
1 Configuration結構體構造
好的,我們來詳細解釋一下 DRAMSysConfiguration.cpp
文件中 from_path
函數的配置構造過程。這個文件是 DRAMSys 從 JSON 配置文件加載配置的關鍵部分。
從代碼來看,DRAMSys 采用了一種非常巧妙且強大的方式來處理配置:主配置文件中可以引用(嵌入)其他子配置文件。這使得配置模塊化,更易于管理。
from_path
函數主要利用了 nlohmann/json
庫的強大功能,特別是它的**解析回調(parser callback)**機制,實現了在解析過程中動態加載和替換 JSON 內容。
1.1 from_path
函數詳解
#include "DRAMSysConfiguration.h" // 包含配置結構體的定義
#include "DRAMSys/config/MemSpec.h" // 包含 MemSpec 相關的常量和結構體#include <fstream> // 用于文件操作
#include <nlohmann/json.hpp> // nlohmann/json 庫namespace DRAMSys::Config
{// 這是核心函數,從給定路徑的配置文件中構造 Configuration 對象
Configuration from_path(std::filesystem::path baseConfig)
{// 1. 打開主配置文件std::ifstream file(baseConfig); // 使用 std::ifstream 打開 JSON 文件std::filesystem::path baseDir = baseConfig.parent_path(); // 獲取配置文件所在的目錄,用于構建子配置文件的絕對路徑// 2. 定義內部枚舉類,用于識別當前正在處理的子配置類型enum class SubConfig{MemSpec,AddressMapping,McConfig,SimConfig,TraceSetup,Unkown // 未知類型} current_sub_config; // 聲明一個變量來存儲當前識別到的子配置類型// 3. 定義自定義解析回調函數// 這是一個 std::function 對象,它會在 nlohmann::json 解析 JSON 文件時被調用// 它的作用是在遇到特定的鍵(例如 "MemSpec")時,將該鍵對應的值(通常是子配置文件的文件名字符串)// 替換為實際解析后的子配置文件 JSON 對象。std::function<bool(int depth, nlohmann::detail::parse_event_t event, json_t& parsed)>parser_callback;parser_callback = [&parser_callback, ¤t_sub_config, baseDir](int depth, nlohmann::detail::parse_event_t event, json_t& parsed) -> bool{using nlohmann::detail::parse_event_t;// nlohmann::json 的解析回調會在解析 JSON 文件的不同事件(如開始對象、遇到鍵、遇到值等)觸發// depth 表示當前解析的 JSON 深度。// event 表示觸發回調的事件類型。// parsed 表示當前解析到的 JSON 值(可能是鍵名、字符串、數字、對象等)。// 我們只關心深度為 2 的事件。DRAMSys 的主配置文件可能在頂層(深度0)有一個總鍵(如"DRAMSys"),// 接著是各個子配置的鍵(如"MemSpec"、"AddressMapping"),這些鍵的值是文件的路徑字符串。// 比如:// {// "DRAMSys": { // depth 1// "MemSpec": "memory.json", // depth 2: "MemSpec" 是 key,"memory.json" 是 value// "AddressMapping": "address.json",// // ...// }// }if (depth != 2)return true; // 如果深度不是2,則不處理,直接返回 true 繼續解析// 處理“鍵”(key)事件if (event == parse_event_t::key){assert(parsed.is_string()); // 斷言當前解析到的值是字符串(即鍵名)// 根據鍵名識別當前正在處理的子配置類型if (parsed == MemSpecConstants::KEY) // 例如 "MemSpec"current_sub_config = SubConfig::MemSpec;else if (parsed == AddressMapping::KEY) // 例如 "AddressMapping"current_sub_config = SubConfig::AddressMapping;else if (parsed == McConfig::KEY) // 例如 "McConfig"current_sub_config = SubConfig::McConfig;else if (parsed == SimConfig::KEY) // 例如 "SimConfig"current_sub_config = SubConfig::SimConfig;else if (parsed == TraceSetupConstants::KEY) // 例如 "TraceSetup"current_sub_config = SubConfig::TraceSetup;elsecurrent_sub_config = SubConfig::Unkown; // 未識別的鍵}// 處理“值”(value)事件// 只有當當前識別到的子配置類型不是未知(即我們之前識別到了一個有效的子配置鍵)// 并且當前事件是 value 時才進入此邏輯。if (event == parse_event_t::value && current_sub_config != SubConfig::Unkown){// 在這里,`parsed` 變量包含了子配置文件的文件名字符串(例如 "memory.json")。// 我們的目標是將這個字符串替換為實際解析后的 JSON 對象。// 定義一個 lambda 表達式 `parse_json`,用于加載和解析子 JSON 文件auto parse_json = [&parser_callback, baseDir](std::string_view sub_config_key,const std::string& filename) -> json_t{// 構建子配置文件的完整路徑std::filesystem::path path{baseDir}; // 以主配置文件所在目錄為基礎path /= filename; // 拼接子文件名std::ifstream json_file(path); // 打開子配置文件if (!json_file.is_open())throw std::runtime_error("Failed to open file " + std::string(path)); // 錯誤處理:文件無法打開// 遞歸地解析子 JSON 文件。// 注意這里再次使用了 `parser_callback`。這意味著子配置文件中如果也包含對其他子配置文件的引用,// 也可以被這個機制處理,形成一個遞歸加載的過程。// `json_t::parse(json_file, parser_callback, true, true)` 會解析文件并應用回調。// `.at(sub_config_key)` 是因為子配置文件可能也有一個頂層鍵,例如 `{"MemSpec": {...}}`。json_t json =json_t::parse(json_file, parser_callback, true, true).at(sub_config_key);return json;};// 根據之前識別到的 `current_sub_config` 類型,調用 `parse_json` 來加載對應的子文件// 并將 `parsed` 變量(它原本是文件名字符串)替換為解析后的 JSON 對象if (current_sub_config == SubConfig::MemSpec)parsed = parse_json(MemSpecConstants::KEY, parsed);else if (current_sub_config == SubConfig::AddressMapping)parsed = parse_json(AddressMapping::KEY, parsed);else if (current_sub_config == SubConfig::McConfig)parsed = parse_json(McConfig::KEY, parsed);else if (current_sub_config == SubConfig::SimConfig)parsed = parse_json(SimConfig::KEY, parsed);else if (current_sub_config == SubConfig::TraceSetup)parsed = parse_json(TraceSetupConstants::KEY, parsed);}return true; // 返回 true 繼續解析過程};// 4. 開始解析主配置文件if (file.is_open()){// 調用 nlohmann::json 的 parse 函數,傳入文件流和自定義的 parser_callback// `true, true` 參數表示:// 第一個 true: allow_exceptions - 允許拋出解析異常// 第二個 true: ignore_comments - 忽略 JSON 中的注釋// `.at(Configuration::KEY)`: 主配置文件可能有一個頂層鍵(例如 "DRAMSys"),我們需要進入這個鍵對應的對象。json_t simulation = json_t::parse(file, parser_callback, true, true).at(Configuration::KEY);// 5. 將最終解析得到的完整 JSON 對象(包含了所有內嵌子配置)// 反序列化為 DRAMSys::Config::Configuration C++ 結構體。// 這需要 Configuration 結構體及其所有嵌套結構體(如 MemSpec、AddressMapping 等)// 都使用了 nlohmann::json 的 `NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE` 或類似的機制進行了序列化/反序列化定義。return simulation.get<Config::Configuration>();}// 6. 文件打開失敗的錯誤處理throw std::runtime_error("Failed to open file " + std::string(baseConfig));
}} // namespace DRAMSys::Config
1.2 構造過程總結
- 打開主配置文件: 函數首先嘗試打開
baseConfig
指定的主 JSON 配置文件。 - 獲取基礎目錄: 記錄主配置文件的父目錄
baseDir
,這對于構建子配置文件的相對路徑至關重要。 - 定義
SubConfig
枚舉: 這是一個內部枚舉,用于在解析過程中識別當前處理的是哪種類型的子配置(例如 MemSpec、AddressMapping 等)。 - 核心:自定義解析回調
parser_callback
:- 這個回調函數是整個機制的核心。它會在
nlohmann::json
解析 JSON 文件時,針對不同的事件(如遇到鍵、遇到值)和深度被調用。 - 識別子配置鍵: 當解析深度為 2 且事件為
key
時(例如解析到"MemSpec"
),回調會根據鍵名設置current_sub_config
變量,以識別當前要加載的子配置類型。 - 替換文件名字符串為 JSON 對象: 當解析深度為 2 且事件為
value
時(此時parsed
變量是子配置文件的文件名字符串,例如"memory.json"
),回調會執行以下操作:- 構建子配置文件的完整路徑(
baseDir
+filename
)。 - 遞歸調用
json_t::parse
: 打開并解析這個子配置文件。關鍵在于,這里再次傳入了parser_callback
。這意味著如果子配置文件內部也引用了其他子配置文件,這個機制可以遞歸地處理它們,實現多層級的配置嵌套。 - 從解析后的子 JSON 中提取出對應子配置根鍵下的內容(例如
{"MemSpec": {...}}
中{...}
部分)。 - 將
parsed
變量(原始的文件名字符串)替換為這個新解析出來的子 JSON 對象。
- 構建子配置文件的完整路徑(
- 這個回調函數是整個機制的核心。它會在
- 啟動主文件解析: 調用
json_t::parse(file, parser_callback, true, true)
來啟動對主配置文件的解析,并將parser_callback
應用于整個解析過程。 - 反序列化為 C++ 對象: 一旦整個 JSON 對象(包括所有內嵌的子配置)被成功解析和組裝,最后一步是調用
.get<Config::Configuration>()
。這會將完整的nlohmann::json::json_t
對象反序列化(deserialize)成DRAMSys::Config::Configuration
結構體的實例。這要求Configuration
結構體及其所有成員(例如MemSpec
,AddressMapping
,McConfig
等結構體)都必須使用nlohmann/json
提供的宏(如NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE
)或者自定義的to_json
/from_json
函數進行了注冊,以便nlohmann/json
知道如何將 JSON 數據映射到 C++ 類型。
這種設計的好處
- 模塊化和可重用性: 不同的子配置(如內存規格、地址映射)可以存儲在獨立的文件中,便于管理和在不同項目中重用。
- 清晰的結構: 主配置文件可以像目錄一樣,組織和引用各個部分的配置。
- 靈活性: 支持多層級的配置嵌套。
- 解耦: 允許配置文件的編寫者專注于特定模塊的參數,而不需要在一個巨大的文件中定義所有內容。
- 易于擴展: 當需要添加新的配置類型時,只需在
SubConfig
枚舉中添加新類型,并在回調中增加相應的處理邏輯即可。
這個 DRAMSysConfiguration.cpp
文件展示了 nlohmann/json
庫在復雜配置管理方面的強大應用,特別是其解析回調機制,使得實現動態加載和組裝配置成為可能。
2 Simulator 例化過程
- 首先通過
teminateInitiator
設置了終止仿真條件。 - 通過
finishTransaction
設置了仿真進度條。 - 關鍵是如何設置仿真的激勵,即trace
2.1 instantiateInitiator
??
Simulator通過configure配置發起方針的initiator,這里的initiator可以有多個,有多少個取決于配置的json文件;
??
instantiateInitiator函數本身并不是一個構造函數,而是一個負責根據配置創建不同類型Initiator對象(流量發起器)的工廠方法。它根據不同的配置創建對應的流量發起器。
??
使用std::visit來處理std::variant類型的配置(這里initiator為什么是std::variant類型的配置?)
-
獲取公共參數: 在開始創建具體的 Initiator 之前,函數首先從 dramSys 模塊獲取一些所有發起器都可能需要的公共參數,例如模擬內存的總大小、DRAM 接口的時鐘周期以及默認的每次突發傳輸的字節數。
-
std::variant 和 std::visit 的使用:
-
DRAMSys::Config::Initiator 結構體內部包含一個 std::variant 成員(通過 initiator.getVariant() 訪問),這個 variant 可以持有不同類型的發起器配置(TrafficGenerator, TracePlayer, RowHammer)。
-
std::visit 是 C++17 引入的一個工具,它允許你對 std::variant 中當前激活的類型執行相應的操作。它會根據 variant 中實際存儲的類型,調用 lambda 表達式中對應的 if constexpr 分支。
-
-
類型判別與實例化:
-
if constexpr 語句在編譯時判斷 config 的具體類型 (T)。
-
TrafficGenerator: 如果配置是 TrafficGenerator 或 TrafficGeneratorStateMachine 類型,它會直接創建并返回一個 TrafficGenerator 對象,并傳入其特有的配置以及公共的模擬參數、內存管理器和回調函數。
-
TracePlayer: 如果配置是 TracePlayer 類型,函數會進一步根據軌跡文件的擴展名(.stl 或 .rstl)來確定軌跡類型(絕對時間或相對時間)。然后,它會創建一個 StlPlayer 對象(負責讀取和解析軌跡文件),并將其包裝在一個 SimpleInitiator 中返回。SimpleInitiator 是一個更通用的發起器模板,可以適配不同的流量源。
-
RowHammer: 如果配置是 RowHammer 類型,它會創建一個 RowHammer 對象(實現 Row Hammer 邏輯),同樣將其包裝在一個 SimpleInitiator 中返回。
-
-
返回 std::unique_ptr: 無論是哪種類型的發起器,instantiateInitiator 函數最終都會返回一個 std::unique_ptr。這意味著它返回一個指向基類 Initiator 的智能指針,確保了內存的安全管理和多態性,允許 Simulator 以統一的方式管理不同類型的發起器。
簡而言之,instantiateInitiator 函數是一個動態的工廠,它根據配置文件中指定的確切類型,“構造”出并返回相應功能的流量發起器對象,這些對象都以 Initiator 接口的形式提供給模擬器使用。