kubo39's blog

ただの雑記です。

各言語処理系からみるLLVMインラインアセンブラ、主にmemory clobberについて

雑メモです。LLVMの実装追いきれてないからなんか間違ってたら、、スマンコ!w

前提

LLVMインラインアセンブラCodeGenの実体としてはnaked function相当。そのため、codegenには関数属性なんかを渡しているし、ジッサイこれが最適化に効いている!

また、RustのドキュメントによるとLLVMインラインアセンブラはmemory clobber(~{memory}的なやつ)をconstraintsとして渡しても無視する。

(一応LLVMの公式ドキュメントにはそれらしい話が書いているが、後述のclangとRustの実装をみる感じではあてにしていなさそうだ)、かわりにmemory constraintがひとつでもあればread/write memoryが起きていると仮定している。

(追記: LLVMでclobberのチェックを行っている箇所のコードではやはりレジスタについてしかチェックしていないようだ)

その代わりに相当する関数属性を付与する(デフォルトで与えられるReadWriteMemory属性)ことで同様の機能を実現している。

LLVMの関数属性のそれぞれの意味は下のリンクを都度参照。

LLVMの関数属性: https://llvm.org/docs/LangRef.html#function-attributes

clang

LLVMに渡している箇所は以下のようになっている。

https://github.com/llvm/llvm-project/blob/6ce23ea0ab6370c944f5e426a20217f93f41aa15/clang/lib/CodeGen/CGStmt.cpp#L2840C1-L2842C63

  llvm::InlineAsm *IA = llvm::InlineAsm::get(
      FTy, AsmString, Constraints, HasSideEffect,
      /* IsAlignStack */ false, AsmDialect, HasUnwindClobber);

ざっくり

  • HasSideEffect: volatile相当
  • IsAlignStack: スタックポインタがalignされてることの保証
  • AsmDialect: AT&T記法かIntel記法か
  • HasUnwindClobber: throwできるか

LLVMを呼ぶところで常にIsAlignStack=falseになっているのが特徴。

memory clobberに関してclangの場合はどうしてるかというと、自前でパースした際にReadOnly/ReadNoneの変数をfalseにセットしている。

https://github.com/llvm/llvm-project/blob/6ce23ea0ab6370c944f5e426a20217f93f41aa15/clang/lib/CodeGen/CGStmt.cpp#L2770

    if (Clobber == "memory")
      ReadOnly = ReadNone = false;

この変更が入ったパッチをみると、

Currently, clang/llvm handles inline-asm instructions in a very conservative manner, choosing not to eliminate the instructions or hoisting them out of a loop even when it's safe to do so. This patch makes changes to clang to attach a readonly or readnone attribute to an inline-asm instruction, which enables passes such as LICM and EarlyCSE to move or optimize away the instruction.

とあり、後述のコメントで参照されているGCCドキュメントの文言:

The "memory" clobber tells the compiler that the assembly code performs memory reads or writes to items other than those listed in the input and output operands (for example, accessing the memory pointed to by one of the input parameters).

I took it to mean that you have to add "memory" to an inline-asm statement's clobber list in the case it reads from memory using one of the input registers. If you don't, gcc will treat it as a read-none statement.

asm ("movl (%1), %0" : "=r" (res) : "r" (ptr) : "memory");

と合わせて、これはLLVMインラインアセンブラではmemory clobberは効果がないとみてよさそうだ。

これらの変数は関数属性の付与に使われる。

  // An inline asm can be marked readonly if it meets the following conditions:
  //  - it doesn't have any sideeffects
  //  - it doesn't clobber memory
  //  - it doesn't return a value by-reference
  // It can be marked readnone if it doesn't have any input memory constraints
  // in addition to meeting the conditions listed above.
  bool ReadOnly = true, ReadNone = true;

volatileを付与している場合はmemory clobberは常に付与されているとみなしてもよさそう。

  // Attach readnone and readonly attributes.
  if (!HasSideEffect) {
    if (ReadNone)
      Result.setDoesNotAccessMemory();
    else if (ReadOnly)
      Result.setOnlyReadsMemory();
  }

Rust

Rustは独自のasm!構文を持っている: https://doc.rust-lang.org/reference/inline-assembly.html

LLVMに渡してるところはここ

