FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (35. 末尾関数呼び出し最適化を実装する1)

f:id:msyksphinz:20190425001356p:plain

関数呼び出しには様々な最適化の形があるが、その中の一つである末尾関数呼び出しでの最適化を実行してみる。末尾関数呼び出しとは、ある関数f1()が関数f2()の最後の処理として呼び出されるケースを言う。

int f1(a, b) { a + b; }
int f2(c, d, e, f) {
    /* 様々な処理*/
    return f1(10, 20);
}

この場合、関数f1()を呼び出すためには引数渡しの処理、スタックの処理、そして呼び出しから戻ってきた場合には戻り値の処理やスタックの後片付けを行う必要がある。

f:id:msyksphinz:20190704233620p:plain
通常のスタックフレームの作成手順。関数が呼び出されると新たなスタックフレームが作成される。

しかし、場合によってはこれが不要なことがある。つまり、どっちにしろf1から戻った後はf2からも戻ってしまうのだから、せっかくなのでf2のスタック領域などもf1で使わせてもらって、関数呼び出し時のスタックの掘り下げを省略してしまいます。つまり、末尾呼び出しではスタックフレームを追加することなくf1を呼び出すことができる。

f:id:msyksphinz:20190704233655p:plain
Tail Callのスタックフレームの作成手順。スタックフレームは新たに作成されず、引数の調整だけを行って関数に飛ぶ。

しかし、この末尾呼び出しはすべての条件下で実現可能なわけではない。上記で説明した通り、f2のスタックフレームを使用してf1が実行される。つまり、f1が使う予定であるスタックフレームのサイズがf2のスタックフレームのサイズを超えていてはいけない。また、f1のスタックフレームのサイズが最初から算出できない場合にも末尾関数呼び出し最適化は適用できない。f1のスタックフレーム以外の場所まで破壊してしまう可能性があるからだ。

では、実際にLLVMで末尾関数呼び出しの最適化を実行してみる。clangでは-O1オプションを付加することで末尾関数呼び出し最適化を施したIRを生成することができる。以下のプログラムをclangでIRを生成してみる。

int tail_call_func(int a, int b) {
    return a + b;
}
int tail_call_main (int a, int b, int c, int d) {
    int e = a + b;
    int f = c + d;
    return tail_call_func(e, f);  /* 末尾関数呼び出し */
}
./bin/clang -O1 --target=mips-unknown-elf tailcall.cpp -c -emit-llvm -o tailcall.o1.bc
./bin/clang -O0 --target=mips-unknown-elf tailcall.cpp -c -emit-llvm -o tailcall.o0.bc

生成されたバイナリをダンプしてみると、tailcall.o1の方ではf1の呼び出しの部分がtail callとなっていることが分かる。

./bin/llvm-dis tailcall.o1.bc -o -
  %sub = sub nsw i32 %a, %b
  %mul = mul nsw i32 %d, %c
  /* Tail Call が使用されている */
  %call = tail call i32 @_Z14tail_call_funcii(i32 signext %sub, i32 signext %mul)
  ret i32 %call
./bin/llvm-dis tailcall.o0.bc -o -
  /* -O0なので最適化が施されていない */
  store i32 %sub, i32* %e, align 4
...
  %mul = mul nsw i32 %2, %3
...
  /* 普通のCallが使用されている */
  %call = call i32 @_Z14tail_call_funcii(i32 signext %4, i32 signext %5)
  ret i32 %call

では、このIRを処理して末尾関数呼び出し最適化を適用してみる。実装しなければならないのは、

  • 末尾関数呼び出し最適化が適用可能かチェックする
  • スタックフレームの生成を実行しないように制御
  • 引数渡しの処理を行う
  • 関数ジャンプ
  • 戻り値処理

