コンテンツへスキップ
GitHubリポジトリ フォーラム RSS-ニュースフィード

LLVM不透明ポインタサポートが実装されました

HertzDevil

間もなくリリースされるCrystal 1.8では、初めてLLVMの不透明ポインタがサポートされます。これにより、コンパイラをLLVM 15以上でビルドできるようになります。さらに、このアップデートによりコンパイル時間が大幅に向上します。

LLVMにおけるポインタ

不透明ポインタの重要性を理解するために、小さなサンプルプログラムを見てみましょう。

# test.cr
class Foo
  def initialize(@x : Int32)
  end
end

Foo.new(1)

上記のプログラムをcrystal build --prelude=empty --no-debug --emit=llvm-ir test.crでビルドします。コンパイラはtest.llというファイルを作成し、プログラムのLLVM IR(LLVMがLLVMバイトコード、最終的にはマシンコードを生成するために使用するプラットフォームに依存しない中間表現)を含みます。Foo#initializeに対応するLLVM関数は次のとおりです。

; Function Attrs: uwtable
define internal i32 @"*Foo#initialize<Int32>:Int32"(%Foo* %self, i32 %x) #0 {
entry:
  %0 = getelementptr inbounds %Foo, %Foo* %self, i32 0, i32 1
  store i32 %x, i32* %0, align 4
  ret i32 %x
}

CrystalがこのメソッドをどのようにLLVM関数にコンパイルするかについての詳細はここでは説明しませんが(過去にこれについて解説がありました)、次のことが分かります。

  • %selfパラメータ(構築中のFooオブジェクト)、およびFoo#initializeからの%xパラメータを引数として受け取ります。
  • Fooオブジェクトの@xインスタンス変数のアドレスをローカル変数%0に代入します。
  • %0を介して@x%xパラメータを格納します。
  • 呼び出し元に%xを返します。(この呼び出し元は通常Foo.newなので、ほとんどの場合使用されません。)

%self%0の型はそれぞれ%Foo*i32*です。これらは型付きポインタであり、LLVMは長い間使用してきました。LLVMでは、異なるポインティ型の型付きポインタは、bitcast LLVM命令を使用して明示的に変換する必要があります。そうでなければ、結果のLLVM IRは不正になります。LLVMフロントエンドが増えるにつれて、型付きポインタは多くの有用なセマンティクスを提供せず、代わりにIR生成と分析に不要な複雑さを加えていることがすぐに分かりました。

不透明ポインタ

不透明ポインタは、2015年2月に最初に提案され、すべてのポインタ型がLLVM IRで単一のptrで表されるようになりました。その後、2022年9月、LLVM 15はデフォルトで不透明ポインタを使用するようになり、型付きポインタはすぐに削除される予定です。上記のプログラムをもう一度コンパイルしますが、今回はLLVM 15を使用してビルドされたCrystalコンパイラを使用すると、不透明ポインタが動作しているのがわかります。

; Function Attrs: uwtable(sync)
define internal i32 @"*Foo#initialize<Int32>:Int32"(ptr %self, i32 %x) #0 {
entry:
  %0 = getelementptr inbounds %Foo, ptr %self, i32 0, i32 1
  store i32 %x, ptr %0, align 4
  ret i32 %x
}

そのためには、型付きポインタを使用するかどうかによって、いくつかのLLVM命令を異なる方法で構築する必要があり、getelementptr命令はその一例です。オブジェクト型は、指定されたポインタから推論されていましたが、不透明ポインタはこの情報を保持しないため、IRジェネレータ(Crystalコンパイラ)はこのオブジェクト型を別途提供する必要があります。この場合、@xインスタンス変数と#initializeメソッドはどちらもFooに属しているので、CrystalはFoogetelementptrに渡すことを知っています。しかし、この命令はコンパイラの他の数十カ所でも使用されており、普遍的な移行は適用できません。

Crystalコンパイラのアップデート

