FPGA開発日記

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

gem5のドキュメントを読む (2. O3CPUのサイクル計算の挙動をソースコード上で追いかける)

gem5のサイクル精度モデルについて理解したいので、O3CPUのドキュメントを読んでみることにする。

www.gem5.org

ソースコードを読みながら、実際の流れをつかんでいこうと思う。

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;
                }

ldstQueueexecuteLoad()を見てみる。

// src/cpu/o3/lsq.cc
Fault
LSQ::executeLoad(const DynInstPtr &inst)
{
    ThreadID tid = inst->threadNumber;

    return thread[tid].executeLoad(inst);
}

今度はLSQUnitexecuteLoad()が呼ばれるようだ。

// 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.ccDynInst::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;
    }