Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15988] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージ内の server.go ファイルに対する変更です。具体的には、HTTPレスポンスを扱う response 構造体において、チャンク転送エンコーディングを処理する chunkWriter 構造体をインライン化(埋め込み)することで、メモリ割り当ての削減とパフォーマンスの向上を図っています。

コミット

commit a891484a4edb241ae1b11f3b336f63fe08b10cb7
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Mar 28 13:13:28 2013 -0700

    net/http: inline chunkWriter in response
    
    A chunkWriter and a response are 1:1. Make them contiguous in
    memory and save an allocation.
    
    benchmark                                   old ns/op    new ns/op    delta
    BenchmarkServerFakeConnWithKeepAliveLite        10715        10539   -1.64%
    
    benchmark                                  old allocs   new allocs    delta
    BenchmarkServerFakeConnWithKeepAliveLite           21           20   -4.76%
    
    benchmark                                   old bytes    new bytes    delta
    BenchmarkServerFakeConnWithKeepAliveLite         1626         1609   -1.05%
    
    R=golang-dev, gri
    CC=golang-dev
    https://golang.org/cl/8114043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a891484a4edb241ae1b11f3b336f63fe08b10cb7

元コミット内容

このコミットの目的は、net/http パッケージの response 構造体内に chunkWriter をインライン化することです。これにより、chunkWriterresponse がメモリ上で連続して配置され、個別のメモリ割り当てが不要になります。結果として、ベンチマークにおいて1操作あたりのナノ秒 (ns/op)、メモリ割り当て回数 (allocs)、割り当てバイト数 (bytes) の全てが削減され、パフォーマンスとメモリ効率が向上しています。

変更の背景

Goの net/http パッケージは、ウェブサーバーを構築するための基盤を提供します。HTTPレスポンスをクライアントに送信する際、特に大きなボディを持つレスポンスの場合、HTTP/1.1のチャンク転送エンコーディングが使用されることがあります。このチャンク転送を効率的に処理するために chunkWriter という内部構造体が利用されます。

以前の実装では、response 構造体は chunkWriter へのポインタ (*chunkWriter) を保持していました。これは、response が作成されるたびに chunkWriter がヒープ上に個別に割り当てられることを意味します。HTTPリクエストごとに response オブジェクトが生成されるため、この個別の割り当ては、特に高負荷なサーバー環境において、ガベージコレクションの頻度を増やし、パフォーマンスのオーバーヘッドとなる可能性がありました。

このコミットの背景にあるのは、このようなマイクロ最適化を通じて、Goの標準ライブラリのパフォーマンスをさらに向上させるという継続的な取り組みです。chunkWriterresponse が常に1対1の関係にあるという事実を利用し、これらをメモリ上で連続させることで、不要なポープタ参照とヒープ割り当てを排除し、より効率的なメモリ利用と高速な処理を実現することが目的です。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびHTTPプロトコルに関する知識が役立ちます。

  1. Go言語の構造体とポインタ:

    • Goの構造体は値型です。構造体を別の構造体のフィールドとして宣言すると、そのフィールドは外側の構造体の一部としてメモリ上に直接配置されます(インライン化)。
    • ポインタ (*T) は、メモリ上の特定のアドレスを指す変数です。ポインタを使用すると、間接参照が発生し、メモリへのアクセスに追加のステップが必要になります。また、new(T) のようにポインタを返す関数は、通常、ヒープにメモリを割り当てます。
    • ヒープ割り当ては、ガベージコレクタの対象となり、ガベージコレクションの頻度や実行時間はアプリケーションのパフォーマンスに影響を与えます。スタック割り当ては、関数呼び出しの終了とともに自動的に解放されるため、ガベージコレクションの対象外です。
  2. HTTP/1.1 チャンク転送エンコーディング:

    • HTTP/1.1では、レスポンスボディの長さを事前に知ることができない場合に、チャンク転送エンコーディング (Transfer-Encoding: chunked) を使用してボディを送信できます。
    • この方式では、ボディは複数の「チャンク」に分割され、各チャンクはサイズ情報とデータを含みます。最後のチャンクはサイズが0で、転送の終了を示します。
    • net/http パッケージの chunkWriter は、このチャンク転送エンコーディングのロジックをカプセル化し、io.Writer インターフェースを実装しています。これにより、アプリケーションは通常の Write 操作を行うだけで、内部的にチャンク形式に変換されて送信されます。
  3. io.Writer インターフェース:

    • Goの標準ライブラリで定義されている基本的なインターフェースの一つで、Write([]byte) (int, error) メソッドを持ちます。
    • ファイル、ネットワーク接続、バッファなど、様々な出力先にデータを書き込むための抽象化を提供します。bufio.WriterchunkWriter はこのインターフェースを実装しています。
  4. bufio.Writer:

    • bufio パッケージは、バッファリングされたI/Oを提供します。bufio.Writer は、基になる io.Writer の上にバッファ層を追加し、小さな書き込み操作をまとめて効率的に処理します。
    • これにより、基になる io.Writer へのシステムコールやネットワークI/Oの回数を減らし、パフォーマンスを向上させます。
  5. ベンチマークの指標:

    • ns/op (nanoseconds per operation): 1操作あたりの平均実行時間。値が小さいほど高速であることを示します。
    • allocs (allocations per operation): 1操作あたりのメモリ割り当て回数。値が小さいほどメモリ割り当てのオーバーヘッドが少ないことを示します。
    • bytes (bytes allocated per operation): 1操作あたりの平均メモリ割り当てバイト数。値が小さいほどメモリ使用量が少ないことを示します。

