FPGA開発日記

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

LLVMのバックエンドを作るための第一歩 (38. 可変長引数のサポート)

f:id:msyksphinz:20190425001356p:plain

C言語の可変長引数では、例えば以下のような記述が可能となる。

  • vararg.cpp
#include <stdarg.h>

int sum_i(int amount, ...)
{
  int i = 0;
  int val = 0;
  int sum = 0;
    
  va_list vl;
  va_start(vl, amount);
  for (i = 0; i < amount; i++)
  {
    val = va_arg(vl, int);
    sum += val;
  }
  va_end(vl);
  
  return sum; 
}

int test_vararg()
{
  int a = sum_i(10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
  return a;
}

C言語の文法としては少し特殊な形をしているかもしれないが、フロントエンドによって一度IRの形式になってしまえば、あとはレジスタを割り当てて命令を生成するだけなのでそこまで難しくはなさそうだ。ただし、この可変長引数をサポートするためには、DAGによってさらに別の種類のノードをサポートする必要がある。

まずは、可変長引数のC言語プログラムから生成されたLLVM IRを見てみる。

./bin/clang vararg.cpp -emit-llvm -o vararg.bc
./bin/llvm-dis vararg.bc -o -
define dso_local i32 @_Z11test_varargv() #0 {
entry:
...
   %call = call i32 (i32, ...) @_Z5sum_iiz(i32 10, i32 0, i32 1, i32 2, i32 3, i32 4, i32 5, i32 6, i32 7, i32 8, i32 9)
...
define dso_local i32 @_Z5sum_iiz(i32 %amount, ...) #0 {
entry:
...
  %arraydecay = getelementptr inbounds [1 x %struct.__va_list_tag], [1 x %struct.__va_list_tag]* %vl, i32 0, i32 0
  %arraydecay1 = bitcast %struct.__va_list_tag* %arraydecay to i8*
  // va_start()を呼ぶ
  call void @llvm.va_start(i8* %arraydecay1)
...
vaarg.in_reg:                                     ; preds = %for.body
...
for.end:                                          ; preds = %for.cond
  // va_end()を呼ぶ
  call void @llvm.va_end(i8* %arraydecay34)
    ret i32 %11
}

in_regの場合と、in_memの場合が挙げられている。for.bodyから進んでいき、%gp_offsetが40よりも小さい場合、in_regに飛んでいき、そうでない場合はin_memに飛んでいくようだ。

このコードの意図をはっきりと説明した文章は見つけられないが、考えられるのは、Callee側が受け入れることのできる引数が可変長だと、コンパイル時にCalleeが使用するスタックの量をあらかじめ算出すことができない、こうすると面倒なので、40バイトまでの可変引数までならスタック経由で渡せるようにする、それ以降はグローバル領域を確保して受け渡す、というようにしていると思われる。

SelectionDAGの生成

VAARG, VACOPY, VAENDの3つは最適化の最中に生成されないように抑制する。つまり、MYRISCVXTargetLoweringにて、setOperationInAction()で対象となるノードの生成を抑制する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM,
                                               const MYRISCVXSubtarget &STI)
...
  // For Var Arguments
  setOperationAction(ISD::VASTART,   MVT::Other, Custom);

  // Support va_arg(): variable numbers (not fixed numbers) of arguments
  //  (parameters) for function all
  setOperationAction(ISD::VAARG,        MVT::Other, Expand);
  setOperationAction(ISD::VACOPY,       MVT::Other, Expand);
  setOperationAction(ISD::VAEND,        MVT::Other, Expand);
...
  • VASTART : 可変長引数の処理を開始する。
  • VAARG : 可変長引数から実際の値を取り出す。
  • VACOPY
  • VAEND : 可変長引数の処理を終了する。

VASTARTだけはカスタム実装に設定したので、lowerVASTART()のみ実装する。

SDValue MYRISCVXTargetLowering::
LowerOperation(SDValue Op, SelectionDAG &DAG) const
{
  switch (Op.getOpcode())
  {
    case ISD::SELECT        : return lowerSELECT(Op, DAG);
    case ISD::GlobalAddress : return lowerGlobalAddress(Op, DAG);
    case ISD::VASTART       : return lowerVASTART(Op, DAG);
  }
  return SDValue();
}

lowerVASTARTでは、

SDValue MYRISCVXTargetLowering::lowerVASTART(SDValue Op, SelectionDAG &DAG) const {
  MachineFunction &MF = DAG.getMachineFunction();
  MYRISCVXFunctionInfo *FuncInfo = MF.getInfo<MYRISCVXFunctionInfo>();

  SDLoc DL = SDLoc(Op);
  SDValue FI = DAG.getFrameIndex(FuncInfo->getVarArgsFrameIndex(),
                                 getPointerTy(MF.getDataLayout()));

  // vastart just stores the address of the VarArgsFrameIndex slot into the
  // memory location argument.
  const Value *SV = cast<SrcValueSDNode>(Op.getOperand(2))->getValue();
  return DAG.getStore(Op.getOperand(0), DL, FI, Op.getOperand(1),
                      MachinePointerInfo(SV));
}

LowerFormalArguments()つまり可変長引数を持つ関数が呼び出されたときの引数の処理について、可変長引数であれば専用のルーチンを呼び出す。writeVarArgRegsを呼び出す。

SDValue
MYRISCVXTargetLowering::LowerFormalArguments (SDValue Chain,
                                              CallingConv::ID CallConv,
                                              bool IsVarArg,
                                              const SmallVectorImpl<ISD::InputArg> &Ins,
                                              const SDLoc &DL, SelectionDAG &DAG,
                                              SmallVectorImpl<SDValue> &InVals) const {
...
  // 可変長引数を処理する場合は、特殊なwriteVarArgRegsに飛ぶ。
  if (IsVarArg)
    writeVarArgRegs(OutChains, Chain, DL, DAG, CCInfo);

  return Chain;
}

このwriteVarArgRegsは、ABIがLP32の時にレジスタ渡しをしたデータをスタックに戻す。せっかくレジスタ渡しをしたのにどうしてスタックに戻すんだ?と思われるかもしれないが、それ以降の処理は一律してfor文の処理で統一するため、レジスタ渡し、メモリロードの2種類でアセンブリを生成したくない、という意図があると思われる。

  /* for文の前に、レジスタ渡しした可変長引数の要素をスタックに置いておく。 */
  for (i = 0; i < amount; i++)
  {
    /* valの取得はスタックから一律に受け取る */
    val = va_arg(vl, int);
    sum += val;
  }