不透明ポインタへの移行作業は、LLVM 15のリリースから1か月後の2022年10月に開始され、数え切れないほどのセグメンテーション違反と仕様違反の後、CrystalはmasterブランチでLLVM 15をサポートするようになりました。これは非常に大規模な作業であったため、LLVMが約束するパフォーマンス上のメリットを不透明ポインタがもたらすことを期待しています。そこで、Apple M2でコンパイラ自体を再構築した際に収集された数値を以下に示します。最初はLLVM 14コンパイラを使用し、次にLLVM 15コンパイラを使用しました。

  • リリースではないビルド
    • コード生成(crystal):2.65秒 → 2.84秒
    • コード生成(bc+obj):5.91秒 → 5.20秒
    • コード生成(リンク):0.76秒 → 0.34秒
    • dsymutil:0.35秒 → 0.39秒
  • リリースビルド
    • コード生成(bc+obj):247.86秒 → 184.37秒
    • コード生成(リンク):0.45秒 → 0.33秒
    • dsymutil:0.63秒 → 0.52秒

LLVMの制御下にある最後の3つのステージだけを考慮すると、リリースではないビルドでは18%、リリースビルドでは34%の高速化が実現されています!CrystalをLLVM 15で再構築することに熱心な開発者からも同様の数値が報告されています。Crystal自体と同じくらい大きなプログラムのLLVM IRを生成するのに平均0.2秒長くかかるものの、LLVMの改善はそれをはるかに上回っています。この移行の努力は確かに報われました。

これは私にどのような影響を与えますか?

CrystalのナイトリービルドはすでにLLVM 15でビルドされたコンパイラを使用しており、試すことができます。1.8はLLVM 15でビルドされた最初の安定版リリースになります。これらのコンパイラは不透明ポインタを使用しており、コード生成時間の改善を示しています。LLVM 14以下のコンパイラは引き続き型付きポインタを使用します。

CrystalプロジェクトでstdlibのLLVM APIを直接使用している場合、いくつかの非推奨事項に注意する必要があります。それ以外の場合は、この変更はCrystalプログラムに何らかの影響を与えることはありません。コンパイラが高速化されるだけです。

一方、Crystalを使用してCrystalのLLVM APIを使用して他のLLVMフロントエンドをビルドしている場合は、不透明ポインタの影響を受ける命令(上記のgetelementptrなど)に対してLLVMが別の型を受け入れる最初のバージョンである8.0をきっかけに、Crystalは8.0未満のLLVMバージョンをサポートしなくなります。型付きポインタに依存するすべての機能は非推奨です。LLVM 15が実際に使用されているかどうかに関係なく、影響を受けるメソッドの完全なリストを以下に示します。

  • LLVM::Type#element_type:LLVM 15以上では、ポインタ型でこのメソッドを呼び出すと例外が発生します。
  • LLVM::Function#function_type#return_type#varargs? これらのメソッドには迅速な移行方法がありません。ただし、関数がLLVM::FunctionCollection#addを介して構築された場合、そのメソッドにはLLVM関数型を直接受け入れる追加のオーバーロードがあります。これにより、LLVM::Type.functionを使用し、関数を構築する前にその型を他の場所に格納することができます。
  • LLVM::Builder#call(func : LLVM::Function, ...)#invoke(fn : LLVM::Function, ...) これらはそれぞれcall(func.function_type, func, ...)およびinvoke(fn.function_type, fn, ...)と同等です。#function_typeは非推奨であり、LLVM 15以上では機能しないことに注意してください。
  • LLVM::Builder#load(ptr, ...)load(ptr.type.element_type, ptr, ...) と同等です。#element_type はLLVM 15以降ではエラーを発生させます。
  • LLVM::Builder#gep(value, ...)LLVM::Builder#inbounds_gep(value, ...) はそれぞれ gep(value.type, value, ...)inbounds_gep(value.type, value, ...) と同等です。#type はLLVM 15以降では単なる不透明ポインタ型です。

さらに、LLVM 15は型付きポインタサポートを有効にするためのオプションフラグを提供していますが、Crystalはこれを全く使用しません。これにより、LLVM 16以降へのアップグレードが容易になります。LLVMは最終的にこのフラグを削除する予定です。