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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージおよび net/http/httputil パッケージにおけるHTTPチャンク転送エンコーディングの読み書き処理において、メモリ割り当て(アロケーション)を削減することを目的としています。具体的には、チャンクの終端を示すCRLF(Carriage Return, Line Feed)の読み取り時と、チャンクサイズをヘッダとして書き出す際に発生する一時的なメモリ割り当てを排除し、パフォーマンスの向上とガベージコレクション(GC)負荷の軽減を図っています。

コミット

commit f3e6b2060679a6f430c9e711cf797d76d4226a15
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Nov 16 13:25:01 2012 -0800

    net/http: reduce allocations in chunk reading & writing

    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6847063

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

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

元コミット内容

net/http: reduce allocations in chunk reading & writing

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6847063

変更の背景

この変更の主な背景は、Goプログラムのパフォーマンス最適化とガベージコレクション(GC)の効率化です。

HTTP/1.1では、メッセージボディの長さを事前に知ることができない場合に「チャンク転送エンコーディング (Chunked Transfer Encoding)」を使用します。これは、メッセージボディを複数の「チャンク」に分割し、各チャンクの前にそのサイズ(16進数表記)とCRLFを付加し、最後にゼロサイズのチャンクとCRLFで終了するという仕組みです。

Goの net/http パッケージは、このチャンク転送エンコーディングを透過的に処理します。しかし、以前の実装では、チャンクの読み取り時(特に各チャンクの終端にあるCRLFを読み取る際)や、チャンクサイズを文字列として書き出す際に、一時的なバイトスライスや文字列がヒープ上に頻繁に割り当てられていました。

Goのガベージコレクタは、ヒープ上に割り当てられたオブジェクトを追跡し、不要になったものを回収します。一時的なオブジェクトが大量に生成されると、GCが頻繁に実行され、そのオーバーヘッドがアプリケーションのパフォーマンスに影響を与える可能性があります。特に、HTTPサーバーのような高スループットが求められるアプリケーションでは、わずかなアロケーションの削減でも全体的なパフォーマンスに大きな改善をもたらすことがあります。

このコミットは、これらの小さな、しかし頻繁に発生するアロケーションを排除することで、GCの負荷を軽減し、HTTPチャンク処理の効率を向上させることを目的としています。

前提知識の解説

1. HTTP チャンク転送エンコーディング (Chunked Transfer Encoding)

HTTP/1.1の機能の一つで、メッセージボディの長さを事前に決定できない場合(例: 動的に生成されるコンテンツ、ストリーミングデータ)に利用されます。

  • 仕組み:

    1. メッセージボディは複数の「チャンク」に分割されます。
    2. 各チャンクの前に、そのチャンクのデータサイズ(バイト数)が16進数で記述され、その後にCRLF (\r\n) が続きます。
    3. 次に実際のチャンクデータが続き、その後にCRLF (\r\n) が続きます。
    4. 最後のチャンクはサイズが 0 であり、その後にCRLF (\r\n) が続きます。
    5. 最後に、オプションのトレーラーヘッダ(追加のHTTPヘッダ)が続き、その後にCRLF (\r\n) が2回続きます。
  • :

    4\r\n        (サイズ: 4バイト)
    Wiki\r\n     (データ: "Wiki")
    5\r\n        (サイズ: 5バイト)
    pedia\r\n    (データ: "pedia")
    E\r\n         (サイズ: 14バイト)
     in\r\nchunks.\r\n (データ: " in\r\nchunks.")
    0\r\n        (最後のチャンク、サイズ0)
    \r\n         (トレーラーヘッダなし、終端)
    

2. Go言語のメモリ管理とガベージコレクション (GC)

Goは自動メモリ管理(ガベージコレクション)を採用しています。

  • ヒープとスタック:
    • スタック: 関数呼び出し、ローカル変数、関数の引数などが格納される領域。サイズがコンパイル時に決定できる、または非常に小さい一時的なデータが置かれます。スタック上のメモリは関数が終了すると自動的に解放されます。アロケーションと解放が非常に高速です。
    • ヒープ: プログラム実行中に動的にサイズが変化するデータ(例: スライス、マップ、チャネル、構造体のポインタなど)が格納される領域。ヒープ上のメモリはGCによって管理されます。
  • アロケーション: makenew を使って作成されるデータは通常ヒープに割り当てられます。
  • ガベージコレクション (GC): ヒープ上に割り当てられたオブジェクトのうち、どのオブジェクトも参照されなくなった(到達不可能になった)ものを自動的に検出し、そのメモリを再利用可能にするプロセスです。
  • GCのオーバーヘッド: GCはプログラムの実行を一時停止させたり(Stop-the-World)、バックグラウンドで実行されたりするため、その実行頻度や時間がパフォーマンスに影響を与えます。ヒープアロケーションが頻繁に発生すると、GCがより頻繁に実行される必要があり、これがアプリケーションのレイテンシやスループットに悪影響を及ぼす可能性があります。
  • アロケーションの削減の重要性: ヒープアロケーションを減らすことは、GCの実行回数を減らし、GCが処理すべきオブジェクトの数を減らすことにつながります。これにより、GCのオーバーヘッドが減少し、アプリケーション全体のパフォーマンスが向上します。