void MYRISCVXTargetLowering::writeVarArgRegs(std::vector<SDValue> &OutChains,
                                             SDValue Chain, const SDLoc &DL,
                                             SelectionDAG &DAG,
                                             CCState &State) const {
  ArrayRef<MCPhysReg> ArgRegs = ABI.GetVarArgRegs();
  unsigned Idx = State.getFirstUnallocated(ArgRegs);
  unsigned RegSizeInBytes = Subtarget.getGPRSizeInBytes();
  MVT RegTy = MVT::getIntegerVT(RegSizeInBytes * 8);

  ...
  /* 引数渡しに使用しているレジスタを1つ1つ退避していく */
  for (unsigned I = Idx; I < ArgRegs.size();
       ++I, VaArgOffset += RegSizeInBytes) {

    unsigned Reg = addLiveIn(MF, ArgRegs[I], RC);
    SDValue ArgValue = DAG.getCopyFromReg(Chain, DL, Reg, RegTy);
    FI = MFI.CreateFixedObject(RegSizeInBytes, VaArgOffset, true);
    SDValue PtrOff = DAG.getFrameIndex(FI, getPointerTy(DAG.getDataLayout()));
    /* Store命令を生成 */
    SDValue Store =
        DAG.getStore(Chain, DL, ArgValue, PtrOff, MachinePointerInfo());
    cast<StoreSDNode>(Store.getNode())->getMemOperand()->setValue(
        (Value *)nullptr);
    OutChains.push_back(Store);
  }
}

LLVMのバックエンドを作るための第一歩 (38. 可変長引数のサポート)

f:id:msyksphinz:20190425001356p:plain

C言語の可変長引数では、例えば以下のような記述が可能となる。

  • vararg.cpp
#include <stdarg.h>

int sum_i(int amount, ...)
{
  int i = 0;
  int val = 0;
  int sum = 0;
    
  va_list vl;
  va_start(vl, amount);
  for (i = 0; i < amount; i++)
  {
    val = va_arg(vl, int);
    sum += val;
  }
  va_end(vl);
  
  return sum; 
}

int test_vararg()
{
  int a = sum_i(10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
  return a;
}

C言語の文法としては少し特殊な形をしているかもしれないが、フロントエンドによって一度IRの形式になってしまえば、あとはレジスタを割り当てて命令を生成するだけなのでそこまで難しくはなさそうだ。ただし、この可変長引数をサポートするためには、DAGによってさらに別の種類のノードをサポートする必要がある。

まずは、可変長引数のC言語プログラムから生成されたLLVM IRを見てみる。

./bin/clang vararg.cpp -emit-llvm -o vararg.bc
./bin/llvm-dis vararg.bc -o -
define dso_local i32 @_Z11test_varargv() #0 {
entry:
...
   %call = call i32 (i32, ...) @_Z5sum_iiz(i32 10, i32 0, i32 1, i32 2, i32 3, i32 4, i32 5, i32 6, i32 7, i32 8, i32 9)
...
define dso_local i32 @_Z5sum_iiz(i32 %amount, ...) #0 {
entry:
...
  %arraydecay = getelementptr inbounds [1 x %struct.__va_list_tag], [1 x %struct.__va_list_tag]* %vl, i32 0, i32 0
  %arraydecay1 = bitcast %struct.__va_list_tag* %arraydecay to i8*
  // va_start()を呼ぶ
  call void @llvm.va_start(i8* %arraydecay1)
...
vaarg.in_reg:                                     ; preds = %for.body
...
for.end:                                          ; preds = %for.cond
  // va_end()を呼ぶ
  call void @llvm.va_end(i8* %arraydecay34)
    ret i32 %11
}

in_regの場合と、in_memの場合が挙げられている。for.bodyから進んでいき、%gp_offsetが40よりも小さい場合、in_regに飛んでいき、そうでない場合はin_memに飛んでいくようだ。

このコードの意図をはっきりと説明した文章は見つけられないが、考えられるのは、Callee側が受け入れることのできる引数が可変長だと、コンパイル時にCalleeが使用するスタックの量をあらかじめ算出すことができない、こうすると面倒なので、40バイトまでの可変引数までならスタック経由で渡せるようにする、それ以降はグローバル領域を確保して受け渡す、というようにしていると思われる。

SelectionDAGの生成

VAARG, VACOPY, VAENDの3つは最適化の最中に生成されないように抑制する。つまり、MYRISCVXTargetLoweringにて、setOperationInAction()で対象となるノードの生成を抑制する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
MYRISCVXTargetLowering::MYRISCVXTargetLowering(const MYRISCVXTargetMachine &TM,
                                               const MYRISCVXSubtarget &STI)
...
  // For Var Arguments
  setOperationAction(ISD::VASTART,   MVT::Other, Custom);

  // Support va_arg(): variable numbers (not fixed numbers) of arguments
  //  (parameters) for function all
  setOperationAction(ISD::VAARG,        MVT::Other, Expand);
  setOperationAction(ISD::VACOPY,       MVT::Other, Expand);
  setOperationAction(ISD::VAEND,        MVT::Other, Expand);
...
  • VASTART : 可変長引数の処理を開始する。
  • VAARG : 可変長引数から実際の値を取り出す。
  • VACOPY
  • VAEND : 可変長引数の処理を終了する。

VASTARTだけはカスタム実装に設定したので、lowerVASTART()のみ実装する。

SDValue MYRISCVXTargetLowering::
LowerOperation(SDValue Op, SelectionDAG &DAG) const
{
  switch (Op.getOpcode())
  {
    case ISD::SELECT        : return lowerSELECT(Op, DAG);
    case ISD::GlobalAddress : return lowerGlobalAddress(Op, DAG);
    case ISD::VASTART       : return lowerVASTART(Op, DAG);
  }
  return SDValue();
}

lowerVASTARTでは、

SDValue MYRISCVXTargetLowering::lowerVASTART(SDValue Op, SelectionDAG &DAG) const {
  MachineFunction &MF = DAG.getMachineFunction();
  MYRISCVXFunctionInfo *FuncInfo = MF.getInfo<MYRISCVXFunctionInfo>();

  SDLoc DL = SDLoc(Op);
  SDValue FI = DAG.getFrameIndex(FuncInfo->getVarArgsFrameIndex(),
                                 getPointerTy(MF.getDataLayout()));

  // vastart just stores the address of the VarArgsFrameIndex slot into the
  // memory location argument.
  const Value *SV = cast<SrcValueSDNode>(Op.getOperand(2))->getValue();
  return DAG.getStore(Op.getOperand(0), DL, FI, Op.getOperand(1),
                      MachinePointerInfo(SV));
}

