gem5のサイクル精度モデルについて理解したいので、O3CPUのドキュメントを読んでみることにする。
ソースコードを読みながら、実際の流れをつかんでいこうと思う。
CPU::tick()
CPU::tick()
の内部で、各モジュールが1サイクルずつ動作しているということになる。
fetch.tick()
decode.tick()
rename.tick()
iew.tick()
commit.tick()
が呼ばれているということになる。
// src/cpu/o3/cpu.cc CPU::tick() { DPRINTF(O3CPU, "\n\nO3CPU: Ticking main, O3CPU.\n"); assert(!switchedOut()); assert(drainState() != DrainState::Drained); ++baseStats.numCycles; updateCycleCounters(BaseCPU::CPU_STATE_ON); // activity = false; //Tick each of the stages fetch.tick(); decode.tick(); rename.tick(); iew.tick(); commit.tick(); // Now advance the time buffers timeBuffer.advance(); fetchQueue.advance(); decodeQueue.advance(); renameToIEWQueue.advance(); renameToROBQueue.advance(); iewQueue.advance(); activityRec.advance();
fetch::tick()
void Fetch::tick() { /* ... 途中省略 ... */ for (threadFetched = 0; threadFetched < numFetchingThreads; threadFetched++) { // Fetch each of the actively fetching threads. fetch(status_change); } /* ... 途中省略 .. */
decode::tick()
void Decode::tick() { /* ... 途中省略 ... */ //Check stall and squash signals. while (threads != end) { ThreadID tid = *threads++; DPRINTF(Decode,"Processing [tid:%i]\n",tid); status_change = checkSignalsAndUpdate(tid) || status_change; decode(status_change, tid); } /* ... 途中省略 ... */
rename::tick()
void Rename::tick() { /* ... 途中省略 ... */ // Check stall and squash signals. while (threads != end) { ThreadID tid = *threads++; DPRINTF(Rename, "Processing [tid:%i]\n", tid); readFreeEntries(tid); readStallSignals(tid); status_change = checkSignalsAndUpdate(tid) || status_change; rename(status_change, tid); if (!checkSendDispatchStall(tid)) { sendToDispatch(tid); } } /* ... 途中省略 ... */
IEW::tick()
void IEW::tick() { /* ... 途中省略 ... */ // Check stall and squash signals, dispatch any instructions. while (threads != end) { ThreadID tid = *threads++; DPRINTF(IEW,"Issue: Processing [tid:%i]\n",tid); checkSignalsAndUpdate(tid); dispatch(tid); } if (exeStatus != Squashing) { executeInsts(); writebackInsts(); // 命令キューに、準備ができた命令をスケジューリングさせる。 // (実際には、このスケジューリングは次のサイクルで // 実行される命令のためのものである)。 instQueue.scheduleReadyInsts(); // また、ステージが走った場合は、それ自身のタイムバッファを進めるべきである。 // 最適な場所ではないが、(うまくいけば)これでうまくいく。 issueToExecQueue.advance(); }
Commit.tick()
void Commit::commit() { /* ... 途中省略 ... */ if (num_squashing_threads != numThreads) { // If we're not currently squashing, then get instructions. getInsts(); // 命令をコミットする commitInsts(); } /* ... 途中省略 ... */ }
Load命令のための必要な関数群をトレースする
IEW**::**tick()**->**IEW**::**executeInsts()
// src/cpu/o3/iew.cc void IEW::executeInsts() { /* ... 途中省略 ... */ // Execute/writeback any instructions that are available. int insts_to_execute = fromIssue->size; int inst_num = 0; for (; inst_num < insts_to_execute; ++inst_num) { /* ... 途中省略 ... */ // Execute instruction. // Note that if the instruction faults, it will be handled // at the commit stage. if (inst->isMemRef()) { DPRINTF(IEW, "Execute: Calculating address for memory " "reference.\n"); /* ... 途中省略 ... */ } else if (inst->isLoad()) { // Loads will mark themselves as executed, and their writeback // event adds the instruction to the queue to commit fault = ldstQueue.executeLoad(inst); if (inst->isTranslationDelayed() && fault == NoFault) { // A hw page table walk is currently going on; the // instruction must be deferred. DPRINTF(IEW, "Execute: Delayed translation, deferring " "load.\n"); instQueue.deferMemInst(inst); continue; }
ldstQueue
のexecuteLoad()
を見てみる。
// src/cpu/o3/lsq.cc Fault LSQ::executeLoad(const DynInstPtr &inst) { ThreadID tid = inst->threadNumber; return thread[tid].executeLoad(inst); }
今度はLSQUnit
のexecuteLoad()
が呼ばれるようだ。
// src/cpu/o3/lsq_unit.cc Fault LSQUnit::executeLoad(const DynInstPtr &inst) { // Execute a specific load. Fault load_fault = NoFault; DPRINTF(LSQUnit, "Executing load PC %s, [sn:%lli]\n", inst->pcState(), inst->seqNum); assert(!inst->isSquashed()); load_fault = inst->initiateAcc();
initiateAcc()というのは何をやっているのだろうか。
// src/cpu/o3/dyn_inst.cc Fault DynInst::initiateAcc() { // @todo: Pretty convoluted way to avoid squashing from happening // when using the TC during an instruction's execution // (specifically for instructions that have side-effects that use // the TC). Fix this. bool no_squash_from_TC = thread->noSquashFromTC; thread->noSquashFromTC = true; fault = staticInst->initiateAcc(this, traceData); thread->noSquashFromTC = no_squash_from_TC; return fault; }
これは、各命令で実装がされていないと駄目な感じかな。
virtual Fault initiateAcc(ExecContext *xc, Trace::InstRecord *traceData) const { panic("initiateAcc not defined!"); }
以下のようなソースコードだろうか:
arch/riscv/insts/static_inst.hh arch/riscv/isa/formats/amo.isa arch/riscv/isa/formats/mem.isa arch/riscv/isa/templates/vector_mem.isa
arch/riscv/isa/formats/mem.isa
うーん、この辺かな。
def template LoadInitiateAcc {{ Fault %(class_name)s::initiateAcc(ExecContext *xc, Trace::InstRecord *traceData) const { Addr EA; %(op_src_decl)s; %(op_rd)s; %(ea_code)s; return initiateMemRead(xc, traceData, EA, Mem, memAccessFlags); } }};
buildディレクトリにいろいろできているな。これは単純にメモリアクセスをするための命令ができているということらしい。
// RISCV/arch/riscv/generated/exec-ns.cc.inc Fault Ld::initiateAcc(ExecContext *xc, Trace::InstRecord *traceData) const { Addr EA; uint64_t Rs1 = 0; int64_t Mem = {}; ; Rs1 = xc->getRegOperand(this, 0); ; EA = Rs1 + offset;; return initiateMemRead(xc, traceData, EA, Mem, memAccessFlags); }
initiateMemRead()
はcpu/o3/dyn_inst.cc
のDynInst::initiateMemRead()
がそれに相当すると思う。
// src/cpu/o3/dyn_inst.cc Fault DynInst::initiateMemRead(Addr addr, unsigned size, Request::Flags flags, const std::vector<bool> &byte_enable) { assert(byte_enable.size() == size); return cpu->pushRequest( dynamic_cast<DynInstPtr::PtrType>(this), /* ld */ true, nullptr, size, addr, flags, nullptr, nullptr, byte_enable); }
pushRequest()
により実際のメモリアクセスがリクエストされるようだ。
// src/cpu/o3/cpu.hh /** CPU pushRequest function, forwards request to LSQ. */ Fault pushRequest(const DynInstPtr& inst, bool isLoad, uint8_t *data, unsigned int size, Addr addr, Request::Flags flags, uint64_t *res, AtomicOpFunctorPtr amo_op = nullptr, const std::vector<bool>& byte_enable=std::vector<bool>()) { return iew.ldstQueue.pushRequest(inst, isLoad, data, size, addr, flags, res, std::move(amo_op), byte_enable); }
read()を呼び出している。これは何をしているのかな?
// src/cpu/o3/lsq.cc Fault LSQ::pushRequest(const DynInstPtr& inst, bool isLoad, uint8_t *data, unsigned int size, Addr addr, Request::Flags flags, uint64_t *res, AtomicOpFunctorPtr amo_op, const std::vector<bool>& byte_enable) { /* ... 途中省略 ... */ /* This is the place were instructions get the effAddr. */ if (request->isTranslationComplete()) { if (request->isMemAccessRequired()) { inst->effAddr = request->getVaddr(); inst->effSize = size; inst->effAddrValid(true); if (cpu->checker) { inst->reqToVerify = std::make_shared<Request>(*request->req()); } Fault fault = NoFault; Fault fault2 = NoFault; if (isLoad) // LSQ::read()を呼び出す fault = read(request, inst->lqIdx); else { fault = write(request, data, inst->sqIdx); if (request_repeat != nullptr) fault2 = write(request_repeat, data, inst->sqIdx); }
各スレッド(lsq_unit
)に応じて、read()
を呼び出している。
// src/cpu/o3/lsq.cc Fault LSQ::read(LSQRequest* request, ssize_t load_idx) { assert(request->req()->contextId() == request->contextId()); ThreadID tid = cpu->contextToThread(request->req()->contextId()); return thread.at(tid).read(request, load_idx); }
LSQUnit::read()では、リクエストのパケット作成と、そのパケットの送出を行っている。
// src/cpu/o3/lsq_unit.cc Fault LSQUnit::read(LSQRequest *request, ssize_t load_idx) { /* ... 途中省略 ... */ // if we the cache is not blocked, do cache access request->buildPackets(); request->sendPacket(); if (!request->isSent()) iewStage->blockMemInst(load_inst); return NoFault; }
ここまででinitiateAcc()の処理は終了となる。
次に、LSQUnit::executeLoad()
において、ロード命令によるcheckViolation()
が実行される。
// src/cpu/o3/lsq_unit.cc Fault LSQUnit::executeLoad(const DynInstPtr &inst) { /* ... 途中省略 ... */ } else { if (inst->effAddrValid()) { auto it = inst->lqIt; ++it; if (checkLoads) return checkViolations(it, inst); } } return load_fault; } Fault LSQUnit::checkViolations(typename LoadQueue::iterator& loadIt, const DynInstPtr& inst) { /* ... 途中省略 ... */ }
ここまでで実行パイプラインによる処理は終了で、今度はDCachePortクラス側からのデータ取得が行われる。これはどこから呼ばれているのだろう?
// src/cpu/o3/lsq.cc bool LSQ::DcachePort::recvTimingResp(PacketPtr pkt) { return lsq->recvTimingResp(pkt); }
そして、completeAcc()
が呼ばれるらしい。これもbuild/
ディレクトリ以下で自動的に生成されるらしい。
// build/RISCV/arch/riscv/generated/exec-ns.cc.inc Fault Ld::completeAcc(PacketPtr pkt, ExecContext *xc, Trace::InstRecord *traceData) const { int64_t Rd = 0; int64_t Mem = {}; getMemLE(pkt, Mem, traceData); Rd = Mem; { RegVal final_val = Rd; xc->setRegOperand(this, 0, final_val); if (traceData) { traceData->setData(final_val); } }; return NoFault; }