3. Goの io パッケージと fmt パッケージ

  • io.Reader / io.Writer: GoにおけるI/O操作の基本的なインターフェース。データを読み書きするための抽象化を提供します。
  • io.ReadFull(r Reader, buf []byte) (n int, err error): r から buf のサイズ分だけデータを読み込もうとします。buf が完全に埋まるまで読み込むか、エラーが発生するまでブロックします。
  • io.WriteString(w Writer, s string) (n int, err error): 文字列 sw に書き込みます。
  • fmt.Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error): format 文字列に従ってフォーマットされたデータを w に書き込みます。printf スタイルのフォーマットをサポートし、直接 io.Writer に書き込むため、中間的な文字列生成を避けることができます。

4. strconv パッケージ

  • strconv.FormatInt(i int64, base int) string: 整数 i を指定された base(基数)で文字列に変換します。この関数は新しい文字列を生成するため、ヒープアロケーションが発生します。

技術的詳細

このコミットは、HTTPチャンク転送エンコーディングの処理における2つの主要なアロケーションポイントをターゲットにしています。

1. チャンク読み取り時のCRLF処理の最適化

HTTPチャンクの各データブロックの終わりには、チャンクデータと次のチャンクサイズの間を区切るCRLF (\r\n) が存在します。以前の実装では、この2バイトのCRLFを読み取るために、毎回 make([]byte, 2) を使用して新しい2バイトのスライスをヒープ上に割り当てていました。

このコミットでは、chunkedReader 構造体に buf [2]byte という固定サイズの配列を追加しました。これはスタック上に割り当てられるか、構造体の一部としてヒープに割り当てられたとしても、そのサイズは固定であり、読み取りごとに新しいスライスを動的に割り当てる必要がなくなります。

io.ReadFull(cr.r, cr.buf[:]) のように、この構造体内の固定配列のスライス (cr.buf[:]) を io.ReadFull に渡すことで、ヒープアロケーションなしでCRLFを読み取ることが可能になります。これにより、チャンクが多数存在するHTTPレスポンスを処理する際に発生する、頻繁な2バイトスライスの割り当てとGCの負荷が大幅に削減されます。

2. チャンク書き込み時のサイズヘッダ処理の最適化

HTTPチャンクを書き出す際には、各チャンクデータの前にそのサイズを16進数で記述した文字列とCRLFを付加する必要があります。以前の実装では、strconv.FormatInt を使用してチャンクサイズを文字列に変換し、その後に io.WriteString を使用してその文字列を書き込んでいました。

strconv.FormatInt は、変換結果の文字列をヒープ上に割り当てます。チャンクごとにこの処理が行われるため、書き込み時にも頻繁な文字列アロケーションが発生していました。

このコミットでは、fmt.Fprintf(cw.Wire, "%x\r\n", len(data)) を使用するように変更しました。fmt.Fprintf は、io.Writer インターフェースを実装する任意のオブジェクトに直接フォーマットされた文字列を書き込むことができます。%x フォーマット指定子は整数を16進数で出力するために使用されます。

fmt.Fprintf の内部実装は、多くの場合、中間的な文字列バッファを再利用したり、直接バイト列として書き込んだりすることで、strconv.FormatInt で文字列を生成し、その後に io.WriteString で書き込むよりも効率的に処理を行うことができます。これにより、チャンクサイズを表す文字列のヒープアロケーションが削減され、書き込み処理のパフォーマンスが向上します。

これらの変更は、Goの net/http パッケージが提供するHTTPサーバーやクライアントが、チャンク転送エンコーディングを扱う際のメモリ効率を大幅に改善し、特に高負荷な環境下でのGCによるパフォーマンス低下を抑制する効果があります。

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

src/pkg/net/http/chunked.go および src/pkg/net/http/httputil/chunked.go

両ファイルで同様の変更が行われています。

1. chunkedReader 構造体へのフィールド追加

--- a/src/pkg/net/http/chunked.go
+++ b/src/pkg/net/http/chunked.go
@@ -39,6 +40,7 @@ type chunkedReader struct {
 	r   *bufio.Reader
 	n   uint64 // unread bytes in chunk
 	err error
+	buf [2]byte // 追加
 }

