こんにちはHabr。 私は比較的最近私の最後の投稿を公開したので、あなたが私の名前がマルコであることを忘れる時間があったとは考えにくい。 今日、私は小さなノートの翻訳を公開しています。これは、まだリリースされていないGo 1.9の非常においしい最適化に関するものです。 これらの最適化により、ほとんどのGoプログラムで生成されるジャンクを減らすことができます。 ガーベッジの削減-ガーベッジの収集にかかる遅延とコストを削減
この記事は、Go 1.9のリリースに向けて準備されている新しいコンパイラーの最適化に関するものですが、ロギングから会話を始めたいと思います。
数週間前、Peter Burgonはgolang-devでロギングを標準化するという提案でスレッドを開始しました。 どこでも使用されているため、パフォーマンスの問題は非常に深刻です。 go-kitパッケージは、次のインターフェイスに基づく構造ログを使用します。
type Logger interface { Log(keyvals ...interface{}) error }
呼び出し例:
logger.Log("transport", "HTTP", "addr", addr, "msg", "listening")
注:Log呼び出しに渡されるものはすべて、インターフェースに変換されます。 これは、多くのメモリ割り当て操作があることを意味します。
別のzap構造ロギングパッケージと比較してください。 ロギングの呼び出しはそれほど便利ではありませんが、インターフェースとそれに応じた割り当てを避けるために行われます。
logger.Info("Failed to fetch URL.", zap.String("url", url), zap.Int("attempt", tryNum), zap.Duration("backoff", sleepFor), )
logger.Info
の引数は、 logger.Info
型logger.Field. logger.Field
logger.Field. logger.Field
は、 string
、 int
、およびinterface{}
それぞれの型とフィールドを含むla union構造です。 そして、基本的なタイプの値を伝えるためにインターフェースは必要ないことがわかります。
しかし、ロギングについては十分です。 値をインターフェイスに変換するときにメモリの割り当てが必要になることがある理由を見てみましょう。
インターフェイスは、2つの単語で表されます:型へのポインターと値へのポインター。 ラスコックスはこれについて素晴らしい記事を書いたので、ここでは繰り返しません。 行って読んでください。
それでも、彼のデータは少し時代遅れです。 著者は明らかな最適化を指摘しています。値のサイズがポインターのサイズ以下である場合、ポインターの代わりに値をインターフェイスの2番目の単語に入れるだけです。 ただし、競合するガベージコレクターの登場により、この最適化はコンパイラーから削除され 、2番目の単語は常に単なるポインターになりました。
次のようなコードがあるとします:
fmt.Println(1)
Go 1.4より前のバージョンでは、値1をインターフェイスの2番目のワードに直接入力できるため、メモリ割り当てにつながりませんでした。
つまり、コンパイラは次のようなことをしました。
fmt.Println({int, 1}),
{typ, val}
はインターフェースの2つの単語を表します。
Go 1.4の後、このコードはメモリ割り当てにつながり始めました。これは、1がポインタではなく、インターフェイスの2番目の単語がポインタになっているためです。 そして、コンパイラーとランタイムが次のようなことをしたことがわかりました。
i := new(int) // allocates! *i = 1 fmt.Println({int, i})
これは不快であり、この変更後、口頭の戦いで多くのコピーが破損しました。
割り当てを取り除くための最初の重要な最適化は、少し後で行われました。 結果のインターフェイスが暴走しなかったときに機能しました( 翻訳者のメモ:エスケープ分析からの用語 )。 この場合、一時的な値はヒープではなくスタックに割り当てることができます。 上記の例を使用します。
i := new(int) // now doesn't allocate, as long as e doesn't escape *i = 1 var e interface{} = {int, i} // do things with e that don't make it escape
残念ながら、 fmt.Println
や上記のロギングの例など、多くのインターフェースが逃げています。
幸い、Go 1.9にはさらにいくつかの最適化が登場します。その実装は、ロギングに関する会話に触発されたものです(もちろん、常に可能な限り最後にロールバックされない限り)。
最初の最適化は、定数をインターフェイスに変換するときにメモリを割り当てないことです。 そのため、 fmt.Println(1)
はメモリ割り当てになりません。 コンパイラは、グローバルな読み取り専用メモリ領域に値1を設定します。 このようなもの:
var i int = 1 // at the top level, marked as readonly fmt.Println({int, &i})
定数は不変(不変)であり、とにかくそのままであるため、これが可能です。
この最適化は、ロギングの議論に触発されました。 構造ロギングでは、多数の引数が定数です(正確にはすべてのキーと、おそらくいくつかの値)。 go-kit
例を覚えておいてください。
logger.Log("transport", "HTTP", "addr", addr, "msg", "listening")
Go 1.9以降のこのコードでは、6つの引数のうち5つが定数文字列であるため、6つではなく1つのメモリ割り当て操作のみになります。
2番目の新しい最適化は、 ブール値とバイトをインターフェイスに変換するときにメモリを割り当てないことです。 この最適化は、結果のすべてのバイナリにstaticbytes
と呼ばれるグローバル[256]byte
配列を追加することで実装されます。 この配列では、すべてのbに対してstaticbytes [b] = bであることが事実です。 コンパイラーは、ブール値、またはuint8
、または他のシングルバイト値をインターフェイスに配置する場合、それを割り当てる代わりに、この配列の要素へのポインターをそこに配置します。 たとえば、次のように:
var staticbytes [256]byte = {0, 1, 2, 3, 4, 5, ...} i := uint8(1) fmt.Println({uint8, &staticbytes[i]})
そして、まだレビュー段階にある3番目の最適化は、 標準のゼロ値をインターフェイスに変換するときにメモリを割り当てないことです。 これは、整数、浮動小数点数、文字列、スライスのヌル値に適用されます。 Rantimeは、値がゼロ値と等しいかどうかをチェックし、等しい場合は、メモリを割り当てる代わりに、 既存の大きなゼロのチャンクへのポインターを使用します。
すべてが計画どおりであれば、Go 1.9はインターフェイスへの変換中に多数の割り当てを削除します。 しかし、彼はそのような割り当てをすべて削除するわけではありません。これは、ロギングの標準化を議論する際にパフォーマンスの問題が引き続き重要であることを意味します。
APIと実装内のいくつかのソリューションとの相互作用は非常に興味深いものです。
APIを選択して作成するには、パフォーマンスの問題について考える必要があります。 io.Readerインターフェースが呼び出しコードが独自のバッファーを使用できるようにすることは偶然ではありません。
パフォーマンスは、さまざまなソリューションの重要な側面です。 上で見たように、インターフェイスの実装の詳細は、メモリ割り当て操作が発生する場所とタイミングに影響します。 同時に、これらの決定は、人々が書くコードに依存します。 コンパイラとランタイムの作成者は、頻繁に使用される実際のコードを最適化しようとしています。 そのため、 fmt.Println(1)
で不必要なメモリ割り当て操作を引き起こす3番目の単語を追加する代わりに、Go 1.4で2つの単語をインターフェイスに保存するという決定は、実際の人が書いたコードを考慮することに基づいています。
そして、人々が書くコードの種類は、多くの場合、使用するAPIに依存するため、一方では驚くべきものであり、他方では管理が困難なフィードバックが得られます。
おそらくこれはあまり深い観察ではありませんが、それでもなお、APIを設計していてパフォーマンスが心配な場合は、コンパイラーとランタイムが何をするかだけでなく、何ができるかにも注意してください。 現在のコードを記述しますが、将来のAPIを設計します。
わからない場合は、お尋ねください。 これはロギング用に(少し)働きました。