LowerFormalArguments()つまり可変長引数を持つ関数が呼び出されたときの引数の処理について、可変長引数であれば専用のルーチンを呼び出す。writeVarArgRegsを呼び出す。

SDValue
MYRISCVXTargetLowering::LowerFormalArguments (SDValue Chain,
                                              CallingConv::ID CallConv,
                                              bool IsVarArg,
                                              const SmallVectorImpl<ISD::InputArg> &Ins,
                                              const SDLoc &DL, SelectionDAG &DAG,
                                              SmallVectorImpl<SDValue> &InVals) const {
...
  // 可変長引数を処理する場合は、特殊なwriteVarArgRegsに飛ぶ。
  if (IsVarArg)
    writeVarArgRegs(OutChains, Chain, DL, DAG, CCInfo);

  return Chain;
}

このwriteVarArgRegsは、ABIがLP32の時にレジスタ渡しをしたデータをスタックに戻す。せっかくレジスタ渡しをしたのにどうしてスタックに戻すんだ?と思われるかもしれないが、それ以降の処理は一律してfor文の処理で統一するため、レジスタ渡し、メモリロードの2種類でアセンブリを生成したくない、という意図があると思われる。

  /* for文の前に、レジスタ渡しした可変長引数の要素をスタックに置いておく。 */
  for (i = 0; i < amount; i++)
  {
    /* valの取得はスタックから一律に受け取る */
    val = va_arg(vl, int);
    sum += val;
  }
void MYRISCVXTargetLowering::writeVarArgRegs(std::vector<SDValue> &OutChains,
                                             SDValue Chain, const SDLoc &DL,
                                             SelectionDAG &DAG,
                                             CCState &State) const {
  ArrayRef<MCPhysReg> ArgRegs = ABI.GetVarArgRegs();
  unsigned Idx = State.getFirstUnallocated(ArgRegs);
  unsigned RegSizeInBytes = Subtarget.getGPRSizeInBytes();
  MVT RegTy = MVT::getIntegerVT(RegSizeInBytes * 8);

  ...
  /* 引数渡しに使用しているレジスタを1つ1つ退避していく */
  for (unsigned I = Idx; I < ArgRegs.size();
       ++I, VaArgOffset += RegSizeInBytes) {

    unsigned Reg = addLiveIn(MF, ArgRegs[I], RC);
    SDValue ArgValue = DAG.getCopyFromReg(Chain, DL, Reg, RegTy);
    FI = MFI.CreateFixedObject(RegSizeInBytes, VaArgOffset, true);
    SDValue PtrOff = DAG.getFrameIndex(FI, getPointerTy(DAG.getDataLayout()));
    /* Store命令を生成 */
    SDValue Store =
        DAG.getStore(Chain, DL, ArgValue, PtrOff, MachinePointerInfo());
    cast<StoreSDNode>(Store.getNode())->getMemOperand()->setValue(
        (Value *)nullptr);
    OutChains.push_back(Store);
  }
}

LLVMのバックエンドを作るための第一歩 (37. 構造体を値渡しするためのByval属性の引数 2)

f:id:msyksphinz:20190425001356p:plain

ByVal属性の値を関数の引数として受け渡す処理の続き。

