FaultMapsと暗黙的なチェック

動機

マネージド言語ランタイムによって生成されたコードは、安全のために必要なチェックを含んでいることがありますが、実際には失敗することはありません。そのような場合、失敗した場合に大幅にコストが高くなるとしても、失敗しない場合のコストを低くすることが有利です。この非対称性は、そのような安全チェックを、チェックが失敗した場合に確実にフォルトが発生するようにできる操作に折り畳み、シグナルハンドラーを使用してそのようなフォルトから回復することによって活用できます。

たとえば、Javaでは、オブジェクトから読み書きする前に、オブジェクトのヌルチェックが必要です。オブジェクトがnullの場合、NullPointerExceptionがスローされ、通常の処理が中断されます。しかし実際には、動作の良いJavaプログラムでは、nullポインタの逆参照は非常にまれであり、通常、ヌルチェックは同じメモリ位置を操作する近くのメモリ操作に折り畳むことができます。

Fault Mapセクション

LLVMによって生成された暗黙的なチェックに関する情報は、特別な「フォルトマップ」セクションに格納されます。Darwinでは、このセクションの名前は__llvm_faultmapsです。

このセクションの形式は次のとおりです。

Header {
  uint8  : Fault Map Version (current version is 1)
  uint8  : Reserved (expected to be 0)
  uint16 : Reserved (expected to be 0)
}
uint32 : NumFunctions
FunctionInfo[NumFunctions] {
  uint64 : FunctionAddress
  uint32 : NumFaultingPCs
  uint32 : Reserved (expected to be 0)
  FunctionFaultInfo[NumFaultingPCs] {
    uint32  : FaultKind
    uint32  : FaultingPCOffset
    uint32  : HandlerPCOffset
  }
}

FailtKindは、予期されるフォルトの理由を記述します。現在、3種類のフォルトがサポートされています。

  1. FaultMaps::FaultingLoad - メモリからのロードによるフォルト。

  2. FaultMaps::FaultingLoadStore - 命令のロードとストアによるフォルト。

  3. FaultMaps::FaultingStore - メモリへのストアによるフォルト。

ImplicitNullChecksパス

ImplicitNullChecksパスは、ポインタがnullかどうかをチェックするための明示的な制御フロー(例:以下)を、

  %ptr = call i32* @get_ptr()
  %ptr_is_null = icmp i32* %ptr, null
  br i1 %ptr_is_null, label %is_null, label %not_null, !make.implicit !0

not_null:
  %t = load i32, i32* %ptr
  br label %do_something_with_t

is_null:
  call void @HFC()
  unreachable

!0 = !{}

ヌルチェックされているポインタを介したロードまたはストア命令に暗黙的に制御フローを変換します。

  %ptr = call i32* @get_ptr()
  %t = load i32, i32* %ptr  ;; handler-pc = label %is_null
  br label %do_something_with_t

is_null:
  call void @HFC()
  unreachable

この変換は、LLVM IRレベルではなくMachineInstrレベルで行われます(したがって、上記の例は代表的なものであり、文字通りのものではありません)。ImplicitNullChecksパスは、-enable-implicit-null-checksllcに渡された場合、コード生成中に実行されます。

ImplicitNullChecksパスは、必要に応じて上記で説明した__llvm_faultmapsセクションにエントリを追加します。

make.implicitメタデータ

ヌルチェックを暗黙的にすることは積極的な最適化であり、多くのメモリ操作がそのためにフォルトを起こしてしまうと、パフォーマンスの低下につながる可能性があります。言語ランタイムは、アプリケーションが定常状態に達した後、無視できる数の暗黙的なヌルチェックだけが実際にフォルトを起こすようにする必要があります。これを行う標準的な方法は、コードパッチまたは再コンパイルを介して、失敗した暗黙的なヌルチェックを明示的なヌルチェックに修復することです。したがって、明示的なヌルチェックを暗黙的なヌルチェックに変換することが有利であるためには、2つの要件を満たす必要があります。

  1. ポインタが実際にnullである場合(つまり、「失敗」の場合)は非常にまれです。

  2. 失敗したパスは、アプリケーションが繰り返しページフォルトを起こさないように、暗黙的なヌルチェックを明示的なヌルチェックに修復します。

フロントエンドは、(1)と(2)を満たすブランチを!make.implicitメタデータノードを使用してマークする必要があります(メタデータノードの実際のコンテンツは無視されます)。!make.implicitメタデータでマークされたブランチのみが、暗黙的なヌルチェックへの変換の候補とみなされます。

(プロファイリングデータを使用して(1)に対処することはできますが、(2)に対処するには、ブランチプロファイルには存在しない情報が必要です。)