extern "C" LLVMValueRef
LLVMRustInlineAsm(LLVMTypeRef Ty, char *AsmString, size_t AsmStringLen,
                  char *Constraints, size_t ConstraintsLen,
                  LLVMBool HasSideEffects, LLVMBool IsAlignStack,
                  LLVMRustAsmDialect Dialect, LLVMBool CanThrow) {
  return wrap(InlineAsm::get(unwrap<FunctionType>(Ty),
                             StringRef(AsmString, AsmStringLen),
                             StringRef(Constraints, ConstraintsLen),
                             HasSideEffects, IsAlignStack,
                             fromRust(Dialect), CanThrow));
}

以下のコメントにあるように、LLVMはmemory constraintを解釈しない。

        if !options.contains(InlineAsmOptions::NOMEM) {
            // This is actually ignored by LLVM, but it's probably best to keep
            // it just in case. LLVM instead uses the ReadOnly/ReadNone
            // attributes on the call instruction to optimize.
            constraints.push("~{memory}".to_string());
        }

NOMEMが指定された場合どうするかというと、関数属性に渡すMemoryEffectsが変わっている。LLVMはこの関数属性をみて最適化可能性を判断する。(Rustのasm!オプションPURE=volatile相当とかは別口だけど)

        let mut attrs = SmallVec::<[_; 2]>::new();
        if options.contains(InlineAsmOptions::PURE) {
            if options.contains(InlineAsmOptions::NOMEM) {
                attrs.push(llvm::MemoryEffects::None.create_attr(self.cx.llcx));
            } else if options.contains(InlineAsmOptions::READONLY) {
                attrs.push(llvm::MemoryEffects::ReadOnly.create_attr(self.cx.llcx));
            }
            attrs.push(llvm::AttributeKind::WillReturn.create_attr(self.cx.llcx));
        } else if options.contains(InlineAsmOptions::NOMEM) {
            attrs.push(llvm::MemoryEffects::InaccessibleMemOnly.create_attr(self.cx.llcx));
        } else {
            // LLVM doesn't have an attribute to represent ReadOnly + SideEffect
        }
        attributes::apply_to_callsite(result, llvm::AttributePlace::Function, &{ attrs });

以下のコードはC言語のinline assemblyとLLVMの関数属性の対応でいうと、

  • volatileなし + memory clobberなし: READ_NONE + WILL_RETURN
  • volatileなし + memory clobberあり
    • RustのREADONLYオプションあり: READ_ONLY + WILL_RETURN
    • RustのREADONLYオプションなし: WILL_RETURN
  • volatileあり + memory clobberなし: InAccessibleMemOnly
  • volatileあり + memory clobberあり: ReadWriteMemory (デフォルト)

と、実に細かく設定できる。

Rustはclangと違い、デフォルトでは IsAlignStack=true に設定している。

https://github.com/rust-lang/rust/blob/558ac1cfb7c214d06ca471885a57caa6c8301bae/compiler/rustc_codegen_llvm/src/asm.rs#L275

        let alignstack = !options.contains(InlineAsmOptions::NOSTACK);

ここの設計判断はこのコメントが詳しく解説している。

また、他のassembly構文にない特徴として preserves_flags というオプションを渡すことができる。Rustはデフォルトで各アーキテクチャのflagレジスタをconstraintsに追加するようになっており、このオプションを追加することでその機能を無効にできる。

総論として、Rustはasm!構文でC言語とは異なる構文を設定しておりデフォルトで安全側に倒すような設計としつつ、最適化について細かく制御可能となっている。

LDC

https://wiki.dlang.org/LDC_inline_assembly_expressions

LDCは常に sideeffect = true を渡しているのでいわゆるvolatile相当が常に付与されている状態となっている。

  // build asm call
  bool sideeffect = true;
  llvm::InlineAsm *ia = llvm::InlineAsm::get(FT, code, constraints, sideeffect);

また、LDCは関数属性をデフォルトからいじるようなことはしていないので、memory clobberをつけなくても同様の効果(ReadWriteMemory属性)にはなる(clangのように自前でパースしてどうこう、というようなことはやらない)。

安全側に倒した実装になっているが、最適化を細かくやる余地は残されていない。

まとめ

いかがでしたか? 個人的には、LLVMインラインアセンブラ実装けっこう思ってたんとちがう!って感じで調べてておもしろかったです。