それでは、次に呼び出し側はどのようになっているのだろうか。呼び出し側でも、ByVal属性のついた引数を一つ一つコピーして関数呼び出され側に渡す必要がある。関数呼び出し側は、MYRISCVXTargetLowering::LowerCallを改造するのだった。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
SDValue
MYRISCVXTargetLowering::LowerCall(TargetLowering::CallLoweringInfo &CLI,
                                  SmallVectorImpl<SDValue> &InVals) const {
...
  CCInfo.rewindByValRegsInfo();
...
  for (unsigned i = 0, e = ArgLocs.size(); i != e; ++i) {
...
    if (Flags.isByVal()) {
      unsigned FirstByValReg, LastByValReg;
      unsigned ByValIdx = CCInfo.getInRegsParamsProcessed();

      CCInfo.getInRegsParamInfo(ByValIdx, FirstByValReg, LastByValReg);

      assert(Flags.getByValSize() &&
             "ByVal args of size 0 should have been ignored by front-end.");
      assert(ByValIdx < CCInfo.getInRegsParamsCount());
      assert(!IsTailCall &&
             "Do not tail-call optimize if there is a byval argument.");
      passByValArg(Chain, DL, RegsToPass, MemOpChains, StackPtr, MFI, DAG, Arg,
                   CCInfo, FirstByValReg, LastByValReg, Flags, Subtarget.isLittle(), VA);
      CCInfo.nextInRegsParam();
      continue;
    }
...
    

LowerFormalArgumetsと内容は似ている。isByVal()フラグが立っていると処理が開始され、FirstByValRegsLastByValRegsに引数渡しに使用されるレジスタが設定される。copyByValRegsとは逆に、passByValArgsを呼び出して、引数をコピーしている。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
void MYRISCVXTargetLowering::
passByValArg(SDValue Chain, const SDLoc &DL,
             std::deque< std::pair<unsigned, SDValue> > &RegsToPass,
             SmallVectorImpl<SDValue> &MemOpChains, SDValue StackPtr,
             MachineFrameInfo &MFI, SelectionDAG &DAG, SDValue Arg,
             const CCState &CC, unsigned FirstReg, unsigned LastReg,
             const ISD::ArgFlagsTy &Flags, bool isLittle,
             const CCValAssign &VA) const {
...
      if (NumRegs) {
    const ArrayRef<MCPhysReg> ArgRegs = ABI.GetByValArgRegs();
    bool LeftoverBytes = (NumRegs * RegSizeInBytes > ByValSizeInBytes);
    unsigned I = 0;

    // レジスタに引数の値をコピーしていく。
    for (; I < NumRegs - LeftoverBytes;
         ++I, OffsetInBytes += RegSizeInBytes) {
      SDValue LoadPtr = DAG.getNode(ISD::ADD, DL, PtrTy, Arg,
                                    DAG.getConstant(OffsetInBytes, DL, PtrTy));
      SDValue LoadVal = DAG.getLoad(RegTy, DL, Chain, LoadPtr,
                                    MachinePointerInfo());
      MemOpChains.push_back(LoadVal.getValue(1));
      unsigned ArgReg = ArgRegs[FirstReg + I];
      RegsToPass.push_back(std::make_pair(ArgReg, LoadVal));
    }
...
  // Copy remainder of byval arg to it with memcpy.
  unsigned MemCpySize = ByValSizeInBytes - OffsetInBytes;
  SDValue Src = DAG.getNode(ISD::ADD, DL, PtrTy, Arg,
                            DAG.getConstant(OffsetInBytes, DL, PtrTy));
  SDValue Dst = DAG.getNode(ISD::ADD, DL, PtrTy, StackPtr,
                            DAG.getIntPtrConstant(VA.getLocMemOffset(), DL));
  Chain = DAG.getMemcpy(Chain, DL, Dst, Src,
                        DAG.getConstant(MemCpySize, DL, PtrTy),
                        Alignment, /*isVolatile=*/false, /*AlwaysInline=*/false,
                        /*isTailCall=*/false,
                        MachinePointerInfo(), MachinePointerInfo());
  MemOpChains.push_back(Chain);

レジスタに入りきらかなった引数は、なんとmemcpyを呼び出して一気にスタックにコピーする。これで、ByValに対する処理は完了だ。

それでは、上記のコードをコンパイルしてアセンブリを生成してみる。

./bin/llc -stats -debug -target-abi=lp32 -march=myriscvx32 -mcpu=simple32 -enable-MYRISCVX-tail-calls=true -relocation-model=static -filetype=asm func_struct_simple.bc -o -
  • func() (関数呼び出され側)
_Z4func1S:
...
# %bb.0:                                # %entry
        addi    x2, x2, -32
        .cfi_def_cfa_offset 32
        addi    x5, x2, 0
        # レジスタ渡しした値をすべてスタックにコピーする。
        addi    x6, x5, 28
        sw      x17, 0(x6)
        addi    x17, x5, 24
        sw      x16, 0(x17)
        addi    x16, x5, 20
        sw      x15, 0(x16)
        addi    x15, x5, 16
        sw      x14, 0(x15)
        addi    x14, x5, 12
        sw      x13, 0(x14)
        addi    x13, x5, 8
        sw      x12, 0(x13)
        ori     x12, x5, 4
        sw      x11, 0(x12)
        addi    x11, zero, 0
        sw      x10, 0(x2)
        addi    x10, x11, 0
...
  • call_func() (関数呼び出し側)
...
        jal     memcpy         # レジスタ渡しができない値をすべてmemcpyでスタックにコピーする。
        addi    x10, x9, 28
        lw      x17, 0(x10)    # レジスタ渡しする値をレジスタにコピー x[7]
        addi    x10, x9, 24
        lw      x16, 0(x10)    # レジスタ渡しする値をレジスタにコピー x[6]
        addi    x10, x9, 20
        lw      x15, 0(x10)    # レジスタ渡しする値をレジスタにコピー x[5]
        addi    x10, x9, 16
        lw      x14, 0(x10)    # レジスタ渡しする値をレジスタにコピー x[4]
        addi    x10, x9, 12
        lw      x13, 0(x10)    # レジスタ渡しする値をレジスタにコピー x[3]
        addi    x10, x9, 8
        lw      x12, 0(x10)    # レジスタ渡しする値をレジスタにコピー x[2]
        ori     x10, x9, 4
        lw      x11, 0(x10)    # レジスタ渡しする値をレジスタにコピー x[1]
        lw      x10, 40(x2)    # レジスタ渡しする値をレジスタにコピー x[0]
        jal     _Z4func1S   # 関数呼び出し
...
f:id:msyksphinz:20190707001604p:plain
構造体を値渡しで呼んだ場合の引数の処理。ByVal属性が付属される。一部はレジスタ渡し、残りはスタック渡しされる。

高性能プロセッサの分岐予測のサーベイ論文を読んで分岐予測について学ぶ (4. 予測が難しい分岐のための分岐予測器 2)

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

読んでいるのは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


4.4 データの相関に基づく分岐予測

データの差分に基づく分岐予測

Heilら[62]の提案。データの相関に基づく分岐予測を提案した。例えば、図16(a)のようなループを想像してほしい。このプログラムでは平均してループが21回実行されるものとする。これはグローバルBHRのビット長よりも長いため、通常の方法では分岐予測をすることができない。さらに、ループの回数はlenの値に相関するため、分岐予測が難しい。

xlen = len / OPSIZE;
while (xlen > 0) {
    // 何らかの処理
    xlen -= 1;
}

しかし、xlenの値が何らかの値に近づけば分岐を行うわけである。この場合は、いくつかの分岐命令がxlenと0の比較を行い、その結果に基づいて分岐を行っているため、この場合はレジスタの値に分岐の結果が強く相関するわけである。

そこで、履歴テーブルに、分岐命令のレジスタの差分値を保存しておくという方法が考えられる。これを差分分岐予測器 (branch difference predictor) と呼ばれる。図16(b)では、この分岐予測器の構成を示している。

しかし、いくら差分値とは言え保存すべきパタンのサイズは大きくなる。そこで、通常時は"Backing Predictor"を使って差分値を使用しない予測を行い、Backing Predictorと予測が異なる場合のみ予測値を書き換えるための"Rare Event Predictor (REP)"を実装する。REPはタグ付きのキャッシュのような構成をしており、予測が難しいような頻度の低い分岐を予測する。Backing Predictorが予測を外した時にのみREPがアップデートされる。エイリアシングが発生し精度が下がることを防ぐために、Backing Predictorは予測を行った場合のみ更新される。

f:id:msyksphinz:20190708230607p:plain
値の差分に基づく分岐予測器 (本論文より抜粋)

命令のチェーンに基づく分岐予測

Chenら[75]の提案では、値ベースの分岐予測器として、命令のチェーンを記録する方法を取っている。分岐の結果に影響しているすべてのレジスタの値が、まえの分岐実行時と同一であるとしたら、そのまま次の分岐の結果となるはずである。しかし、値ベースの分岐予測を行うためにすべての依存性のあるレジスタの値を使用することは現実的ではないので、データ依存のチェーンを使用することになる。

この方式ではテーブルを用意し、このテーブルを参照するためにPCとレジスタIDを使用する。さらにハッシュとしてレジスタの値を使用することで、値が異なるレジスタの記録同士でテーブルがぶつからないように配慮している。

この手法は、使用するテーブルのサイズを同一とするならば、ハイブリッド分岐予測器よりも高い分岐予測精度を実現する。

インフルエンサー分岐

インフルエンサー分岐は、分岐予測をすべき分岐 B _ {0} に対して、そのオペランドを決定するための分岐 B _ {i} に関する分岐予測手法である。このため、インフルエンサー分岐と呼ばれる。

例として、図17(a)のような制御フローがあり、そのうち網掛けの部分をCPUが通過しているとする。この時B8を予測したいとすると、B8はレジスタR2とR3に依存していることが分かる。

f:id:msyksphinz:20190708230659p:plain
制御フローに基づくインフルエンサー分岐 (本論文より抜粋)

BB3でR2の値が生成され、BB2でR1の値が生成される。R3の値はBB7において生成される。このため、以下のように考える。

この情報を利用して、アーキテクチャレジスタに書き込む最新の命令について、インフルエンサー分岐を追跡し、その情報を「インフルエンサーレジスタファイル (IRF)」に格納する。条件分岐の場合には、インフルエンサー分岐の詳細はそのソースオペランドの生成元から継承される。また、そのソースレジスタのIRFエントリが読みだされ、ORされて、「インフルエンサー分岐ビットマップ(IBB)」が生成される。

この情報に基づいて、2つの手法を提案している。

  • ゼロ化方式(図18(a)) : グローバル履歴内の非インフルエンサービットは、それらをIBBとANDすることによってマスクされる。したがって、インフルエンサー分岐以外のビットはグローバル履歴内ではマスクして見えなくなる。この値に基づきFold演算とOR演算によって予測器のインデックスを参照するために必要なインデックスが生成される。
  • パッキング方式(図18(b)) : ゼロ化方式と似ているが、違いは、インフルエンサーでないビットをマスク後に除去してしまい、インデックスのサイズを減らす。この後にFold演算を行って、Predictorのインデックスに入力する。
f:id:msyksphinz:20190708230741p:plain
インフルエンサー分岐の2種類の構成方法 (本論文より抜粋)

高性能プロセッサの分岐予測のサーベイ論文を読んで分岐予測について学ぶ (4. 予測が難しい分岐のための分岐予測器)

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

読んでいるのは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


4.1 ループの抜け出しを予測する

Sheerwoodら[73]によれば、ローカルヒストリを用いる分岐予測器では、ローカルヒストリのサイズがNの場合に、Nよりも大きなループを抜け出すタイミングを予測するのは不可能である。Nよりも大きなグローバルヒストリを使用する場合か、ループを抜け出す前にはっきりとした分岐のシーケンスが登場する場合にのみ分岐予測を行うことが可能である。このような状態は、ループ終了バッファ(Loop Exit Buffer : LEB)ベースの分岐予測器を用いてループの終了を予測する方式が提案されている。図10(a)はループの分岐の例を示している。LEBはメインの分岐予測器により予測が失敗した逆方向の分岐をループの分岐として検出し、図10(b)に示すようにLEBに挿入する。全体的な分岐予測器の設計として、メインの分岐予測器は予測を提供し、LEBベースの分岐予測器が高い精度を達成することができる場合にのみ、その予測がLEBにより上書きされる。

LEBでは、TripCountフィールドでループ分岐が最後に不成立だった時から何回成立しているかを示している。Confidenceビットは同じループのTripCountのループが2回以上連続で登場したことを示している。LEBにヒットするすべての分岐では、もしIterCountがTripCountと一致しない場合、IterCountが連続してインクリメントさせる。つまり、IterCountは分岐が連続で成立した回数を記録している。IterCountがTripCountと同じ値になれば、Confidenceビットが1に設定され、ループの終了が予測される。分岐が解決されると、分岐不成立のTripCountとConfidenceビットがアップデートされる。図10(b)には示していないが、分岐予測の失敗から復帰するための余分なカウンタも搭載されている。ループ予測器は、ウォームアップが終了すると最大で100%まで分岐を予測することができる。しかし、ループカウントが頻繁に変わる場合、このような方式では上手く動作しない。

f:id:msyksphinz:20190707201815p:plain
図10. ループの分岐(a)、LEBによる動作 (b) (本論文より抜粋)

Sendagら[46]らの提案は、これまでの分岐予測器に追加して補完的な分岐予測器 (complementary BP: CBP)を追加するものである。CBPは主に分岐予測に失敗した分岐に着目し、一方でこれまでの分岐予測器は予測可能な分岐に対して投機を行う。彼らは、「分岐予測失敗予測器(branch misprediction predictor : BMP)」というものを使用し、任意のインデックスに対して分岐予測に失敗した分岐間にコミットされた分岐の数を使用する。この情報を使って次に分岐予測に失敗するタイミングを計測する。この情報を使用して、分岐予測に失敗する分岐に対して予測を変更し、正しい方向に分岐を実行する。BMPにより除去される分岐予測の失敗は以下の通りである。

  1. ループの回数がBPよりも長く、ループの回数が変化する分岐
  2. break命令などによるループから早期に抜け出す状態

BMPはどのような分岐予測引退しても動作する。彼らの技術は全体的な分岐の精度を向上させ、電力を削減できる。

Laiら[92]の提案は、複数の予測訂正器を使用してメインの分岐予測器が予測に失敗した分岐を正すためのデザインである。それぞれの予測訂正器は、異なるパタンに着目する。したがって、タグにより記憶されている分岐のみを正しく訂正することができる。それぞれの予測訂正器では、必要ならば最も自信のない予測は置き換えられる。彼らは部分アップデートポリシーのgskew BP[86]を使用し、全ての予測訂正器はその自身の度合いを記録し、予測の訂正はその自身の値がスレッショルドを超えた場合にのみ訂正されるようになっている。図11に示すように、彼らは3つの予測訂正器実装した:

  1. ループ終了の分岐を検出するためのインターバル予測訂正器。固定のインターバルでメインの分岐予測器が予測に失敗するという仮定のもと、その予測を訂正するという考え方である。彼らは2つのインターバル値を使用して、早期のループ終了のパタンにも対応している。メインの予測器が予測が正しいかそうでないかによって、予測訂正器の自信度の値は変動する。もしメインの分岐予測器が予測に失敗し、現在のカウンタが2つのインターバル値と一致しない場合、カウンタのインターバル値を変更し、自信度を下げる。また、固定値の自信度が高い場合のインターバル値は1づつ下げられる。それぞれのインターバル予測修正器と、SendagらのBMP[46]は、分岐予測に失敗したときに発動し、次の予測の失敗を避けるためのものであると言う事に注意すること。
  2. ローカルヒストリとバイモーダルのパタンを記録するためのバイモーダル予測訂正器。
  3. 2レベルのパタンを記録する2レベルの予測訂正器[78]。他の予測訂正器と同様に「逆転・アップデート」のスキームを持っている。グローバル履歴を使用するため、高い精度を実現し自信度のスレッショルドは他の予測訂正器よりも低い。
f:id:msyksphinz:20190707202022p:plain

精度が低くなるという面では、全体的な分岐予測器のデザインとしては高い予測精度を実現する。このデザインの限界としては、メインの分岐予測器と予測訂正器が同時に動き、半分以上が同じ予測を出力したならば、この構造自体が冗長でありエネルギーを無駄に消費してしまっていることである。

4.2 多重ループをどのように予測するか

ワームホールプレディクタ

Albericio[61]らの提案。ネストされたループを分岐予測するのは難しい。どちらかといえば、ネストされた内部のループを分岐予測するのには、内部のループの結果よりも外部のループの結果を使用した方が精度が上がるという研究がある。

数学的には、2重ループ内の分岐の結果Outcome[i][j]は、Outcome[i-1][j+D]に相関するという研究結果がある。ここでDは0,-1,+1などの比較的小さな値であり、分岐の結果は、1つ前(i-1)外部ループの、近くのインデックス(j+D)に相関しているということである。

図12のループを考えると、arr1, arr2, arr3, arr4の値がループ内で不変であるとすると、Branch1の結果Outcome[i][j]Outcome[i-1][j+1]と同一であり、Branch2の結果Outcome[i][j]Outcome[i-1][j]に弱相関している。Branch3の分岐予測結果Outcome[i][j]はBranch4の分岐予測結果Outcome[i][j]と一致している。

for ( i = 0; i < iMax; i++ )
  for ( j = 0; j < jMax; j++) {
     if ( arr1[i+j] > 0 ) {...}               // Branch1
     if ( arr2[i][j]-arr2[i-1][j] > 0) {...}  // Branch2
     if ( arr3[j] > 0 )                       // Branch3
     if ( arr4[j] > 0 ) {...}                 // Branch4

これらの分岐予測の情報を1次元の配列に保存してしまった場合、予測することは難しいが、多次元のマトリックスに保存すると、強い相関を観察することができる。図13(b)は相関の情報を2次元の行列に保存した結果である。

彼らの提案では、多次元の分岐結果保存用の配列に保存して、パタンを見つけだすという分岐予測器である。サイドプレディクタ―としてISL-TAGE分岐予測器を使用した。これにより、全体のループカウント数から内部ループの分岐を特定することができるようになる。

それぞれのイタレーションでほぼ同一の動きをする分岐では、最初のイタレーションの結果を保存しておき、これを再利用することで分岐予測の精度を向上させることができる。図13(a)この時のサンプルプログラムで、Branch1はjの値にのみ依存しており、つまりjが同一である場合は分岐結果は同一であるため、前のイタレーションの結果が分かっていれば分岐予測は容易に実現できる。

//Z: vector of objects, p: a point
while(true)                  // Loop1
  for(j=0; j<nObjects; j++)  // Loop2
    if(dist(Z[j],p)< theta)  // Branch 1
    { /* do something */ }
f:id:msyksphinz:20190707202449p:plain
図13. Branch1のイタレーション空間(本論文より抜粋)

例えばほかの分岐予測では、対角線上のパタンを見つけることもできる。図14(a)では、分岐の結果はijに依存している。したがって分岐の結果は対角線上のパタンとして現れる。このような分岐では、最初の分岐の結果を保存しておく。

// A: a matrix, B: right hand side
// X & X0: current & partial solution
for ( i = 0; i < N; i++ ) {   // Loop3
  X0[i] = B[i];
  for ( j = 0; j < N; j++ )   // Loop4
    if ( j != i ) // Branch 2
      X0[i]=X0[i]-A[i + j*n]*X[j];
  X0[i] = X0[i] /A[i + i*n];
}

Branch2の結果は以下のようになり、このパタンを解析すれば次のイタレーションでの分岐予測ができるようになる。

f:id:msyksphinz:20190707202641p:plain
図14(b) Branch2のイタレーション空間

彼らの提案した分岐予測は、ISL-TAGEに対して副次的に分岐予測を行い、精度が高ければISL-TAGEの予測に対してオーバライドを行う。サイドプレディクタとして使用することで、分岐予測の精度を大幅に上げることができる。

Seznecら[16]の提案は、ネストされたループの分岐の相関を"最内部イタレーション (Inner-most loop iteration : IMLI)"カウンタベースの分岐予測として実現する。IMLIカウンタはループの繰り返しの回数をカウントしており、負の方向へ分岐する分岐命令(ループ終了条件の分岐)が何回成功したかをカウントしている。

この情報を使用するための2つのコンポーネントを提案している。

  • 予測が難しい分岐命令に関しては、内部のイタレーションがカウントされる。つまり、Outcome[i][j] == Outcome[i-1][j]である。このような「同じイタレーションの相関」については、1番目のコンポーネントはIMLIカウンタとPCによりインデックスされるテーブルを用いる。ワームホールプレディクタは通常のループにおいて固定されたイタレーションのみカウントするが内部ループのイタレーション毎に実行される相関のみを追跡するが、この分岐予測器にはその制限はない。
  • ワームホールプレディクタは対角線パタンの予測をすることができるが、これに対処するためにそれらの2番目のコンポーネントは、Outcome[i][j]を予測しながらOutcome[i-1][j]Outcome[i-1][j-1]の両方が可能になるようにテーブルを使用する。彼らのIMLIベースのBPコンポーネントは、GHベースのBPと一緒に使用することができ、それらを使用することで、ローカルヒストリーを使用することによるさらなる利点は小さくなる。さらに、それらの提案された分岐予測器の推測状態は容易に管理可能であり、それらの分岐予測器を任意のTAGEまたはニューラルベースの分岐予測器を有するサイドプレディクタとして使用することは高い精度を提供する。

4.3 繰り返し回数の多い分岐を予測する

Kampeらの提案 : プログラムによっては、履歴ビットよりも長い期間でループが発生し、予測を行うためのすべての分岐実行パタンを記録することができない。すべての分岐の履歴を取るためには、 2^ {13}ビットの履歴が必要となる。これほどの分岐履歴ビットは用意できないので、履歴を時間領域から周波数領域に変換することで、履歴レジスタのサイズを削減することができる。例えば、 2^ {13} ビットの履歴を表現するために必要なビットは52ビットまで削減される。これに基づいて、離散フーリエ変換(DFT)ベースのサイドプレディクターを提案する。DFTの待ち時間とオーバヘッドが生じるため、この分岐予測器はメインの分岐予測の結果が99.99%未満の場合に適用される。

分岐結果のTaken/NotTakenをそれぞれ1/0として、DFTを使用して分岐履歴を周波数領域に変換する(図15(a), 図15(b))。最大ピークの周波数成分だけを記録し、残りの成分は図15(c)に示すように取り除かれる。そして、すべての周波数成分 A_n \sin(\omega_n t) を加算し、その合計がしきい値よりも高い場合(図15(d))に、時間 t で分岐が発生すると予測される(図15(e))。この分岐予測器の欠点は、周波数成分を使用するために、Taken/NotTakenの確率が50%-50%の場合には精度が高くない。このため、ハイブリッド分岐予測器のコンポーネントとして使用すると、予測ミス率を大幅に削減することができる。

周波数領域に履歴を保存する方式と、幾何学的履歴長テーブル(TAGE)の両方を使用することで、大きな周期を持つ分岐の予測を向上させることができる。周波数領域に履歴を保存する方式は長い履歴を必要とする分岐に焦点を当てているのに対して、TAGEの方式は短期及び中長期の履歴を必要とする分岐に焦点を合わせている。

f:id:msyksphinz:20190707202728p:plain
図15. フーリエ変換をベースにした分岐予測器 (本論文より抜粋)

LLVMのバックエンドを作るための第一歩 (37. 構造体を値渡しするためのByval属性の引数 1)

f:id:msyksphinz:20190425001356p:plain

今までの関数処理の中で、引数は基本的にポインタか値渡し、そして値も何らかの型の値を1つずつ渡していくという形式だった。しかし、C言語では構造体などの複数の要素をまとめた型を渡すことができる。また、C言語では構造体の値をそのまま値渡しで引数として渡すことができる。

  • func_struct_simple.cpp
struct S
{
  int x[17];
};

void func (struct S elem) {
  for(int i = 0; i < 17; i++) {
    elem.x[i] ++;
  }
}

void call_func () {
  struct S elem;
  for (int i = 0; i < 17; i++) {
    elem.x[i] = i;
  }
  func (elem);
}

上記のプログラムでは、funcに構造体struct S elemを値渡ししている。つまり、func内でstruct S elemの変更を行っても、その変更の結果は関数の呼び出し側にの値に影響を与えない。ポインタではないのでアセンブリ的には、引数の要素を一つ一つコピーして、関数呼び出され側に値を渡してやる必要がある。

上記の例では、実際に渡さなければならない値はx[17]の17個だ。しかしMYRISCVXのABIでは引数私で使用できるレジスタの数はA0-A7の8個までだ。この場合、どのようにして構造体の値を渡していけばよいだろう?

まず、clangでbyval属性の引数が生成されることを確認する。上記のfunc_struct_simple.cppをclangでLLVM IRを生成する。

./bin/clang -O1 func_struct_simple.cpp -emit-llvm
./bin/llvm-dis func_struct_simple.bc -o -
%struct.S = type { [17 x i32] }
; byval属性が引数に付加された。
define dso_local void @_Z4func1S(%struct.S* byval nocapture align 8 %elem) local_unnamed_addr #0 {
entry:
  br label %for.body
...

引数にbyval属性が付加されていることが分かる。byval属性が付加されている引数には、特別な処理を付け加える。引数の解析だから、当然処理を追加しなければならないのはMYRISCVXTargetLowering::LowerFormalArguments()となる。

まず、MYRISCVXのCalling ConventionにByVal渡しを有効にしなければなりない。このためには、MYRISCVXCallingConv.tdを追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXCallingConv.td
...
def CC_MYRISCVX_ByVal : CallingConv<[   // ByVal渡しを有効とする。
  CCIfByVal<CCPassByVal<4, 4>>
]>;
...
def CC_LP32 : CallingConv<[
  CCIfByVal<CCDelegateTo<CC_MYRISCVX_ByVal>>,   // LP32ではByVal渡しを有効にする。
...
]>;
...
def CC_STACK32 : CallingConv<[
  CCIfByVal<CCDelegateTo<CC_MYRISCVX_ByVal>>,   // STACK32ではByVal渡しを有効にする。
...
]>;

次にLowerFormalArgumetsを編集する。引数の解析にByvalの条件を追加する。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXISelLowering.cpp
SDValue
MYRISCVXTargetLowering::LowerFormalArguments (SDValue Chain,
                                              CallingConv::ID CallConv,
                                              bool IsVarArg,
                                              const SmallVectorImpl<ISD::InputArg> &Ins,
                                              const SDLoc &DL, SelectionDAG &DAG,
                                              SmallVectorImpl<SDValue> &InVals) const {

...
  CCInfo.rewindByValRegsInfo();
...
  // 引数毎に処理を行うループ
  for (unsigned i = 0, e = ArgLocs.size(); i != e; ++i) {
    ...
    ISD::ArgFlagsTy Flags = Ins[i].Flags;
    ...
    // ByVal属性がついているかチェックする
    if (Flags.isByVal()) {
      assert(Ins[i].isOrigArg() && "Byval arguments cannot be implicit");
      CCInfo.getInRegsParamInfo(ByValIdx, FirstByValReg, LastByValReg);

      assert(Flags.getByValSize() &&
             "ByVal args of size 0 should have been ignored by front-end.");
      assert(ByValIdx < CCInfo.getInRegsParamsCount());
      copyByValRegs(Chain, DL, OutChains, DAG, Flags, InVals, &*FuncArg,
                    FirstByValReg, LastByValReg, VA, CCInfo);
      CCInfo.nextInRegsParam();
      continue;
    }
...

ByVal属性が付加されている場合の処理を追加した。まず、ByVal属性が付加されている引数を見つけると、これがレジスタ経由で渡すことができるのか、そしてレジスタ私ができる場合はどのレジスタが使用できるのかを確認する。これがCCInfo.getInRegsParamInfo(ByValIdx, FirstByValReg, LastByValReg); だ。FirstByValRegsLastByValRegsが、引数渡しの場合に使用できるレジスタの最初のインデックスと最後のインデックスになる。この情報を使用してレジスタコピーのコードを生成する。具体的には、RISC-Vの場合はA0からA7までのレジスタのうち、上記のサンプルコードならばx[0]-x[7]までの値がレジスタ渡しで渡される。この値を、スタックに置きなおして実際に使用できるようにするのがcopyByValRegsの働きになる。

void MipsTargetLowering::copyByValRegs(
    SDValue Chain, const SDLoc &DL, std::vector<SDValue> &OutChains,
    SelectionDAG &DAG, const ISD::ArgFlagsTy &Flags,
    SmallVectorImpl<SDValue> &InVals, const Argument *FuncArg,
    unsigned FirstReg, unsigned LastReg, const CCValAssign &VA,
    MipsCCState &State) const {
...
  unsigned NumRegs = LastReg - FirstReg;  // 引数渡しに使用できるレジスタの数
  unsigned RegAreaSize = NumRegs * GPRSizeInBytes;
...
  if (!NumRegs)  // 引数渡しに使用できるレジスタがなければ、この処理は不要
    return;
...
  for (unsigned I = 0; I < NumRegs; ++I) {
    unsigned ArgReg = ByValArgRegs[FirstReg + I];
    unsigned VReg = addLiveIn(MF, ArgReg, RC);
    unsigned Offset = I * GPRSizeInBytes;
    SDValue StorePtr = DAG.getNode(ISD::ADD, DL, PtrTy, FIN,
                                   DAG.getConstant(Offset, DL, PtrTy));
    SDValue Store = DAG.getStore(Chain, DL, DAG.getRegister(VReg, RegTy),
                                 StorePtr, MachinePointerInfo(FuncArg, Offset));
    OutChains.push_back(Store);
  }

スタックポインタをベースにして、ストア命令を生成する。関数の呼びされ側(Callee)では、レジスタ渡しを行った値をスタックに戻しているのだ。

f:id:msyksphinz:20190707001313p:plain
構造体を値渡しで呼んだ場合の引数わたしの処理。ByVal属性が付属される。一部はレジスタ渡し、残りはスタック渡しされる。

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

f:id:msyksphinz:20190425001356p:plain

最後に、アセンブリの生成です。TAILCALLTAILCALL_Rは疑似命令なのでそのままではアセンブリ命令を出力することができない。これを置き換える処理は、tdファイルからすでに生成されている。MYRISCVXGenMCPseudoLowering.incを見てみみる。

  • build-myriscvx/lib/Target/MYRISCVX/MYRISCVXGenMCPseudoLowering.inc
bool MYRISCVXAsmPrinter::
emitPseudoExpansionLowering(MCStreamer &OutStreamer,
                            const MachineInstr *MI) {
  switch (MI->getOpcode()) {
    default: return false;
    case MYRISCVX::TAILCALL: {
      MCInst TmpInst;
      MCOperand MCOp;
      TmpInst.setOpcode(MYRISCVX::JAL);
      // Operand: target
      lowerOperand(MI->getOperand(0), MCOp);
      TmpInst.addOperand(MCOp);
      EmitToStreamer(OutStreamer, TmpInst);
      break;
    }
    case MYRISCVX::TAILCALL_R: {
      MCInst TmpInst;
      MCOperand MCOp;
      TmpInst.setOpcode(MYRISCVX::JALR);
      // Operand: rs1
      lowerOperand(MI->getOperand(0), MCOp);
      TmpInst.addOperand(MCOp);
      EmitToStreamer(OutStreamer, TmpInst);
      break;
    }
  }
  return true;
}

TAILCALLではJAL命令に置き換え、TAILCALL_RノードではJALR命令に置き換えるシーケンスが生成されていることが分かる。なぜこれが生成されたかというと、これらのノードにはPseudoInstExpansionが追加されており、自動的に展開することができるようになっていたからです。

  • llvm-myriscvx/lib/Target/MYRISCVX/MYRISCVXAsmPrinter.cpp
//- EmitInstruction() must exists or will have run time error.
void MYRISCVXAsmPrinter::EmitInstruction(const MachineInstr *MI) {
...
  do {
    // Do any auto-generated pseudo lowerings.
    if (emitPseudoExpansionLowering(*OutStreamer, &*I))
      continue;
... 

ここまでで、末尾関数呼び出し最適化を適用した場合とそうでない場合についてアセンブリ命令を生成してみる。

# 末尾関数呼び出し最適化を適用した場合
./bin/llc -stats -debug -target-abi=lp64 -march=myriscvx32 -mcpu=simple32 -enable-MYRISCVX-tail-calls=true -relocation-model=static -filetype=asm result/tailcall_-O1_-march=myriscvx32_-mcpu=simple32_static_lp64_-enable-MYRISCVX-tail-calls=true/tailcall.bc -o -
# 末尾関数呼び出し最適化を適用しない場合
./bin/llc -stats -debug -target-abi=lp64 -march=myriscvx32 -mcpu=simple32 -enable-MYRISCVX-tail-calls=false -relocation-model=static -filetype=asm result/tailcall_-O1_-march=myriscvx32_-mcpu=simple32_static_lp64_-enable-MYRISCVX-tail-calls=false/tailcall.bc -o -

- 末尾関数呼び出し最適化を適用した場合の生成されたアセンブリ命令

​```asm
// 関数呼び出され側 (Callee)
_Z14tail_call_funcii:
...
# %bb.0:                                # %entry
    add x10, x11, x10
    ret x1

// 関数呼び出し側 (Caller)
_Z14tail_call_mainiiii:
...
# %bb.0:                                # %entry
    sub x10, x10, x11
    mul x11, x13, x12
    jal _Z14tail_call_funcii
...
  • 末尾関数呼び出し最適化を適用しない場合の生成されたアセンブリ命令
// 関数呼び出され側 (Callee)
_Z14tail_call_funcii:
...
# %bb.0:                                # %entry
    add x10, x11, x10
    ret x1

_Z14tail_call_mainiiii:
...
# %bb.0:                                # %entry
    addi    x2, x2, -8
    .cfi_def_cfa_offset 8
    sw  x2, 4(x2)               # 4-byte Folded Spill
    .cfi_offset 2, -4
    sub x10, x10, x11
    mul x11, x13, x12
    jal _Z14tail_call_funcii
    lw  x2, 4(x2)               # 4-byte Folded Reload
    addi    x2, x2, 8
    ret x1

末尾関数呼び出し最適化を適用しない場合場合は、_Z14tail_call_funcii呼び出し前にスタックの調整が行われていることが確認できる。一方で、末尾関数呼び出し最適化を適用した場合はスタックの調整コードが省略されている。これにより、関数呼び出しの場合のスタックフレームの消費を抑え、動作を高速化することができる。

f:id:msyksphinz:20190704233655p:plain