「人生は短い、Python が必要だ」!
古いプログラマーは Python の優雅さを愛しています。しかし、運用環境では、パフォーマンスを最適化するように構築されていない Python のような動的言語機能は危険である可能性があります。そのため、主に TensorFlow や PyTorch などの一般的な高パフォーマンス ライブラリが使用されます。最適化された C/C ライブラリと対話するためのインターフェイス言語として Python を使用します。
Python プログラムのパフォーマンスを最適化する方法はたくさんあります。コンパイラの観点から見ると、高いパフォーマンスを C や C などの下位レベルの静的分析可能な言語に埋め込み、ランタイムにコンパイルすることができます。オーバーヘッド ネイティブ マシン コードにより、C/C と同等のパフォーマンスが得られます。
Codon は、事前コンパイル、特殊な双方向型チェック、および言語の構文のオプションの特異性とコンパイラの最適化を可能にする新しい双方向中間表現を使用するコンパイラとして見ることができます。これにより、プロのプログラマーは、直感的で高レベルの使い慣れた方法で高性能のコードを作成できます。
他のパフォーマンス指向の Python 実装 (PyPy や Numba など) とは異なり、Codon はスタンドアロン システムとしてゼロから構築され、既存の Python ランタイムを必要とせず、事前に静的実行可能ファイルにコンパイルされます。 (例: CPython)。したがって、原則として、Codon はより優れたパフォーマンスを実現し、グローバル インタープリター ロックなどの Python ランタイム固有の問題を克服できます。実際には、Codon は Python スクリプト (C コンパイラーなど) をネイティブ コードにコンパイルし、解釈された実行より 10 ~ 100 倍高速に実行します。
コドンは、バイオインフォマティクス DSL である Seq 言語に基づいてモデル化されています。 Seq はもともと、導入の容易さ、優れたパフォーマンス、強力な表現力など、多くの利点を備えたピラミッド スタイルの DSL として設計されました。ただし、厳密な型規則のため、Seq は多くの一般的な Python 言語構造をサポートしておらず、新しいコンパイラの最適化を簡単に実装するメカニズムも提供していません。双方向 IR と改良された型チェッカーを適用することにより、Codon は Seq.1 に基づいてこれらの問題に対する一般的な解決策を提供します。
Codon は Python のほとんどの機能をカバーし、特定の領域で最適化するためのフレームワークを提供します。さらに、さまざまな言語機能をより適切に処理するために、柔軟な型システムが提供されています。型システムは、RPython や PyPy、静的型システムに似ています。これらのアイデアは、PRuby などの他の動的言語のコンテキストにも適用されています。双方向 IR で使用されるアプローチは、Java の検査フレームワークなどのフォワード プラガブル型システムと類似しています。
Codon の Intermediate Expression は最初のカスタマイズ可能な IR ではありませんが、すべてのコンテンツのカスタマイズをサポートするわけではなく、より複雑な特性を実現するために双方向性と組み合わせることができる、シンプルで明確に定義されたカスタマイズを選択します。構造の点では、CIR は LLVM と Rust の IR からインスピレーションを得ています。これらの IR は、大幅に簡素化されたノード セットと構造の恩恵を受け、結果として IR チャネルの実装が簡素化されます。ただし、構造的には、これらの実装はソース コードを根本的に再構築し、変換を実行するためにリファクタリングする必要があるセマンティック情報を排除します。この欠点に対処するために、Taichi は、複雑さの増加を犠牲にして制御フローとセマンティック情報を維持する階層構造を採用しています。ただし、Codon とは異なり、これらの IR は言語のフロントエンドにほとんど依存しないため、型の正確性を維持したり、新しいコードを生成したりすることが多少非現実的か、不可能になります。したがって、CIR はこれらのメソッドの単純化された階層を活用し、ソース コードの制御フロー ノードと完全に削減された内部ノードのサブセットを維持します。重要なのは、この構造を双方向性で強化し、新しい IR の生成と操作を容易にすることです。
Codon は静的型チェックを利用し、実行時の型情報を使用せずに LLVM IR にコンパイルします。これは、Python などの動的言語のコンテキストでのエンドツーエンドの型チェックに関する以前の作業に似ています。このため、Codon には、LTS-DI と呼ばれる静的双方向型システムが付属しています。このシステムは、HindleyMilner スタイルの推論を利用して、ユーザーが手動で型に注釈を付ける必要なく、プログラム内の型を推論します (この手法はサポートされていますが、サポートされていません)。 Python でサポートされています (開発者の間では一般的ではありません)。
Python の構文と一般的な Python のイディオムの性質により、LTS-DI は標準的な hm のような推論を採用して、内包表記、反復子、ジェネレーター、複雑な関数操作、変数パラメーター、静的型チェックなどの注目すべき Python 構造をサポートします。 、など。これらおよび他の多くの構造を処理するために、LTS-DI は以下に依存します。
多くの Python 構造では、コドンによってサポートされるコンパイル時の式 (C のパックされた pr 式と同様) も必要です。これらのアプローチは実際には珍しいことではありませんが (たとえば、C のテンプレートはシングルトンを使用します)、遅延インスタンス化は HMF 型システムですでに使用されていますが、型チェックされた Python プログラムのコンテキストでこれらを組み合わせて使用することは認識されていません。最後に、現在の実装における Codon の型システムは完全に静的であり、実行時の型推論は実行されないため、実行時ポリモーフィズムや実行時リフレクションなどの一部の Python 機能はサポートされていないことに注意してください。科学技術コンピューティングの文脈では、これらの機能を削除すると、実用性とパフォーマンスの間の合理的なトレードオフが得られることがわかっています。
多くの言語は比較的単純な方法でコンパイルします: ソース コードは通常、次のようなフレームワークの助けを借りて、抽象構文ツリー (AST) に解析されます。 LLVM、最適化され、マシンコードに変換されます。このアプローチは実装が比較的簡単ですが、多くの場合、AST には特定のプログラムを表すのに必要なノード タイプよりも多くのノード タイプが含まれます。この複雑さにより、最適化、変換、分析の実装が困難になったり、非現実的になる場合があります。もう 1 つのアプローチは、最適化パスを実行する前に AST を中間表現 (IR) に変換することです。中間表現には通常、明確に定義されたセマンティクスを持つ単純化されたノードのセットが含まれており、変換と最適化がより容易になります。
Codon は、上の図に示すように、型チェック フェーズと最適化フェーズの間にある IR でこのアプローチを実装します。コドンの中間表現 (CIR) は AST よりもはるかに単純で、構造が単純でノード タイプが少なくなっています。その単純さにもかかわらず、Codon の中間表現はソース コードの意味論的な情報の多くを維持し、「漸進的削減」を容易にし、複数の抽象化レベルでの最適化を可能にします。
CIR は、部分的に LLVM の IR からインスピレーションを受けています。 LLVM では、シングル スタティック アロケーション (SSA) の形式に似た構造が採用されており、概念的にはメモリの場所に似た場所に割り当てられた値と変数を区別し、コンパイルは最初に線形に進行します。抽象構文ツリーに解析され、型チェックが実行されて中間式が生成されます。ただし、他のコンパイル フレームワークとは異なり、Codon は双方向であり、IR 最適化は型チェック段階に戻って、元のプログラムにはなかった新しいノードを生成できます。このフレームワークは「ドメイン拡張可能」であり、「DSL プラグイン」はライブラリ モジュール、構文、およびドメイン固有の最適化で構成されます。
ソース コード構造のマッピングを実現するために、値を任意の大きなツリーにネストできます。この構造により、CIR を制御フロー グラフなどに簡単に変換できます。ただし、LLVM とは異なり、CIR は当初、ストリームと呼ばれる明示的なノードを使用して制御フローを表現し、ソース コードと構造的に厳密に対応させることができました。制御フロー階層を明示的に表現することは、Taichi が採用したアプローチと似ています。重要なのは、これにより、制御フローの正確な概念に依存する最適化と変換の実装が容易になることです。簡単な例はストリームです。ストリームは、LLVM IR のように分岐の迷路を解読するのではなく、CIR で明示的なループを保持し、コドンがループの一般的なパターンを簡単に識別できるようにします。
CIR は " " のような演算子を明示的に表現しませんが、それらを対応する関数呼び出しに変換します。これにより、Python と同じセマンティクスで、あらゆるタイプのシームレスな演算子のオーバーロードが可能になります。たとえば、オペレーターは追加呼び出しを解決します。
このアプローチから生じる当然の疑問は、int や浮動小数点数などのプリミティブ型の演算子をどのように実装するかということです。 Codon は、@llvm 関数アノテーションを介してインライン LLVM IR を許可することでこの問題を解決し、これによりすべてのプリミティブ演算子をコドン ソース コードに記述できるようになります。可換性や結合性などの演算子のプロパティに関する情報は、IR の注釈として渡すことができます。
従来のコンパイル パイプラインは、データ フローが直線的です。ソース コードは AST に解析され、通常は IR に変換され、最適化されて、最終的にマシン コードに変換されます。 Codon は、双方向 IR の概念を導入しました。この概念では、IR チャネルが型チェック段階に戻り、新しい IR ノードとソース プログラムに存在しない特殊なノードを生成できます。利点は次のとおりです。
同様に、IR チャネル自体は、Codon の式型システムを使用してさまざまな型を操作する汎用的なものにすることができます。 IR 型には、関連付けられたジェネリックスがありません (AST 型とは異なります)。ただし、各 CIR 型には、その生成に使用された AST 型への参照と、AST ジェネリック型パラメーターが含まれています。これらの関連付けられた AST 型は、型チェッカーを再起動するときに使用され、CIR 型の基になるジェネリックをクエリできるようになります。 CIR タイプは高レベルの抽象化に対応し、LLVM IR タイプは下位であり、コドン タイプに直接マッピングされないことに注意してください。
実際、CIR 配信中に新しい型をインスタンス化できる機能は、多くの CIR 操作にとって重要です。たとえば、指定された CIR 値 x および y からタプル (x,y) を作成するには、新しいタプル タイプ tuple[X,Y] (大文字の識別子が式のタイプ) をインスタンス化する必要があり、これには Instantiate new が必要です。等価および不等価のチェック、反復、ハッシュなどのためのタプル演算子。ただし、型チェッカーをコールバックすると、これがシームレスなプロセスになります。
上の図は、フィボナッチ関数を CIR ソース コードにマッピングする簡単な例です。関数 fib は、単一の整数引数を使用して CIR BodiedFunc にマップされます。本体には、定数を返すか、関数を再帰的に呼び出して結果を取得する If 制御フローが含まれています。このような演算子は関数呼び出し (add など) に変換されますが、IR はその構造内で元のソース コードにマップされ、単純なパターン マッチングと変換が可能であることに注意してください。この場合、単純に Call ハンドラーをオーバーロードし、関数が置換の条件を満たしているかどうかを確認し、一致する場合はアクションを実行します。ユーザーは独自のトラバーサル スキームを定義し、IR 構造を自由に変更することもできます。
CIR は、包括的な分析および変換インフラストラクチャを提供します。ユーザーは、さまざまな CIR 組み込みアプリケーション クラスを使用してパスを作成し、パスワード マネージャーに登録します。より複雑なチャネルは、 CIR の双方向性の利点を活用し、新しい CIR タイプ、関数、およびメソッドのタイプ チェッカーを再呼び出しします。その例を以下の図に示します。
この例では、関数 foo の呼び出しを検索し、各呼び出しの後に foo とその出力呼び出しを検証するパラメーターを挿入します。どちらの関数もジェネリックであるため、型チェッカーが再度呼び出され、3 つの新しい一意の検証インスタンスが生成されます。新しい型と関数をインスタンス化するには、可能な特殊化を処理し、追加のノードを実装する必要があります (たとえば、検証を実装するには例に == 演算子メソッド __eq__ を実装する必要があります)。また、後で使用できるように実装をキャッシュする必要があります。
Codon は LLVM を使用してネイティブ コードを生成します。コドン IR から LLVM IR への変換は、通常は簡単なプロセスです。ほとんどのコドン型は、直観的に LLVM IR 型に変換することもできます。つまり、int は i64、float は double、bool は int8 などになります。これらの変換により、C/C の相互運用性も可能になります。タプル型は、値によって渡される適切な要素型を含む構造体型に変換されます (Python ではタプルは不変であることに注意してください); このタプルの処理方法により、ほとんどの場合、LLVM を完全に最適化できます。リスト、Dict などの参照型は、動的に割り当てられたオブジェクトとして実装され、参照によって渡されます。これらは Python の変数セマンティック型に従い、値を処理しないように必要に応じてオプションの型にアップグレードできます。オプションの型は次のように実装されます。 LLVM の i1 型と基礎となる型のタプル。前者はオプションの型に値が含まれるかどうかを示します。参照型のオプションは、欠損値を示すために null ポインターを使用するように特別に設計されています。
ジェネレーターは Python でよく使われる言語構造で、実際、すべての for ループがジェネレーターを反復処理します。重要なのは、Codon のジェネレーターには追加のオーバーヘッドがなく、可能な限り同等の標準 C コードにコンパイルされることです。この目的のために、Codon は LLVM コルーチンを使用してジェネレーターを実装します。
Codon は、コードを実行するときに小さなランタイム ライブラリを使用します。特に、Boehm ガベージ コレクターは、割り当てられたメモリを管理するために使用されます。 Codon には、デバッグとリリースという 2 つのコンパイル モードが用意されています。デバッグ モードには完全なデバッグ情報が含まれており、GDB や LLDB などのツールを使用してプログラムをデバッグできるほか、ファイル名や行番号などの完全なトレースバック情報も含まれています。リリース モードでは、さらに多くの最適化 (GCC/Clang からの -O3 最適化を含む) が実行され、一部のセキュリティおよびデバッグ情報が省略されます。したがって、ユーザーはデバッグ モードを使用して高速なプログラミングとデバッグ サイクルを実現し、リリース モードを使用して高パフォーマンスの展開を実現できます。
フレームワークの柔軟性と双方向 IR、および Python 構文の全体的な表現力により、Codon アプリケーションは通常、ドメイン固有のコンポーネントの機能のほとんどをソース コードに実装します。それ自体。モジュール式のアプローチは、ダイナミック ライブラリおよび Codon ソース ファイルとしてパッケージ化できます。このプラグインは、コンパイル時にコドン コンパイラーによってロードできます。
MLIR などの一部のフレームワークではカスタマイズが可能です。一方、Condon IR は一部のタイプのノードを制限し、さらなる柔軟性のために双方向性に依存しています。特に、CIR を使用すると、ユーザーは宣言型インターフェイスを通じてフレームワークの残りの部分と対話する「カスタム」型、ストリーム、定数、およびディレクティブから派生することができます。たとえば、カスタム ノードは適切なカスタム基本クラス (カスタム タイプ、カスタム ストリームなど) から取得され、対応する LLVM IR を構築するための「ビルダー」を公開します。カスタム タイプとノードの実装には、仮想メソッドによるジェネレーター (タイプの構築など) の定義が含まれます。カスタム タイプ クラス自体は、このジェネレーターのインスタンスを取得するメソッド getBuilder を定義します。この標準化されたノードの構築は、既存のチャネルや分析とシームレスに連携します。
多くの標準的な Python プログラムはすぐに使用できるため、辞書の更新など、Python コードのいくつかの一般的なパターンを簡単に最適化できます。 (2 つではなく 1 つの検索を使用するように最適化できます)、または連続した文字列の追加 (割り当てのオーバーヘッドを削減するために 1 つの結合に折りたたむことができます)。
上のグラフは、ベンチマーク テストにおける Codon の実行時のパフォーマンスと、CPython (v3.10) および PyPy (v7.3) のパフォーマンスを示しています。制限は、外部ライブラリに依存しない「コア」ベンチマークの 1 セットです。 CPython や PyPy と比較すると、Codon は常に高速であり、場合によっては桁違いに高速です。ベンチマークはパフォーマンスの優れた指標ですが、欠点がないわけではなく、多くの場合、全体像を伝えられません。 Codon を使用すると、ユーザーはさまざまなドメイン向けの単純な Python コードを記述しながら、実際のアプリケーションやデータ セットで高いパフォーマンスを実現できます。
Codon は既存の Python ランタイムとは独立して構築されているため、CPython グローバル インタープリター ロックの影響を受けず、マルチスレッドを最大限に活用できます。並列プログラミングをサポートするために、Codon の拡張機能により、エンド ユーザーは OpenMP を使用できるようになります。
OpenMP の場合、並列ループの本体は新しい関数として概説され、OpenMP ランタイムによって複数のスレッドによって呼び出されます。たとえば、以下の図のループの本体は、パラメータとして変数 a、b、c およびループ変数 i を取る関数 f として概説されます。
f への呼び出しは、ブロック サイズ 10 の OpenMP の動的ラウンドロビン スケジューリング ルーチンを呼び出す新しい関数 g に挿入されます。最後に、キュー内のすべてのスレッドが OpenMP の fork_call 関数を通じて g を呼び出します。結果は上の画像の正しいコード スニペットに示されており、共有変数だけでなくプライベート変数の処理にも特別な注意が払われています。変数を削減するには、アトミック操作 (またはロックの使用) のための追加のコード生成と、OpenMP API 呼び出しの追加レイヤーも必要です。
Codon の双方向コンパイルは、OpenMP パスの重要なコンポーネントです。さまざまなループの「テンプレート」は Codon ソース コードに実装されています。コード分析後、これらの「テンプレート」は渡され、ループ本体、ブロック サイズ、スケジュールを埋めたり、共有変数に依存する式を書き換えたりすることで特殊化されます。この設計により、パスの実装が大幅に簡素化され、ある程度の汎用性が追加されます。
Clang や GCC とは異なり、Codon の OpenMP チャネルは、どの変数が共有され、どの変数がプライベートであるか、また、行われている削減のコードを推定できます。カスタム リダクションは、リダクション タイプに適切なアトミック マジック メソッド (.aborom_add など) を提供するだけで簡単に作成できます。 Codon は、ジェネレーター (Python ループのデフォルト動作) を介して「命令ループ」、つまり開始値、停止値、ステップ値を持つ C スタイルのループを繰り返します。 @par タグが存在する場合、強制ループは OpenMP 並列ループに変換されます。非強制並列ループは、ループ反復ごとに新しい OpenMP タスクを生成し、ループの後に同期ポイントを配置することによって並列化されます。このスキームにより、すべての Python for ループを並列化できます。
OpenMP 変換は、@par 属性でマークされた for ループと一致する CIR のセットとして実装され、これらのループは CIR 内の適切な OpenMP 構造に変換されます。ほとんどすべての OpenMP 構造は、Condon 自体の高次関数として実装されます。
CoLa は、主にブロックベースのデータ圧縮をターゲットとしたコドンベースの DSL であり、現在多くの一般的な画像やビデオで使用されています。圧縮アルゴリズムのこと。これらのタイプの圧縮は、ピクセル領域を一連のより小さなブロックに分割し、各ブロックが他のブロックに対する相対的な位置を認識する必要がある多次元データ階層を形成することに大きく依存しています。たとえば、H.264 ビデオ圧縮では、入力フレームを一連の 16x16 ピクセル ブロックに分割し、各ピクセルを 8x8 ピクセル ブロックに分割してから、それらのピクセルを 4x4 ピクセル ブロックに分割します。これらのピクセルの個々のパッチ間の位置を追跡するには、大量の情報データが必要ですが、既存の実装の基礎となるアルゴリズムがすぐにわかりにくくなります。
#CoLa では、階層型多次元配列 (HMDA) 抽象化を導入し、階層データの表現と使用を簡素化します。 HMDA は位置の概念を備えた多次元配列を表し、グローバル座標系を基準にして任意の HMDA の原点を追跡します。 HMDA は、サイズと歩幅を追跡することもできます。これら 3 つのデータを使用すると、どの HMDA もプログラム内の任意の時点で他の HMDA に対する相対的な位置を決定できます。 CoLa は、ブロックとビューという 2 つの新しいデータ型を中心としたライブラリとして Codon の HMDA を抽象化します。ブロックは基礎となる多次元配列を作成して所有し、ビューはブロックの特定の領域を指します。 CoLa は、構築操作、位置コピー、およびパーティショニングという 2 つの主要な階層を公開しており、それぞれブロックとビューを作成します。 CoLa は、整数インデックスとスライス インデックスを使用した標準インデックス作成をサポートしていますが、圧縮標準がデータ アクセスを記述する方法をエミュレートする 2 つの独自のインデックス作成スキームも導入しています。 「範囲外」インデックスを使用すると、ユーザーはビューの周囲のデータにアクセスできます。一方、「管理」インデックスを使用すると、ユーザーは別の HMDA を使用して 1 つの HMDA にインデックスを付けることができます。
Codon の物理的プロパティと CoLa の抽象化の組み合わせにより、高級言語と圧縮固有の抽象化の利点がユーザーに提供されますが、HMDA 抽象化では追加のインデックス作成操作が必要となるため、実行時に大幅なオーバーヘッドが生じます。圧縮の場合、多くの HMDA アクセスは計算の最も内側のレベルで発生するため、元の配列へのアクセスに加えて追加の計算が実行されると、ランタイムに悪影響を及ぼすことがわかります。 CoLa は Codon フレームワークを利用して階層を実装し、作成される中間ビューの数を減らし、特定の HMDA の位置を推論する伝播試行を削減します。これにより、階層全体のサイズが削減され、実際のインデックスの計算が簡素化されます。これらの最適化を行わないと、CoLa は JPEG および H.264 の基準 C コードより平均して 48.8 倍、6.7 倍、20.5 倍遅くなります。最適化後、パフォーマンスは大幅に向上し、同じリファレンス コードと比較して平均実行時間はそれぞれ 1.06 倍、0.67 倍、0.91 倍になりました。
CoLa は Codon プラグインとして実装されており、圧縮プリミティブのライブラリに加えて、作成およびアクセス ルーチンを最適化する CIR および LLVM チャネルのセットが付属しています。 CoLa は、Codon が提供するカスタム データ構造アクセス構文と演算子を使用して、一般的なインデックス作成とリダクション操作も簡素化します。
本質的に、コドンは、DSL を設計して迅速に実装するためのドメイン構成可能なフレームワークです。特殊な型チェック アルゴリズムと新しい双方向 IR アルゴリズムを適用することで、さまざまな領域の動的コードを簡単に最適化できます。 Python を直接使用する場合と比較して、Codon は高レベルの単純さを犠牲にすることなく C/C++ のパフォーマンスに匹敵します。
現在、Codon にはサポートされていない Python 機能がいくつかあります。主にランタイムポリモーフィズム、ランタイムリフレクション、型操作 (動的メソッドテーブル変更、クラスメンバー、メタクラス、クラスデコレータの動的追加など) が含まれます。標準の Python ライブラリの範囲にもギャップがあります。 Codon でコンパイルされた Python は制限的なソリューションとして存在しますが、注意を払う価値があります。
[参考資料と関連書籍]
以上がコンパイラの観点から見た Python パフォーマンスの最適化の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。