FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages

オリジナルLLVMバックエンド実装をまとめる(22. 可変長引数をサポートするための処理)

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によってさらに別の種類のノードをサポートする必要がある。

しかし、可変長引数でも基本的な考え方は変わらない。 関数を呼び出す側は、可能な数だけレジスタに引数を渡し、レジスタに入りきらなければスタックに積み上げていく。 呼び出された関数の処理が、これまでと異なる。 つまり、効率的に可変長引数を扱うために、レジスタ経由で受け付けた引数をいったんスタックにすべて退避していく処理が追加される。 これにより、実際の引数を処理するプログラムを効率的に扱うことができるようになる。

f:id:msyksphinz:20191013114926p:plain
可変長引数での引数の受け渡し方。関数呼び出し側は変更ないが、呼び出され側はスタックへの退避処理が追加される。
  1. 可変長引数に関係するノードの生成条件を変更する。VASTARTのみを取り扱うように変更する。
  2. VASTARTに基づいてDAGを生成するためのMYRISCVXTarget::selectVASTART()を実装する。これにより可変長引数の情報がメモリに渡されるDAGが生成される。
  3. MYRISCVXTargetLowering::LowerFormalArguments()内にwriteVarArgRegs()の呼び出しを追加し、可変長引数を受け取るとその値をスタックに積み上げる操作を追加する。

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

./bin/clang vararg.cpp -emit-llvm -o vararg.rv32.bc --target=riscv32-unknown-elf
./bin/llvm-dis vararg.rv32.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-myriscvx80/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);
  }
}

可変長引数の関数を呼び出す側の処理

次に、可変長引数の関数を呼び出す側の処理が、これは追加の実装は必要ない。

これまで通り引数をレジスタに渡し、レジスタに収まりきらなければスタックに積んでいけば良い。

上記のコードをコンパイルして、どのようなコードが生成されているのか見てみる。

./bin/clang -O0 -c vararg.cpp -emit-llvm -o vararg.bc
# ABI=LP64でコンパイルした場合
./bin/llc -stats -debug -target-abi=lp -march=myriscvx32 -filetype=asm vararg.bc -o -
# ABI=STACK32でコンパイルした場合
./bin/llc -stats -debug -target-abi=stack -march=myriscvx32 -filetype=asm vararg.bc -o -
  • 可変長引数を渡す側
    • ABI=LP64の場合
        addi    x10, zero, 7    # レジスタ経由で入りきらない引数はスタックを経由する。第8引数
        sw      x10, 0(x2)
        addi    x10, x2, 8
        addi    x11, zero, 9    # レジスタ経由で入りきらない引数はスタックを経由する。第10引数
        sw      x11, 0(x10)
        addi    x10, x2, 4
        addi    x11, zero, 8    # レジスタ経由で入りきらない引数はスタックを経由する。第9引数
        sw      x11, 0(x10)
        addi    x10, zero, 10   # 引数の数 : amount
        addi    x11, zero, 0    # 第1引数
        addi    x12, zero, 1    # 第2引数 ...
        addi    x13, zero, 2
        addi    x14, zero, 3
        addi    x15, zero, 4
        addi    x16, zero, 5
        addi    x17, zero, 6    # レジスタ経由はこれが限界
        jal     _Z5sum_iiz

レジスタ渡しできる限りはレジスタ経由で渡す。A0-A7までのレジスタを使って引数を渡し、それでも足りないので残りはスタック経由で渡している。

  • 可変長引数を受け取る側
    • ABI=LP64の場合
_Z5sum_iiz:
        .cfi_startproc
...
# %bb.0:                                # %entry
        addi    x2, x2, -64
        .cfi_def_cfa_offset 64
        sw      x17, 60(x2)     # 可変引数8をスタックに格納
        sw      x16, 56(x2)     # 可変引数7をスタックに格納
        sw      x15, 52(x2)     # ....
        sw      x14, 48(x2)
        sw      x13, 44(x2)
        sw      x12, 40(x2)
        sw      x11, 36(x2)     # 可変引数1をスタックに格納
        sw      x10, 32(x2)     # 引数amountをスタックに格納
        addi    x10, zero, 0

まず、レジスタ経由で渡した引数をすべてスタックに格納する。もったいないが、こうすることで以降の処理コードをより簡潔にする。