2. chunkedReader.Read メソッド内のCRLF読み取り部分の変更

--- a/src/pkg/net/http/chunked.go
+++ b/src/pkg/net/http/chunked.go
@@ -74,9 +76,8 @@ func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
 	cr.n -= uint64(n)
 	if cr.n == 0 && cr.err == nil {
 		// end of chunk (CRLF)
-		b := make([]byte, 2) // 削除: 新しいスライスのアロケーション
-		if _, cr.err = io.ReadFull(cr.r, b); cr.err == nil {
-			if b[0] != '\r' || b[1] != '\n' {
+		if _, cr.err = io.ReadFull(cr.r, cr.buf[:]); cr.err == nil { // 変更: 構造体内の固定バッファを使用
+			if cr.buf[0] != '\r' || cr.buf[1] != '\n' { // 変更: 固定バッファを参照
 				cr.err = errors.New("malformed chunked encoding")
 			}
 		}

3. chunkedWriter.Write メソッド内のチャンクサイズ書き込み部分の変更

--- a/src/pkg/net/http/chunked.go
+++ b/src/pkg/net/http/chunked.go
@@ -147,9 +148,7 @@ func (cw *chunkedWriter) Write(data []byte) (n int, err error) {
 		return 0, nil
 	}
 
-	head := strconv.FormatInt(int64(len(data)), 16) + "\r\n" // 削除: 文字列生成とアロケーション
-
-	if _, err = io.WriteString(cw.Wire, head); err != nil { // 削除: 文字列書き込み
+	if _, err = fmt.Fprintf(cw.Wire, "%x\r\n", len(data)); err != nil { // 変更: fmt.Fprintfで直接書き込み
 		return 0, err
 	}
 	if n, err = cw.Wire.Write(data); err != nil {

コアとなるコードの解説

chunkedReader の変更

  • buf [2]byte の追加: chunkedReader 構造体に buf [2]byte という2バイトの固定長配列が追加されました。これは、各チャンクの終端にあるCRLF(2バイト)を読み取るための一時バッファとして再利用されます。固定長配列は、そのサイズがコンパイル時に決定されるため、ヒープではなくスタックに割り当てられる可能性が高く(エスケープ解析の結果による)、たとえヒープに割り当てられたとしても、読み取りごとに動的に make([]byte, 2) で新しいスライスを割り当てるオーバーヘッドがなくなります。

  • io.ReadFull(cr.r, cr.buf[:]) への変更: 以前は b := make([]byte, 2) で新しいスライスを生成し、そこにCRLFを読み込んでいました。この make 呼び出しがヒープアロケーションを引き起こしていました。 変更後は、cr.buf[:]io.ReadFull に渡すことで、chunkedReader インスタンスが持つ既存の固定バッファを再利用してCRLFを読み込みます。これにより、チャンクごとに発生していた2バイトスライスのヒープアロケーションが完全に排除されます。

chunkedWriter の変更

  • fmt.Fprintf(cw.Wire, "%x\r\n", len(data)) への変更: 以前は、チャンクサイズを16進数文字列に変換するために strconv.FormatInt(int64(len(data)), 16) を使用していました。この関数は結果の文字列をヒープに割り当てるため、チャンクごとにアロケーションが発生していました。その後、io.WriteString でその文字列を cw.Wire(基となる io.Writer)に書き込んでいました。 変更後は、fmt.Fprintf を使用しています。fmt.Fprintf は、指定されたフォーマット文字列 ("%x\r\n") と引数 (len(data)) を直接 cw.Wire に書き込みます。%x は整数を16進数でフォーマットする指示子です。 fmt.Fprintf の内部実装は、多くの場合、中間的な文字列オブジェクトをヒープに割り当てることなく、直接バイト列として io.Writer に書き込むように最適化されています。これにより、チャンクサイズ文字列の生成と書き込みに伴うヒープアロケーションが削減され、チャンク書き込み処理の効率が向上します。

これらの変更は、GoのHTTPチャンク処理におけるメモリフットプリントとGCオーバーヘッドを削減し、特に多数のチャンクを含むHTTPメッセージを扱う際のパフォーマンスを向上させることに貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (pkg.go.dev)
  • HTTP/1.1 RFC 2616
  • Go言語のメモリ管理とガベージコレクションに関する一般的な知識
  • Go言語のソースコード (net/http/chunked.go, net/http/httputil/chunked.go)
  • Goのコードレビューシステム (golang.org/cl/6847063)
  • Goのパフォーマンス最適化に関する一般的な記事や議論(アロケーション削減の重要性)