と、とりあえずは関数呼び出しのシーケンスの部分に手を加えれば良さそうだ。これらはすべてIRからDAGへの変換時に適用できるので、MYRISCVXISelLowering.cppに記述されているMYRISCVXTargetloweringクラスに手を加える。関数呼び出しの部分なので、MYRISCVXTargetLowering:::LowerCallを改造すれば良さそうだ。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
//@LowerCall {
/// LowerCall - functions arguments are copied from virtual regs to
/// (physical regs)/(stack frame), CALLSEQ_START and CALLSEQ_END are emitted.
SDValue
MYRISCVXTargetLowering::LowerCall(TargetLowering::CallLoweringInfo &CLI,
                                  SmallVectorImpl<SDValue> &InVals) const {
...
  bool &IsTailCall                      = CLI.IsTailCall;
...
  // Check if it's really possible to do a tail call.
  if (IsTailCall)
    IsTailCall =
        isEligibleForTailCallOptimization(CCInfo, NextStackOffset,
                                          *MF.getInfo<MYRISCVXFunctionInfo>());
  if (!IsTailCall && CLI.CS && CLI.CS.isMustTailCall())
    report_fatal_error("failed to perform tail call elimination on a call "
                       "site marked musttail");
...

IsTailCall変数は関数呼び出しが末尾関数呼び出しになっている場合にまずはTrueとなる。次に、本当に末尾関数呼び出し最適化が適用可能かチェックする。これを行っているのがisEligibleForTailCallOptimizationだ。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXSEISelLowering.cpp
bool MYRISCVXSETargetLowering::
isEligibleForTailCallOptimization(const CCState &CCInfo,
                                  unsigned NextStackOffset,
                                  const MYRISCVXFunctionInfo& FI) const {
  if (!EnableMYRISCVXTailCalls)
    return false;

  // Return false if either the callee or caller has a byval argument.
  if (CCInfo.getInRegsParamsCount() > 0 || FI.hasByvalArg())
    return false;

  // Return true if the callee's argument area is no larger than the
  // caller's.
  return NextStackOffset <= FI.getIncomingArgSize();
}

末尾関数呼び出し最適化が適用できる条件としては、

  • EnableMYRISCVXTailCallsがTrue、つまりllcのオプションで-enable-MYRISCVX-tail-callsがTrueになっていないとダメだ。
  • 呼び出し先もしくは呼び出し元がbyvalの引数を持っている場合はダメだ。
  • 呼び出し先が使用する予定のスタックサイズ(NextStackOffset)よりも呼び出し元が持っているスタックのサイズFI.getIncomingArgSize()が大きい場合はダメだ。

上記をすべてクリアできれば末尾関数呼び出し最適化が適用できる。

では、LowerCallに戻ります。IsTailCallがTrueとなり、末尾関数呼び出し最適化が有効として読み進める。

...
  if (!IsTailCall)
    Chain = DAG.getCALLSEQ_START(Chain, NextStackOffset, 0, DL);
...

末尾関数呼び出し最適化が有効の場合は、新たにスタックフレームを作らないのでCALLSEQ_STARTは生成しない。

  /* 引数渡しのDAG生成部 */
  for (unsigned i = 0, e = ArgLocs.size(); i != e; ++i) {
...
    MemOpChains.push_back(passArgOnStack(StackPtr, VA.getLocMemOffset(),
                                         Chain, Arg, DL, IsTailCall, DAG));
  }

スタック渡しの引数の場合は、少し細工をする。

SDValue
MYRISCVXTargetLowering::passArgOnStack(SDValue StackPtr, unsigned Offset,
                                       SDValue Chain, SDValue Arg, const SDLoc &DL,
                                       bool IsTailCall, SelectionDAG &DAG) const {
  if (!IsTailCall) {
...
  }

  MachineFrameInfo &MFI = DAG.getMachineFunction().getFrameInfo();
  int FI = MFI.CreateFixedObject(Arg.getValueSizeInBits() / 8, Offset, false);
  SDValue FIN = DAG.getFrameIndex(FI, getPointerTy(DAG.getDataLayout()));
  return DAG.getStore(Chain, DL, Arg, FIN, MachinePointerInfo(),
                      /* Alignment = */ 0, MachineMemOperand::MOVolatile);
}

LowerCallに戻ります。TailCallノードを挿入する。

  if (IsTailCall)
    return DAG.getNode(MYRISCVXISD::TailCall, DL, MVT::Other, Ops);

上記で使用したMYRISCVXISD::TailCallノードも定義しておく必要がある。これはMYRISCVXInstrInfo.tdで新たなノードとして定義する。MYRISCVXCallSeqStartMYRISCVXCallSeqEndのように、TailCallノードもノードとして定義しておく(後で除去するため)。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
// Tail call
def MYRISCVXTailCall : SDNode<"MYRISCVXISD::TailCall", SDT_MYRISCVXCall,
                               [SDNPHasChain, SDNPOptInGlue, SDNPVariadic]>;
...
let isCall = 1, isTerminator = 1, isReturn = 1, isBarrier = 1, hasDelaySlot = 0,
    hasExtraSrcRegAllocReq = 1 in {
  class TailCall<Instruction JumpInst> :
    MYRISCVXPseudo<(outs), (ins calltarget:$target), "", []>,
    PseudoInstExpansion<(JumpInst calltarget:$target)>;

  class TailCallReg<RegisterClass RO, Instruction JRInst,
    RegisterClass ResRO = RO> :
    MYRISCVXPseudo<(outs), (ins RO:$rs), "", [(MYRISCVXTailCall RO:$rs)]>,
    PseudoInstExpansion<(JRInst ResRO:$rs)>;
}

TailCallTailCallRegを定義しました。通常はTailCallのみで良いはずだが、PIC時にレジスタ経由でのジャンプすることも想定して、TailCallRegを作っておく。どちらも実際は疑似命令で、最終的にはJAL命令もしくはJALRに置き換えられる。

def TAILCALL : TailCall<JAL>;
def TAILCALL_R : TailCallReg<GPR, JALR>;

TAILCALL側にはDAG生成パタンを登録していないので、パタンを登録しておく。

def : Pat<(MYRISCVXTailCall (iPTR tglobaladdr:$dst)),
                             (TAILCALL tglobaladdr:$dst)>;
def : Pat<(MYRISCVXTailCall (iPTR texternalsym:$dst)),
                             (TAILCALL texternalsym:$dst)>;

LLVMのバックエンドを作るための第一歩 (34. 関数コールと戻り値に関するDAGを生成する)

f:id:msyksphinz:20190425001356p:plain

3. 関数呼び出しのDAG生成

次に、ジャンプ先となる関数のアドレスを計算する。PICモードでコンパイルしている場合はGOT経由でのアドレスを計算、そうでない場合はっ直接アドレスの計算を行う。これをGlobalAddressSDNode, ExternalSymbolSDNodeのタイプに対してそれぞれ行う。

  if (GlobalAddressSDNode *G = dyn_cast<GlobalAddressSDNode>(Callee)) {
    if (IsPICCall) {
      const GlobalValue *Val = G->getGlobal();
      InternalLinkage = Val->hasInternalLinkage();

      if (InternalLinkage)
        Callee = getAddrLocal(G, Ty, DAG);
      else
        Callee = getAddrGlobal(G, Ty, DAG, MYRISCVXII::MO_GOT_CALL, Chain,
                               FuncInfo->callPtrInfo(Val));
    } else
      Callee = DAG.getTargetGlobalAddress(G->getGlobal(), DL,
                                          getPointerTy(DAG.getDataLayout()), 0,
                                          MYRISCVXII::MO_NO_FLAG);
    GlobalOrExternal = true;
  } else if (ExternalSymbolSDNode *S = dyn_cast<ExternalSymbolSDNode>(Callee)) {
    const char *Sym = S->getSymbol();

    if (!IsPIC) // static
      Callee = DAG.getTargetExternalSymbol(Sym,
                                           getPointerTy(DAG.getDataLayout()),
                                           MYRISCVXII::MO_NO_FLAG);
    else // PIC
      Callee = getAddrGlobal(S, Ty, DAG, MYRISCVXII::MO_GOT_CALL, Chain,
                             FuncInfo->callPtrInfo(Sym));

    GlobalOrExternal = true;
  }

最後に、MYRISCVXISD::CALLノードを接続して、実際に関数ジャンプを行うノードをDAGに接続する。

  Chain = DAG.getNode(MYRISCVXISD::CALL, DL, NodeTys, Ops);

最初に、CALLSEQ_STARTノードを挿入したので、ここまでで関数ジャンプのDAGが終わりであることを示すCALLSEQ_ENDノードを挿入する。

  // Create the CALLSEQ_END node.
  Chain = DAG.getCALLSEQ_END(Chain, NextStackOffsetVal,
                             DAG.getIntPtrConstant(0, DL, true), InFlag, DL);

4. 戻り値取得のDAG生成

戻り値の処理だが、LowerCallResult関数を作成してここだけ別の関数に切り出する。LowerCallResultで行わなければならないのは関数から戻ってきたときの戻り値を受け取ってしかるべき場所に格納するという処理だ。

/// LowerCallResult - Lower the result values of a call into the
/// appropriate copies out of appropriate physical registers.
SDValue
MYRISCVXTargetLowering::LowerCallResult(SDValue Chain, SDValue InFlag,
                                        CallingConv::ID CallConv, bool IsVarArg,
                                        const SmallVectorImpl<ISD::InputArg> &Ins,
                                        const SDLoc &DL, SelectionDAG &DAG,
                                        SmallVectorImpl<SDValue> &InVals,
                                        const SDNode *CallNode,
                                        const Type *RetTy) const {
...
  // Assign locations to each value returned by this call.
  SmallVector<CCValAssign, 16> RVLocs;
  CCState CCInfo(CallConv, IsVarArg, DAG.getMachineFunction(),
                 RVLocs, *DAG.getContext());
  const ExternalSymbolSDNode *ES =
      dyn_cast_or_null<const ExternalSymbolSDNode>(CallNode);
  CCInfo.AnalyzeCallResult(Ins, RetCC_MYRISCVX);
 ...

AnalyzeCallResultを呼んで、関数の戻り値を格納するレジスタの解析を行う。AnalyzeFormalArguments()と構成は似ており、呼び出し規約RetCC_MYRISCVXを呼び出して解析に使用している。念のためRetCC_MYRISCVXの中身を確認してみると、RetCC_MYRISCVXEABIを呼び出しており、RetCC_MYRISCVXEABIでは戻り値レジスタとしてA0とA1レジスタを使用することが明記されている。

  • llvm-myriscvx/lib/CodeGen/CallingConvLower.cpp
/// Analyze the return values of a call, incorporating info about the passed
/// values into this state.
void CCState::AnalyzeCallResult(const SmallVectorImpl<ISD::InputArg> &Ins,
                                CCAssignFn Fn) {
  for (unsigned i = 0, e = Ins.size(); i != e; ++i) {
    MVT VT = Ins[i].VT;
    ISD::ArgFlagsTy Flags = Ins[i].Flags;
    if (Fn(i, VT, VT, CCValAssign::Full, Flags, *this)) {
#ifndef NDEBUG
      dbgs() << "Call result #" << i << " has unhandled type "
             << EVT(VT).getEVTString() << '\n';
#endif
      llvm_unreachable(nullptr);
    }
  }
}
  • build-myriscvx/lib/Target/MYRISCVX/MYRISCVXGenCallingConv.inc
static bool RetCC_MYRISCVXEABI(unsigned ValNo, MVT ValVT,
                               MVT LocVT, CCValAssign::LocInfo LocInfo,
                               ISD::ArgFlagsTy ArgFlags, CCState &State) {

  if (LocVT == MVT::i32) {
    static const MCPhysReg RegList1[] = {
      MYRISCVX::A0, MYRISCVX::A1
    };
    if (unsigned Reg = State.AllocateReg(RegList1)) {
      State.addLoc(CCValAssign::getReg(ValNo, ValVT, Reg, LocVT, LocInfo));
      return false;
    }
  }

  return true;  // CC didn't match.
}

解析後、すべての戻り値(といってもC言語の場合は戻り値は1つだけだが)において、DAGを生成する。getCopyFromReg()のDAGを生成して物理レジスタからノードにデータをコピーして、その後のDAGに接続できるようにしている。RVLocs[i].getValVT() != RVLocs[i].getLocVT()は、格納しているレジスタの場所とデータの実体の型が合っていない場合に、BITCASTノードを接続して型の拡張を行うためのDAGだ。

最終的に、生成されたDAGは以下のようになった。それぞれのステップで挿入したノードに、赤い四角を書いている。

f:id:msyksphinz:20190630142116p:plain
関数コールのために生成されたDAG

LLVMのバックエンドを作るための第一歩 (33. 関数コールに関するLLVM IRをDAGに変換する)

f:id:msyksphinz:20190425001356p:plain

関数コールに関するLLVM IRをDAGに変換するためには、MYRISCVXTaregtLowering::LowerCallを実装する。LowerCallの役割を理解するために、LowerCallのコメントを読んでみる。

  • llvm-myriscvx/include/llvm/CodeGen/TargetLowering.h
  /// This hook must be implemented to lower calls into the specified
  /// DAG. The outgoing arguments to the call are described by the Outs array,
  /// and the values to be returned by the call are described by the Ins
  /// array. The implementation should fill in the InVals array with legal-type
  /// return values from the call, and return the resulting token chain value.
  /// このフックは関数コールを特定のDAGに変換するために実装する必要がある。
  /// 関数に渡される引数はOuts配列に記述されており、関数から戻される値はIns配列に記述されている。
  /// 実装では、InVals配列に関数からの正しい型の戻り値を埋める必要があり、トークンチェインの値を戻す必要がある。
  virtual SDValue
    LowerCall(CallLoweringInfo &/*CLI*/,
              SmallVectorImpl<SDValue> &/*InVals*/) const {
    llvm_unreachable("Not Implemented");
  }

LowerCallの中で実装しなければならないことは、具体的には以下になる。

  1. 引数を解析し、レジスタ経由で渡すかスタック経由で渡すかなどの情報を決定する。
  2. 全ての引数について、レジスタ経由渡しの場合は引数のレジスタコピーのDAGを生成する。スタック渡しの場合は適切なスタックスロットに値をコピーするDAGを生成する。
  3. 呼び出し先関数の場所を取得するためのDAGを生成する。呼び出し先の関数アドレスに対してMYRISCVXISD::CALLノードを生成し、関数へのジャンプするDAGを生成する。
  4. 戻り値を扱うためのDAGを生成する。これには、LowerCallResultという別の関数を定義する。
f:id:msyksphinz:20190630142009p:plain
LowerCallとLowerCallResultで実施される処理の概要

1. 引数の解析

引数の解析はLowerFormalArgumentsでも取り上げた。LowerFormalArgumentsでは引数毎にレジスタから取るか、スタックから取るかを解析したが、LowerCallの場合はその逆で、引数毎にレジスタに値を置くか、スタックに置くかを解析する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
SDValue
MYRISCVXTargetLowering::LowerCall(TargetLowering::CallLoweringInfo &CLI,
                                  SmallVectorImpl<SDValue> &InVals) const {
...
  // Analyze operands of the call, assigning locations to each operand.
  SmallVector<CCValAssign, 16> ArgLocs;
  CCState CCInfo(CallConv, IsVarArg, DAG.getMachineFunction(),
                 ArgLocs, *DAG.getContext());
  CCInfo.AnalyzeCallOperands (Outs, CC_MYRISCVX);
...

CCInfoの中に、引数の渡し方についての情報が格納された。これを使って引数渡しのためのDAGを構築していくのだが、その前にまずは関数コールのためのお膳立てをする。

2. 引数渡し用のDAGの生成

まず、CALLSEQ_STARTDAGノードを生成して、このノードから関数コールのためのDAGが始まるということを宣言する。

Chain = DAG.getCALLSEQ_START(Chain, NextStackOffset, 0, DL);

ここから先は、引数渡しのためのDAGを作っていく。そのために、引数の数だけループを回する。

  for (unsigned i = 0, e = ArgLocs.size(); i != e; ++i) {
    SDValue Arg = OutVals[i];
    CCValAssign &VA = ArgLocs[i];
    MVT LocVT = VA.getLocVT();
    ISD::ArgFlagsTy Flags = Outs[i].Flags;
...
    // Arguments that can be passed on register must be kept at
    // RegsToPass vector
    if (VA.isRegLoc()) {
      RegsToPass.push_back(std::make_pair(VA.getLocReg(), Arg));
      continue;
    }

レジスタ渡しの引数の場合は、RegsToPassに引数の情報を詰め込んでいく。RegsToPassstd::pair<unsigned, SDValue>のキューになっており、引数私に使用されるレジスタの番号と引数そのものをペアで格納していく。

    // Arguments that can be passed on register must be kept at
    // RegsToPass vector
    if (VA.isRegLoc()) {
      RegsToPass.push_back(std::make_pair(VA.getLocReg(), Arg));
      continue;
    }

スタック渡しの引数の場合もMemOpChainsに引数を積み上げていく。passArgOnStack()は、具体的にはスタックポインタのアドレス計算のためのDAGを生成して、それを元にストア命令のためのDAGを生成する。

    // Register can't get to this point...
    assert(VA.isMemLoc());

    // emit ISD::STORE whichs stores the
    // parameter value to a stack Location
    MemOpChains.push_back(passArgOnStack(StackPtr, VA.getLocMemOffset(),
                                         Chain, Arg, DL, IsTailCall, DAG));
SDValue
MYRISCVXTargetLowering::passArgOnStack(SDValue StackPtr, unsigned Offset,
                                       SDValue Chain, SDValue Arg, const SDLoc &DL,
                                       bool IsTailCall, SelectionDAG &DAG) const {
    SDValue PtrOff =
        DAG.getNode(ISD::ADD, DL, getPointerTy(DAG.getDataLayout()), StackPtr,
                    DAG.getIntPtrConstant(Offset, DL));
    return DAG.getStore(Chain, DL, Arg, PtrOff, MachinePointerInfo());
}

上記の処理を引数の数だけ繰り返する。スタック経由でのアクセスが発生している場合は、すべてのストア命令を1つの単一ノードに変化してDAGに接続する。

  // Transform all store nodes into one single node because all store
  // nodes are independent of each other.
  if (!MemOpChains.empty())
    Chain = DAG.getNode(ISD::TokenFactor, DL, MVT::Other, MemOpChains);

高性能プロセッサの分岐予測のサーベイ論文を読んで分岐予測について学ぶ (3. Biased分岐予測、TAGE分岐予測)

プロセッサアーキテクチャについて再度復習その2。分岐予測の続き。

読んでいるのはA Survey of Techniques for Dynamic Branch Predictionという論文で、新規技術の解説ではないのだが、現在の有名どころの分岐予測技術についてまんべんなく解説してくれている論文だ。

  • A Survey of Techniques for Dynamic Branch Prediction
    • Sparsh Mittal

https://arxiv.org/pdf/1804.00261.pdf


プロセッサアーキテクチャについて再度復習その2。分岐予測の続き。

読んでいるのはA Survey of Techniques for Dynamic Branch Predictionという論文で、新規技術の解説ではないのだが、現在の有名どころの分岐予測技術についてまんべんなく解説してくれている論文だ。

  • A Survey of Techniques for Dynamic Branch Prediction
    • Sparsh Mittal

https://arxiv.org/pdf/1804.00261.pdf


3.2 グローバルヒストリと分岐アドレスの両方を利用する方式

Mcfarling [81]らの報告:

  • 同一のBPサイズに対して、グローバル履歴BPはローカル履歴BPよりも精度が低い
  • BPサイズが1KBを超える場合にのみ、バイモーダルBPよりも高い精度が得られる。
    • 小さいBPサイズの場合、バイモーダルBPで採用されている分岐アドレスビットが異なる分岐を効果的に区別できるため
    • カウンタの数が増えるにつれて、頻繁に発生するすべての分岐が異なるカウンタにマップされるため、非常に大きなテーブルでは、分岐アドレスビットの情報値はゼロに近づく。

テーブルサイズが大きくなると、グローバルBPは異なる分岐命令を区別できるようになるが、分岐命令を区別したいならば、分岐命令の分岐アドレスを使用したほうが効果的。

Mcfarling[81]の提案した、グローバルなBHRのビットと分岐アドレスの下位ビットを活用する方式を提案した。

  • gselect : 分岐アドレスの下位ビットとグローバルBHRのビットを連結してPHTの参照アドレスを算出する。
  • gshare : 分岐アドレスの下位ビットとグローバルBHRのビットのXORを取ってPHTの参照アドレスを算出する。

表3は、それぞれ2つの共通グローバル履歴のみを持つ2つの分岐の例を示している。gselectは4つの場合を識別することはできていないが、gshareは4つの場合を識別することができる。ただし、テーブルのサイズが小さい場合はグローバル履歴を追加してしまうとエイリアシングが派生しやすくなってしまうため、gshareの精度は低くなる。

f:id:msyksphinz:20190701222718p:plain
gselect分岐予測、gshare分岐予測

さらに、

  • gshareとgselectの両方を実行して最良のBPを選択するというメタ分岐予測
  • 2コンポーネントを仕様するハイブリッドBP(例えば、Bimodal[80] + gshare)を提案する。
Branch address Global history gselect index (4b+4b) gshare index (8b XOR 8b)
00000000 00000001 00000001 00000001
00000000 00000000 00000000 00000000
11111111 00000000 11110000 11111111
11111111 10000000 11110000 01111111

3.3 複数のインデックス関数の使用

Michaudら[86]の提案は、BPテーブルのエイリアシングはキャッシュミスと似ているので、エイリアシングはキャッシュ内のものと似ている競合、容量、および強制として分類することができることに注意してください。エイリアシングを除去するためのタグの使用は大きなオーバーヘッドを導入するので、「スキュー」BP設計を提案します。これは、スキューアソシアティブキャッシュ設計[87]と同様のメカニズム。衝突の正確な発生はマッピング機能に依存するので、それらのBPは複数の奇数(例えば、3)のBPバンクを使用するが、同じ情報(例えば、グローバル履歴および分岐アドレス)から得られる異なるハッシュ関数を使用する。各バンクはタグなしの予測器として機能し、並行してアクセスされます。すべてのバンクが予測を提供し、1つのバンクに干渉するバンクが他のバンクではそうしそうにないという考えに基づいて、多数決投票を使用して最終予測が選択されます。複数のバンク/テーブルに分割されたBPの場合、使用するインデックス機能[86、88]またはそれらによって使用される履歴の長さ[20、27]が異なる場合があることに注意してください。

BPを更新するために、彼らはすべてのバンクが更新される「全更新」方針と最終予測が正しいが最終予測が不正確であるときに間違った予測を提供するバンクが更新されない「部分更新」方針を考える。バンクが更新されます。 3バンクのBPは、1バンクの大規模BPと同じ精度を達成しながら、ほぼ半分のストレージリソースを必要とします。これは、競合するエイリアシングを減らしながらキャパシティエイリアシングを増やす複数のテーブルを使用することで、偏ったBPが冗長性を増すためです。さらに、不正確な予測を提供するバンクを更新しないことがそれがBPの全体的な有効性を増大させる他の何らかの流れに対する正しい予測に寄与することを可能にするので、部分更新方針は全更新方針よりも良好に機能する。

3.4 偏った分岐の強化

Changら[64]の提案。分岐を異なるストリームに分類する。

  • 利用率に基づいて、使用する分岐予測器を分類する。
    • 2ビットカウンタ
    • PA, GA, GAg(gshare) の3つの2レベル分岐予測器
  • 傾向 : 履歴レジスタBHRが小さいほどエイリアシングが減少するPHTの数が多いことから、偏った分岐は短いBHRを持つ分岐予測器によってより適切に予測させることができる。
  • 傾向 : 混合分岐、偏りのない分岐は、多くの実行状態を区別できる長いBHRを有する分岐予測器によって正確に予測することができる。

Leeら[7]は、図7に示されている「バイモーダル」BPを提示しています。それらのBPは、レベル2カウンターテーブルを2つの部分に分けています。"Taken"および"Not Taken"の「方向予測器」。どの履歴パターンでも、各部分から1つのカウンターが選択されます。これらの中から、別の「選択予測器」の入力に基づいて、予測を行うために1つのカウンターが最終的に選択されます。選択予測器は、分岐アドレスによってのみ索引付けされます。 2つのグループのGHパターンの分類は、選択予測器のアドレスごとの偏りに基づいて行われます。それらは部分更新ポリシーを使用します。そこでは、選択されたカウンターのみが分岐結果に基づいて更新されます。選択が分岐結果と反対であるが選択されたカウンタの最終予測が正しい場合を除いて、それらは常に選択予測器を更新する。全体として、バイモーダルBPは、負のエイリアシングを示す分岐を分離し、ニュートラル/有益なエイリアシングを示す分岐をまとめます。有害なエイリアシングを除去することによって、それらの技術は高い予測精度を達成する。

Leeら[7]の提案 : Bimodal分岐予測器。レベル2のカウンタテーブルを2つに分ける。

  • "Taken"および"Not Taken"の分岐予測器に分ける。どのような履歴パタンでも、各部分から1つのカウンタが選択される。これらは、最終的に選択予測器(choice predictor)によってどちらを使用するかを選択する。選択予測器は分岐アドレスのみで選択される。
  • 2つのグループのGHパタンの分類は、選択されたカウンタのみが分岐結果に基づいて更新される。
    • 最終的な選択が分岐結果と反対であり、選択されたカウンタの最終予測が正しい場合を除き、分岐予測器が予測を更新する。
  • Bimodal分岐予測は、負のエイリアシングを除去し、これにより高い予測精度を達成することができる。
f:id:msyksphinz:20190701222836p:plain
バイモーダル分岐予測器 (本論文より抜粋)

Sprangleら[19]は、PHTエントリの解釈を変更することによって有害な干渉を有益なケースまたは無害なケースに変換する手法を提案します。図8は、合意予測値と呼ばれるBPを示しています。各分岐に対して、分岐の最も可能性の高い結果を予測するバイアスビットを追加します。 (従来の方式におけるように)分岐の方向を予測する代わりに、それらの技術におけるPHTカウンタは、分岐結果がバイアスビットと一致するかどうかを予測する。分岐が解決されると、分岐方向がバイアスビットと一致した場合にPHTカウンタが増加し、逆の場合も同様です。彼らは最初の実行で分岐の方向にバイアスビットを設定します。バイアスビットの適切な選択により、同じPHTエントリにマッピングする2つの分岐のカウンタは、同じ方向に更新される可能性がより高い。状態に同意します。

Sprangleら[19]の提案 : PHTエントリの解釈を変更することによって、有害なエイリアシングを有益な場合と無害な場合に変換する手法。

  • 各分岐に対して、分岐の最も可能性の高い結果を予測するバイアスビットを追加する。PHTカウンタは、分岐結果がアイアスビットと一致するかどうかを予測する。
    • 分岐が解決されたとき、分岐方向がバイアスビットと一致した場合にPHTカウンタが増加し、不一致の場合はPHTカウンタが減少する。
  • バイアスビットを適切に選択することにより、同じPHTエントリにマッピングしている2つの分岐予測のカウンタは同じ方向に更新される可能性がより高い。
  • 数学的な説明 : 分岐成立=T, 分岐不成立=NT, 一致=A, 不一致=D の確率を考える。分岐B1と分岐B2を考える。
    • 有害なエイリアシングの確率 : [tex:T{B1} \times NT{B2} + NT{B1} \times T{B2}] (一方が、分岐成立、一方が分岐不成立)
    • 本提案における有害なエイリアシング : [tex:A{B1} \times D{B2} + A{B2} \times D{B1}]
    • [tex:T{B1} = 85\%] および [tex:T{B2}=15\%] の場合 [tex:A{B1}=85\%] および [tex:A{B2}=85\%] となる。
    • したがって、有害なエイリアシング25\% となる。
f:id:msyksphinz:20190701222915p:plain
Agree分岐予測器 (本論文より抜粋)

3.5 Geometric history length BPs

Seznec[27]の提案 : GEHL (Geometric history length : 幾何学的履歴長) 分岐予測器

  • 分岐予測器を複数( M(=4\text{から}12) )個用意する。分岐テーブルはそれぞれ T_i(0\le i &lt; M) と名付ける。
  • 分岐予測器の参照インデックス : 分岐アドレスとハッシュされたグローバルパス・分岐履歴から計算する。
  • 表4に基づいて、各テーブルの長さを決定する。例えば、 M=8 の分岐予測器を仮定し、 \alpha=2 かつ L(1)=2 の場合は、テーブルの長さ L(i) = 0, 2, 4, 8, 16, 32, 64, 128 となる。

したがって、5つのテーブルは最大で16個の履歴ビットを使用してインデックスされ、それでもなお128ビットの履歴との相関関係を捉えている。

予測 各テーブル T_i から1つのカウンタ C(i) が読みだされる。このカウンタのアドレスは以下のようにして計算される: \text{Sum}=M/2+\sum_{i=0}^{M-1}C(i) もし \text{Sum}\ge 0 ならば、分岐予測はTakenとなり、そうでない場合はNot Takenとなる。
テーブルの参照 テーブルT0は分岐アドレスを用いて参照される。テーブル 1\le i &lt;ML(i) = \lfloor \alpha^{i-1}\times L(1)+0.5)\rfloor の履歴長を用いてインデックスされる。

予測器は、予測が誤った場合、もしくは \text{Sum} の絶対値が閾値を下回った場合に更新される。分岐予測器は、履歴長を調整して、更新時のエイリアシングが大きい場合は短い履歴長を使用し、エイリアシングが小さい場合には長い履歴長を使用する。この分岐予測器の問題点は、予測器自体の複雑さと、予測ミスにより復元しなければならない大量の状態をチェックポイントする必要性である。全体的に、GEHL分岐予測器は高い精度を提供することができる。

Seznecら[20]の提案 : 現在のTAGE(tagged geometric history length : タグ付き幾何履歴長)分岐予測器

TAGE分岐予測器の構成要素 :

  • T_0 : 基本予測器。2ビットの倍モーダルカウンタ。
  • T_i(1\le i \le M) : 部分タグ付き予測器。インデックスに使用するのは L(i) = \lfloor \alpha^{i-1}\times L(1)+0.5\rfloor ビット分のグローバル履歴テーブル。以下の要素を持っている。
  • 符号付き2ビットカウンタ
  • タグ
  • U(useful) カウンタ

図9は、5つの成分を含むTAGE BPを示している。

f:id:msyksphinz:20190701233703p:plain
TAGE分岐予測器

ベースBPとタグ付きBPが同時にアクセスされ、複数のTAG付BPがヒットした場合は最も長い履歴長のものが選択される。

アップデート時には、新たな予測情報が最終的な予測結果と異なる場合、"U"カウンタがアップデートされる。

  • 最終的な予測結果と一致した場合 : インクリメント
  • 最終的な予測結果と不一致であった場合 : デクリメント

定期的に"U"カウンタはリセットされ、Uselfulフラグは定期的に使用できなくなる。 T_c の"Pred"カウンタは、予想の一致、不一致でアップデートされる。さらに、予測が誤っており、 c<M の場合は、 Tk(c < k < M) エントリに割り当てられる。

T_c コンポーネントの予測が、「新たに割り当てられた」エントリから送られてきたならば、この予測はまだ誤った予測を行う可能性があるため、別の予測器の値が使用される可能性が高い。TAGE分岐予測器はこれまでの分岐予測器よりも性能が高く、従ってこの「幾何履歴長、部分タグ」の方式が、加算器のツリーと比較して最もコスト効率が良い分岐予測器となる。また、8タグ以上の分岐予測器を使用しても性能上意味がない。Tcコンポーネントと予測カウンタを観測することによって、分岐予測ミスが発生する確率を計算することができる。

比較 : ハードウェアの比較として、gshareとパーセプトロンBPはそれぞれ20から60個の分岐記録について相関がある。一方で、GEHLとTAGEは200から2000個の分岐記録について相関がある。単一のBPについては、TAGE分岐予測器は最も精度の高い分岐予測器であると考えられている。また、TAGE分岐予測器は複数のタグ付され予測器を使用するのに対して、他の分岐予測器は1つのタグ分岐予測器を使用する。


過去の記事

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

LLVMのバックエンドを作るための第一歩 (32. 関数を呼び出す側のDAGの作成:ノードの定義)

f:id:msyksphinz:20190425001356p:plain

引数のある関数を変換するにあたり、関数本体側の引数を取り込む処理は完成したのだが、次は関数を呼び出す側だ。関数コールにあたり、引数をABIのルールに則ってレジスタもしくはスタックに配置する必要がある。

関数呼び出し側の処理を行うためには、MYRISCVXTargetLoweringクラスのLowerCallをオーバーライドする必要がある。xxx

ここまでで、関数コールに必要なLowerメソッドについてまとめる。

  • 関数呼び出し側 : 引数の設定、戻り値の処理 : MYRISCVXTargetLowering::LowerCall
  • 関数本体
    • 引数の取り出し : MYRISCVXTargetLowering::LowerFormalArguments
    • 戻り値の設定 : MYRISCVXTargetLowering::LowerReturn

関数コールを行うためのノードを作成する

MYRISCVXで関数コールを行うのには、RISC-Vと同様にjalr命令もしくはjal命令を使用する。これらの命令の仕様は以下のようになっている。

  • jalr rd, rs1, imm : GPR[rs1] + immのPCアドレスにジャンプする。現在のPC値+4をGPR[rd]に保存する。
  • jal rd, imm : PC + immのPCアドレスにジャンプする。現在のPC値+4をGPR[rd]に保存する。

また、jalrjalの特殊な形式として以下の2つの命令も定義されている(というかエイリアスだね)。

  • jr rs1 : jalr x0, rs1, 0 : rs1のアドレスにジャンプする。rd = 0なので、リンクレジスタには何も保存しない。
  • j imm : jal x0, imm : PC + immのアドレスにジャンプする。rd = 0なので、リンクレジスタには何も保存しない。

関数コールを行うためにはこれらの命令が活用できる。jal命令で関数ターゲットまでジャンプするか、jalr命令でrs1に関数ターゲットのアドレスを格納し、imm=0でジャンプするという方法で実現できそうだ。

まず、関数コールを行うための専用ノードをLLVM IRで定義する。MYRISCVXInstrInfo.tdに、関数コールのためのノードMYRISCVXCallノードを定義する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
def SDT_MYRISCVXCall       : SDTypeProfile<0, 1, [SDTCisVT<0, iPTR>]>;
...
def MYRISCVXCall : SDNode<"MYRISCVXISD::CALL", SDT_MYRISCVXCall,
                          [SDNPHasChain, SDNPOptInGlue, SDNPOutGlue,
                           SDNPVariadic]>;

前回も説明した通り、MYRISCVXCallが関数コールのためのノードで、SDT_MYRISCVXCallがノードの制約条件を記述している。ノードには1つの入力が与えられ、出力はない。入力ノードの型はポインタとなる。

関数コールを行う命令の定義と推論パタンの登録

では、MYRISCVXCallを使用して命令を定義する。ここでは上記のjalr命令とjal命令を定義するのだが、jaljr命令も定義しておく。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXInstrInfo.td
let isCall=1, hasDelaySlot=0, Defs = [RA], isCodeGenOnly = 0 in {
  class JumpRegister<bits<7> opcode, bits<3> funct3,
        string instr_asm>:
    MYRISCVX_I<opcode, funct3, (outs), (ins GPR:$rs1),
      !strconcat(instr_asm, "\tx1, $rs1, 0"),
      [(MYRISCVXCall GPR:$rs1)], IIAlu> {
      let rd = 1;
      let imm12 = 0;
      let isBranch = 1;
      let isTerminator = 1;
      let isBarrier = 1;
      let hasDelaySlot = 0;
  }

  // Jump and Link (Call)
  class JumpLink<bits<7> opcode, string opstr, DAGOperand opnd> :
    MYRISCVX_J<opcode, (outs), (ins opnd:$target), !strconcat(opstr, "\t$target"),
                   [(MYRISCVXCall tglobaladdr:$target)], IIAlu> {
    let rd = 1;
    let DecoderMethod = "DecodeJumpTarget";
  }
}

ここでは、3つのクラスを定義した。

  • JumpRegister クラス。レジスタrs1に格納されているアドレスにジャンプする。ここでは関数コールのために使用するので、制約条件としてrd = 1(=x1)imm12 = 0としている。命令フォーマットとしてはMYRISCVX_Iを使用しており、jal, jr命令を想定している。
    • このクラスのPredicationとしてMYRISCVXCallを登録している。つまりこのJumpRegisterクラスを使用して定義した命令は、汎用レジスタオペランドに持つMYRISCVXCallノードを使用することになる。
  • JumpLinkクラス。ノードtargetのアドレスにジャンプする。今回は関数コールのために使用するので、制約条件としてrd = 1(=x1)としている。命令フォーマットとしてはMYRISCVX_Jを使用しており、jalr命令を想定している。
    • このクラスのPredicationとしてMYRISCVXCallを登録している。つまりこのJumpLinkクラスを使用して定義した命令は、ターゲットノードをオペランドに持つMYRISCVXCallノードを使用することになる。

これらのクラスを使用して命令を定義する。jal, jalr, jr命令を定義する。命令のマシンコードはRISC-Vのものに則っている。

def JAL : JumpLink<0b1101111, "jal", calltarget>;
def JALR : JumpRegister <0b1100111, 0b000, "jalr">;
def JR   : JumpRegisterX0<0b1100111, 0b000, "jr">;

念のため、もう一つj命令を定義しておく。j命令はjal命令のrd=0版なので新しくJumpOffestX0クラスを定義する。MYRISCVX_Jをベースとし、rd=0という制約を加えます。

class JumpOffsetX0<bits<7> opcode, string instr_asm>:
  MYRISCVX_J<opcode, (outs), (ins brtarget20:$addr),
  !strconcat(instr_asm, "\t$addr"), [], IIAlu> {
    let rd = 0;
    let isBranch = 1;
    let isTerminator = 1;
    let isBarrier = 1;
    let hasDelaySlot = 0;
}

def J   : JumpOffsetX0<0b1101111, "j">;

最後に、MYRISCVXCallの命令生成パタンを登録する。jalを使用した場合のパタン2つと、jalrを使用した場合のパタン2つを登録しておく。

def : Pat<(MYRISCVXCall (i32 tglobaladdr:$dst)),  (JAL tglobaladdr:$dst)>;
def : Pat<(MYRISCVXCall (i32 texternalsym:$dst)), (JAL texternalsym:$dst)>;
def : Pat<(MYRISCVXCall (i32 tglobaladdr:$dst)),  (JALR tglobaladdr:$dst)>;
def : Pat<(MYRISCVXCall (i32 texternalsym:$dst)), (JALR texternalsym:$dst)>;

MYRISCVXCallオペランドとして2種類、tglobaladdrtexternalsymを使用している。tglobaladdrはグローバルアドレスを示すノード、外部シンボルを示すためのノードだ。

  • llvm-myriscvx/include/llvm/Target/TargetSelectionDAG.td
def tglobaladdr : SDNode<"ISD::TargetGlobalAddress",  SDTPtrLeaf, [],
                         "GlobalAddressSDNode">;
...
def texternalsym: SDNode<"ISD::TargetExternalSymbol", SDTPtrLeaf, [],
                         "ExternalSymbolSDNode">;

ここまでで関数ジャンプを生成するための準備は整った。次に、LowerCallメソッドを実装して、関数コールに関するLLVM IRをDAGに変換していく。

LLVMのバックエンドを作るための第一歩 (31. i32よりも小さな値を引数に渡すとどのようなDAGになるのか)

i32型の変数を引数として渡した関数の場合は、レジスタもスタックもサイズはi32で考えているのでこの場合は問題ないのだが、ではi32よりも小さな型の引数を渡す場合にはどのような処理になっているのだろうか。 この場合は呼び出し規約のルールで型の拡張を行っている。この時には、AssertSextもしくはAssertZextノードを挿入する必要がある。このノードは2つのオペランドを持っており、1つは拡張された値そのもの、そしてもう一つは拡張前の値のビットサイズだ。

AssertSext レジスタに保持できる値よりも小さな値を保持している場合に挿入される。オリジナルの値よりも符号拡張された値が格納されていることを示す。 オペランド1: 値そののもの
オペランド2. 符号拡張される前の型
AssertZext レジスタに保持できる値よりも小さな値を保持している場合に挿入される。オリジナルの値よりも符号なし拡張された値が格納されていることを示す。 オペランド1: 値そののもの
オペランド2. 無し拡張される前の型
TRUNCATE 上位ビットを除去する。
  • ch9_1_extend.cpp
int sum_i(short x1, short x2)
{
  int sum = x1 + x2;
  return sum;
}

引数の型がshort(=i16)なのでi32に拡張が行われれ、さらにTRUNCATEが挿入されるはずだ。具体的にDAGを生成してみよう。

./bin/clang -O3 -target mips-unknown-linux-gnu -c ../lbdex/input/ch9_1_extend.cpp -emit-llvm
./bin/llc -view-dag-combine1-dags -march=myriscvx32 -mcpu=simple32 -relocation-model=pic -filetype=asm -target-abi=lp32 ch9_1_extend.bc -o -

DAGをプロットしてみると、以下のようになった。

f:id:msyksphinz:20190627013348p:plain
i32よりも小さな型をレジスタ渡しした場合のDAG

2つの引数について、まずAssertSextが挿入されている。AssertSextの1つ目の分岐には値そのものが接続されており、もう一つの分岐には元の値を示すノードValueType:i16が接続されている。次にtruncateで、上位ビットを削って実際の引数の型を元に戻すノードが挿入されている。

高性能プロセッサの分岐予測のサーベイ論文を読んで分岐予測について学ぶ (2. 2レベル分岐予測器の構成について)

プロセッサアーキテクチャについて再度復習その2。分岐予測の続き。

読んでいるのはA Survey of Techniques for Dynamic Branch Predictionという論文で、新規技術の解説ではないのだが、現在の有名どころの分岐予測技術についてまんべんなく解説してくれている論文だ。

  • A Survey of Techniques for Dynamic Branch Prediction
    • Sparsh Mittal

https://arxiv.org/pdf/1804.00261.pdf


3. 分岐予測器の設計

  • 2ビットカウンタ分岐。
    • 基本的なもの : 2-bit×1次元の分岐予測テーブル。テーブルのインデックスは分岐アドレスビットを使用する → 分岐成立・不成立を予測する。
    • Section 3.1 : 2レベル分岐予測 : 分岐履歴と現在の分岐アドレスを両方使用して分岐成立・不成立を予測する。
    • Section 3.2 : テーブルインデックスの取得方法 : 様々な要素と方法でインデックスの計算を行う。
  • Section 3.3 : 複数のインデックス計算アルゴリズムを使い、複数の分岐予測テーブルにアクセスする。
  • Section 3.4 : Biased Branchを使用する。
  • Section 3.5 : 幾何履歴長分岐予測器を使用して、非常に大きな履歴を追跡する分岐予測器。

3.1 2レベル分岐予測器

使用するもの : 分岐履歴レジスタ(Branch History Register : BHR) , 分岐パタン履歴テーブル(Pattern History Table : PHT)

分岐パタン履歴テーブル PHT : 2ビットカウンタが並んだテーブル。どのインデックスを参照するかは、分岐履歴レジスタと現在のプログラムカウンタを使用する。分岐レジスタは同一のPHTを参照するため、グローバルPHTと呼ばれる。PHTがPビットであったとすると、最近のP回の分岐結果が格納されている。過去のS回の分岐履歴を使って予測が行われる。例えばP=7で最近の分岐結果が1010011であり、S=5であったとすると

分岐履歴レジスタ BHR : Pビット定義されているとすると、PHTは 2^{P} だけエントリが用意される。

PHTでは、これらのP個の分岐の与えられたパターンの最近の発生に対する分岐動作が格納され、そのうちのS個の分岐結果が予測に使用される。分岐は、パターンの最近のインスタンスに対する分岐応答を見ることによって予測されます。たとえばP = 7で、最後のP回の分岐結果は1010011である場合(1=taken、0=not taken)を考える。S=5で、それぞれの最後の5回のそれぞれにおいて過去7つの分岐がパターン1010011を示し、分岐結果が取られたものと取られなかったものとの間で切り替えられた場合、レベル2履歴は10101となり、BPの次の分岐予測は"not-taken"となる。

Yehらは、分岐予測の実装として以下の3つの方式を提案した。

  • GAgスキーム : グローバルBHRを1つ、グローバルPHTを1つ用意する。もっとも単純な方式だが、BHRとPHTで最も高い確率でエイリアシングが発生する。
  • PAgスキーム : 分岐命令のアドレスに応じて複数のBHRから1つを選択し、BHRのビット列に応じてPHTを選択する。グローバルBHRの衝突によるエイリアシングの確立を削減することができるが、PHTのエイリアシングは削減することはできない。
  • PApスキーム : 分岐命令のアドレスに応じて複数のBHRから1つを選択し、さらに分岐アドレスに応じて複数のPHTテーブルセットからPHTテーブルを選択する。PHTのエイリアシングも削減することができる。

PAgおよびPApでは、BHRテーブルはセットアソシアティブまたはダイレクトマッピングとして実装できる。

結果

  • エイリアシングの影響 : 性能低下が発生する可能性 : PAp < PAG < GAgとなる。
  • ハードウェアコスト : PAg < GAg < PAp (GAが長い履歴レジスタが必要となるため)。

Yehら[16]はこれらをさらに一般化し、

  • GHRの配置の方法 : G/P/S
  • 割り当ての方法 : g/p/s

として、合計で9つの分類を行ってみる。

Level-1 GA PA SA
k分岐の格納 Global Address History
最後のk回の分岐結果が見える
Partial Address History
同じ分岐に対する最後のk回の分岐結果が見える
Per-Set History
同じセットの最後のk回の分岐の結果が見える
Leve-2 g p s
PHTの参照元 すべての分岐命令から参照される 各分岐命令から参照させる 分岐のセットから参照される
  1. Global History BPでは、必要なBHRは1つだけである。
    1. GAg : グローバルBHRを1つだけ持つ。Global PHTを持ち、BHRの値から参照するPHTを決定する。
    2. GAs : グローバルBHRを1つだけ持つ。Global PHTを複数持ち、分岐命令の分岐アドレスに応じてどのPHTにアクセスするかはセットアソシアティブにより決める。例えば、1KBのブロックを1セットとすると、ブロック内の分岐命令(最大256命令)は同じセットのメンバーとして構成される。
    3. GAp : グローバルBHRを1つだけ持つ。Global PHTを複数持ち、分岐命令の分岐アドレスをダイレクトマップしてどのPHTセットにアクセスするかを決める。
f:id:msyksphinz:20190627232559p:plain
GAp, GAs, GApの構成
f:id:msyksphinz:20190627232820p:plain
本論文から抜粋。GAg, GAs, GAp, PAg, PAs, PAp, SAg, SAs, SAp の分類。

結果 : グローバル履歴分岐予測は効果的であることは間違いないが、エイリアシングを削減するために長いBHRが必要になるのでハードウェアコストとしては他の方式よりも効率が悪い。 PA(Per Address)分岐予測は、ループ毎に確実にBHRに分岐履歴を記録できるため精度が高い。また、エイリアシングが削減されるためにテーブルサイズを削減することができる。 SA(Per-set History)分岐予測はより高い精度を実現することができるが、Set毎にPHTを用意する必要があるため、ハードウェアコストがより大きくなる。

f:id:msyksphinz:20190627232650p:plain
PAg, PAs, PApの構成

Skadronら[6]の提案 : BHRとPHTの分岐予測を使用しただけでは、グローバルな分岐(ループに関連しない分岐)とローカルな分岐(ループに関連する分岐)を区別することができず、そこで余計なエイリアシングや分岐予測の失敗が発生する可能性がある。これを防ぐために、PHTを参照するためのインデックの計算として、BHRのの結果に加えて、GHR(Global History Register)の値を結合したうえで算出する方法を提案した。GHRはすべての分岐予測の結果を格納しており、この値をPHT参照のためのアドレスインデックスに付け加えることで、グローバルな分岐とローカルな分岐を区別して分岐予測の失敗を防ぐことができる。これを、Yehらの用語を拡張して、「インデックスのMerge」の意味でMA分岐予測と呼んでいる。 さらに、BHRとPHTを用いる方式の問題点として、まずはBHRにアクセスした後PHTにアクセスするという2段階のシリアルアクセスが発生するというものである。このため全体のオーバヘッドが大きくなってしまうので、BHRとPHTに並行してアクセスする方法を考える。図5(b)のように、まずはGHRと分岐アドレスを使用してPHTのインデックスを計算し、可能性のあるすべてのPHTにアクセスする。同時にBHRにアクセスして、実際に使用するPHTの場所を計算する。最後に、複数のPHTの中からBHRに基づいて必要な分岐予測を取り出す、という方式である。この方式だとBHRとPHTに同時にアクセスできるが、PHTを分割したことによるオーバヘッドと、最後に結果をマルチプレクスする回路面積のオーバヘッドが問題となる。しかし、小規模なデザインでは、この方式はより高い精度を提供できる。

f:id:msyksphinz:20190627232912p:plain
本論文から抜粋。2レベル分岐予測の構成方法。

Sechrestらによると、規模の小さな分岐予測器の場合、エイリアスの影響により分岐予測の精度が大幅に低下してしまう。このため、gshare[91]やGA[16]の相関関係を利用する分岐予測の利点を無効化してしまうことがある。したがって、グローバル履歴ベースの分岐予測の場合は十分に大きなサイズを確保する必要がある。PAなどのローカル履歴ベースの分岐予測器の場合は、BHRの方がPHTよりも干渉が大きくなる傾向があり、全体としては、BHR、PHTのどちらも十分な容量を確保する必要が生じる。

Panら[79]の研究によれば、分岐予測はターゲットとなる分岐命令そのものだけでなく、その前に実行された分岐命令の結果によっても予測が可能であるということである。例えば、図6(a)では分岐B3は分岐B1とB2に相関しており、B1とB2の条件が真であれば分岐B3は分岐しないと正確に予測することができるが、図6(c)のように自分の履歴のみを用いたBPではそれを実現することはできない。B3の履歴を、図6(b)に示すように4つの可能性について分類することでより正確に予測することができるようになる。図6(d)は K=2 (過去2回の分岐履歴を使用する)、 N=2 (2ビット飽和カウンタの分岐予測器を使用する)の場合を示しており、 2^{K} 個の N ビット分岐予測器の結果から、1つの分岐予測を取り出すという方式を示している。この手法により、わずかなハードウェアオーバヘッドで高精度な分岐予測を実現することができる。

f:id:msyksphinz:20190627233030p:plain
本論文から抜粋。過去の相関を利用した分岐予測。

Chenら [82]はデータ圧縮をベースとする分岐予測の概念モデルを提案しており、データ圧縮の研究に基づく分岐予測の新たな利点について示している。2レベルもしくは相関を用いるBPは、データ圧縮における最適な予測器である"Partial Matchに基づく分岐予測"(PPM)の単純なものであるとしている。この分岐予測では、PPMが小規模のBTBを用いた2レベル予測器よりもわずかに高い精度を持っていると報告している。