技術的詳細

このコミットの核心は、Go言語の構造体の埋め込み(インライン化)とメモリ割り当ての最適化にあります。

以前の response 構造体は、chunkWriter をポインタとして保持していました。

type response struct {
    // ...
    cw *chunkWriter // chunkWriterへのポインタ
    // ...
}

この定義では、response 構造体が作成される際に、cw フィールド自体はポインタを格納するためのメモリを消費しますが、chunkWriter の実際のデータは別のメモリ領域(通常はヒープ)に割り当てられ、そのアドレスが cw に格納されます。new(chunkWriter) の呼び出しは、この個別のヒープ割り当てを引き起こします。

変更後、response 構造体は chunkWriter を直接埋め込むようになりました。

type response struct {
    // ...
    cw chunkWriter // chunkWriterを直接埋め込み
    // ...
}

この変更により、response 構造体がメモリに割り当てられる際(スタック上またはヒープ上)、chunkWriter の全てのフィールドも response 構造体の一部として連続したメモリ領域に配置されます。これにより、以下の利点が得られます。

  1. メモリ割り当ての削減: chunkWriter のための個別の new 呼び出しとそれに伴うヒープ割り当てが不要になります。response 構造体全体の割り当てが1回行われるだけで、chunkWriter のメモリも同時に確保されます。これにより、ガベージコレクタの負担が軽減され、全体的なパフォーマンスが向上します。ベンチマーク結果の allocs が21から20に減少しているのは、この1回のアロケーション削減を明確に示しています。
  2. キャッシュ効率の向上: responsechunkWriter がメモリ上で連続して配置されるため、CPUのキャッシュ効率が向上します。response のデータにアクセスする際に chunkWriter のデータも同時にキャッシュにロードされる可能性が高まり、メモリアクセスのレイテンシが減少します。
  3. ポインタの間接参照の排除: cw がポインタではなくなったため、response から chunkWriter のフィールドにアクセスする際にポインタの間接参照が不要になります。これはわずかながらCPUサイクルを節約し、命令キャッシュの効率も向上させます。

また、newBufioWriterSize 関数に渡す引数も変更されています。以前は w.cw (ポインタ) を渡していましたが、変更後は &w.cw (埋め込まれた chunkWriter のアドレス) を渡しています。これは、newBufioWriterSizeio.Writer インターフェースを期待しており、chunkWriter がそのインターフェースを実装しているため、chunkWriter の値のアドレスを渡す必要があるためです。

ベンチマーク結果は、この変更が実際にパフォーマンスに寄与していることを示しています。

  • BenchmarkServerFakeConnWithKeepAliveLitens/op10715 から 10539 へと 1.64% 減少。
  • allocs21 から 20 へと 4.76% 減少。
  • bytes1626 から 1609 へと 1.05% 減少。

これらの数値は、この比較的小さな変更が、高頻度で実行されるHTTPサーバーのコアパスにおいて、測定可能なパフォーマンス改善をもたらしたことを裏付けています。

コアとなるコードの変更箇所

変更は src/pkg/net/http/server.go ファイルの2箇所です。

  1. response 構造体の cw フィールドの型変更:

    --- a/src/pkg/net/http/server.go
    +++ b/src/pkg/net/http/server.go
    @@ -288,7 +288,7 @@ type response struct {
     	wroteContinue bool     // 100 Continue response was written
    
     	w  *bufio.Writer // buffers output in chunks to chunkWriter
    -	cw *chunkWriter
    +	cw chunkWriter
     	sw *switchWriter // of the bufio.Writer, for return to putBufioWriter
    

    cw フィールドの型が *chunkWriter (ポインタ) から chunkWriter (値) に変更されました。

  2. readRequest 関数内での chunkWriter の初期化と bufio.Writer への渡し方:

    --- a/src/pkg/net/http/server.go
    +++ b/src/pkg/net/http/server.go
    @@ -558,10 +558,9 @@ func (c *conn) readRequest() (w *response, err error) {\n     		req:           req,\n     		handlerHeader: make(Header),\n     		contentLength: -1,\n    -		cw:            new(chunkWriter),\n     	}\n     	w.cw.res = w
    -	w.w, w.sw = newBufioWriterSize(w.cw, bufferBeforeChunkingSize)
    +	w.w, w.sw = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
     	return w, nil
     }
    
    • response 構造体の初期化から cw: new(chunkWriter), の行が削除されました。cwresponse 構造体の一部として自動的にゼロ値で初期化されるため、明示的な new 呼び出しは不要になります。
    • newBufioWriterSize 関数に渡す引数が w.cw から &w.cw に変更されました。これは、newBufioWriterSizeio.Writer インターフェースを実装するオブジェクトを期待しており、chunkWriter は値型として io.Writer を実装しているため、そのアドレスを渡す必要があるためです。

コアとなるコードの解説

このコミットの変更は、Go言語における構造体の埋め込みと、それによるメモリ管理の最適化の典型的な例を示しています。

response 構造体は、HTTPレスポンスの構築と送信に必要な様々な情報をカプセル化しています。その中には、レスポンスボディの書き込みを処理する io.Writer のチェーンが含まれます。具体的には、*bufio.WriterchunkWriter に書き込み、chunkWriter が最終的に基になるネットワーク接続に書き込みます。

変更前は、response 構造体は chunkWriter へのポインタ (*chunkWriter) を保持していました。これは、response オブジェクトが作成されるたびに、chunkWriter のインスタンスがヒープに別途割り当てられ、そのポインタが responsecw フィールドに格納されることを意味します。

// 変更前: cwはポインタ
type response struct {
    // ...
    cw *chunkWriter // ヒープに割り当てられたchunkWriterへのポインタ
    // ...
}

func (c *conn) readRequest() (w *response, err error) {
    w = &response{
        // ...
        cw: new(chunkWriter), // ここでヒープ割り当てが発生
    }
    w.cw.res = w
    w.w, w.sw = newBufioWriterSize(w.cw, bufferBeforeChunkingSize) // ポインタを渡す
    return w, nil
}

変更後は、response 構造体内に chunkWriter が直接埋め込まれるようになりました。

// 変更後: cwは値
type response struct {
    // ...
    cw chunkWriter // response構造体の一部としてメモリに直接配置される
    // ...
}

func (c *conn) readRequest() (w *response, err error) {
    w = &response{
        // ...
        // cw: new(chunkWriter), // この行は不要になり削除
    }
    w.cw.res = w // cwはresponseの一部なので、直接アクセス
    w.w, w.sw = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize) // cwのアドレスを渡す
    return w, nil
}

この変更により、response 構造体が割り当てられる際に、chunkWriter のメモリも同時に確保されます。これにより、chunkWriter のための個別のヒープ割り当てが不要となり、メモリ割り当ての回数が1回削減されます。これは、ベンチマーク結果の allocs の減少に直接的に反映されています。

また、newBufioWriterSize 関数は io.Writer インターフェースを引数として受け取ります。Goでは、値型がインターフェースを実装している場合、その値のアドレス(ポインタ)をインターフェース型として渡すことができます。chunkWriterio.Writer インターフェースを実装しているため、変更後は &w.cw (埋め込まれた chunkWriter のアドレス) を渡すことで、bufio.WriterchunkWriter を基になるライターとして使用できるようになります。

この最適化は、HTTPサーバーのような高スループットが求められるアプリケーションにおいて、小さなメモリ割り当ての削減が全体的なパフォーマンスに大きな影響を与える可能性があることを示しています。特に、ガベージコレクションの頻度を減らすことは、アプリケーションのレイテンシとスループットの安定性にとって重要です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (src/pkg/net/http/server.go)
  • Go言語のベンチマークに関する一般的な知識
  • Go言語におけるメモリ管理とガベージコレクションに関する一般的な知識
  • HTTP/1.1 プロトコル仕様 (RFC 7230)