[インデックス 14444] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
および net/http/httputil
パッケージにおけるチャンク読み込み処理の改善を目的としています。具体的には、チャンクサイズを読み取る際に発生する不要なメモリ割り当て(ガベージ)を削減し、パフォーマンスを向上させるための変更が含まれています。
コミット
commit 9466c27fec1d5e37c37f73a4cd2e32ad16460384
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Nov 19 19:50:42 2012 -0800
net/http: remove more garbage from chunk reading
Noticed this while closing tabs. Yesterday I thought I could
ignore this garbage and hope that a fix for issue 2205 handled
it, but I just realized that's the opposite case,
string->[]byte, whereas this is []byte->string. I'm having a
hard time convincing myself that an Issue 2205-style fix with
static analysis and faking a string header would be safe in
all cases without violating the memory model (callee assumes
frozen memory; are there non-racy ways it could keep being
modified?)
R=dsymonds
CC=dave, gobot, golang-dev
https://golang.org/cl/6850067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9466c27fec1d5e37c37f73a4cd2e32ad16460384
元コミット内容
net/http: remove more garbage from chunk reading
(net/http: チャンク読み込みからさらなるガベージを除去)
このコミットは、チャンク読み込み時に発生する不要なメモリ割り当てを削減することを目的としています。コミットメッセージでは、以前はIssue 2205の修正で対応できると考えていたが、今回のケース([]byte
からstring
への変換)は逆のケースであり、Issue 2205のような静的解析と文字列ヘッダの偽装による修正が、メモリモデルを侵害せずに常に安全であるとは限らないと述べています。
変更の背景
HTTP/1.1では、メッセージボディをチャンク形式で転送することができます。これは、サーバーがレスポンス全体のサイズを事前に知らなくても、動的にコンテンツを生成しながら送信できるため、特にストリーミングや動的なコンテンツ配信において有用です。チャンク形式では、各チャンクの前にそのチャンクのサイズが16進数で記述され、その後にチャンクデータが続きます。
このコミットの背景には、GoのHTTPクライアントまたはサーバーがチャンク形式のデータを読み取る際に、チャンクサイズを解析する過程で不要なメモリ割り当て(ガベージ)が発生していた問題があります。特に、bufio.Reader
から読み取ったバイトスライス([]byte
)をstrconv.ParseUint
に渡す前にstring
に変換していたことが、その原因の一つでした。Goでは、[]byte
からstring
への変換は、新しい文字列のメモリを割り当てるため、頻繁に行われるとガベージコレクションの負荷を増大させ、パフォーマンスに影響を与えます。
コミットメッセージで言及されている「Issue 2205」は、Goのコンパイラ最適化に関するもので、string
から[]byte
への変換において、コンパイラが一時的な[]byte
スライスを生成する際に、元の文字列のメモリを再利用できるような最適化を検討していた問題です。しかし、今回のケースは[]byte
からstring
への逆方向の変換であり、かつ、読み取ったバイトスライスがbufio.Reader
の内部バッファを指しているため、その内容が後で変更される可能性があるというメモリモデル上の懸念がありました。そのため、Issue 2205のようなアプローチを直接適用することは困難であり、別の方法でガベージを削減する必要がありました。
この変更は、HTTP通信の効率性を高め、特に大量のチャンクデータを扱うアプリケーションにおいて、メモリ使用量とGCレイテンシを削減することを目的としています。
前提知識の解説
HTTPチャンク転送エンコーディング (Chunked Transfer Encoding)
HTTP/1.1で導入された転送エンコーディングの一種で、メッセージボディを可変長のチャンクに分割して送信する仕組みです。これにより、Content-Lengthヘッダを事前に知る必要がなくなり、動的なコンテンツ生成やストリーミングが可能になります。
チャンク形式のメッセージボディは以下の構造を持ちます。
- チャンクサイズ: 16進数で表現されたチャンクのバイト数。その後にCRLF(
\r\n
)が続く。 - チャンクデータ: チャンクサイズで指定されたバイト数のデータ。その後にCRLFが続く。
- 上記1と2の繰り返し。
- 最終チャンク: サイズが0のチャンク。その後にCRLFが続く。
- トレーラーヘッダ(オプション): 追加のHTTPヘッダ。その後にCRLFが続く。
- 空行: 最終的なCRLF。
例:
4\r\n
Wiki\r\n
5\r\n
pedia\r\n
E\r\n
in\r\n
\r\n
chunks.\r\n
0\r\n
\r\n
Go言語のメモリモデルとstring
vs []byte
Go言語では、string
型はイミュータブル(不変)なバイト列を表します。一方、[]byte
型はミュータブル(可変)なバイトスライスです。
string
から[]byte
への変換:[]byte(myString)
のように変換すると、通常は新しいバイトスライスが作成され、元の文字列のデータがコピーされます。ただし、Goコンパイラは特定の状況下で最適化を行い、コピーを避けることがあります(例:[]byte(myString)[:n]
のようにスライスする場合)。[]byte
からstring
への変換:string(myBytes)
のように変換すると、常に新しい文字列が作成され、myBytes
の内容がコピーされます。これは、string
が不変であるため、元の[]byte
が後で変更されても文字列の内容が変わらないようにするためです。このコピー操作が、頻繁に行われるとメモリ割り当てとガベージコレクションのオーバーヘッドを引き起こします。
ガベージコレクション (GC)
Go言語は自動ガベージコレクションを採用しています。プログラムが不要になったメモリを自動的に解放し、再利用可能にします。しかし、頻繁なメモリ割り当てはGCの頻度と時間を増加させ、アプリケーションのレイテンシやスループットに悪影響を与える可能性があります。特に、短命なオブジェクトが大量に生成されると、GCの負担が大きくなります。
bufio.Reader.ReadSlice
bufio.Reader
は、バッファリングされたI/Oを提供するGoの標準ライブラリです。ReadSlice
メソッドは、指定されたデリミタ(この場合は改行\n
)までデータを読み込み、そのデータを指すバイトスライスを返します。このバイトスライスは、bufio.Reader
の内部バッファを直接指しているため、次の読み込み操作が行われると、その内容が無効になる可能性があります。
技術的詳細
このコミットの主要な変更点は、チャンクサイズを読み取る際の[]byte
からstring
への不要な変換を排除し、代わりに[]byte
を直接処理するように変更したことです。
変更前は、chunkedReader.beginChunk()
内でチャンクサイズを表す行を読み取る際に、readLine
関数がstring
を返していました。このreadLine
関数は、内部でreadLineBytes
が返した[]byte
をstring()
に変換していました。このstring()
変換が、チャンクサイズを読み取るたびに新しい文字列オブジェクトをヒープに割り当て、ガベージを生成していました。
変更後は、chunkedReader.beginChunk()
がreadLine
から[]byte
を受け取るように変更され、strconv.ParseUint
の代わりに新しく導入されたparseHexUint
関数が[]byte
を直接受け取って16進数をuint64
に変換するようになりました。これにより、[]byte
からstring
への変換が不要になり、メモリ割り当てが削減されます。
また、readLineBytes
関数はreadLine
にリネームされ、bytes.TrimRight
の代わりにtrimTrailingWhitespace
という新しいヘルパー関数が導入されました。bytes.TrimRight
も内部で新しいバイトスライスを生成する可能性があるため、これも最適化の一環と考えられます。trimTrailingWhitespace
は、元のスライスを再スライスするだけで、新しいメモリ割り当てを避けています。
これらの変更は、net/http/chunked.go
とsrc/pkg/net/http/httputil/chunked.go
の両方に適用されており、HTTPクライアントとサーバーの両方でチャンク読み込みの効率が向上します。
テストコードでは、TestChunkReaderAllocs
が追加され、チャンクリーダーがチャンクデータを読み取る際のメモリ割り当て回数を検証しています。このテストは、チャンクリーダーが1回以下のメモリ割り当てで動作することを確認し、変更が意図した通りにガベージを削減していることを保証します。また、TestParseHexUint
が追加され、新しく導入されたparseHexUint
関数の正確性を検証しています。
コアとなるコードの変更箇所
以下のファイルが変更されています。
src/pkg/net/http/chunked.go
src/pkg/net/http/chunked_test.go
src/pkg/net/http/httputil/chunked.go
src/pkg/net/http/httputil/chunked_test.go
主な変更点は以下の通りです。
src/pkg/net/http/chunked.go
および src/pkg/net/http/httputil/chunked.go
:
chunkedReader.beginChunk()
内で、line
変数の型がstring
から[]byte
に変更されました。- var line string + var line []byte
strconv.ParseUint
の代わりに、新しく追加されたparseHexUint
関数が使用されるようになりました。- cr.n, cr.err = strconv.ParseUint(line, 16, 64) + cr.n, cr.err = parseHexUint(line)
readLineBytes
関数がreadLine
にリネームされました。-func readLineBytes(b *bufio.Reader) (p []byte, err error) { +func readLine(b *bufio.Reader) (p []byte, err error) {
readLine
(旧readLineBytes
) 関数内で、bytes.TrimRight
の代わりにtrimTrailingWhitespace
が使用されるようになりました。- p = bytes.TrimRight(p, " \r\t\n") + return trimTrailingWhitespace(p), nil
readLineBytes, but convert the bytes into a string.
というコメントと共に存在した、[]byte
をstring
に変換するreadLine
関数が削除されました。- 新しいヘルパー関数
trimTrailingWhitespace(b []byte) []byte
が追加されました。これは、バイトスライスの末尾から空白文字を削除します。func trimTrailingWhitespace(b []byte) []byte { for len(b) > 0 && isASCIISpace(b[len(b)-1]) { b = b[:len(b)-1] } return b }
- 新しいヘルパー関数
isASCIISpace(b byte) bool
が追加されました。これは、バイトがASCIIの空白文字であるかを判定します。func isASCIISpace(b byte) bool { return b == ' ' || b == '\t' || b == '\n' || b == '\r' }
- 新しいヘルパー関数
parseHexUint(v []byte) (n uint64, err error)
が追加されました。これは、バイトスライスとして与えられた16進数文字列をuint64
にパースします。func parseHexUint(v []byte) (n uint64, err error) { for _, b := range v { n <<= 4 switch { case '0' <= b && b <= '9': b = b - '0' case 'a' <= b && b <= 'f': b = b - 'a' + 10 case 'A' <= b && b <= 'F': b = b - 'A' + 10 default: return 0, errors.New("invalid byte in chunk length") } n |= uint64(b) } return }
src/pkg/net/http/chunked_test.go
および src/pkg/net/http/httputil/chunked_test.go
:
TestChunkReaderAllocs
という新しいテスト関数が追加されました。このテストは、チャンクリーダーがチャンクデータを読み取る際のメモリ割り当て回数を計測し、それが1回以下であることを検証します。TestParseHexUint
という新しいテスト関数が追加されました。このテストは、parseHexUint
関数の正確性を検証します。
コアとなるコードの解説
このコミットの核心は、チャンクサイズを読み取る際の[]byte
からstring
への変換を排除することによるメモリ割り当ての削減です。
変更前は、chunkedReader.beginChunk()
がreadLine
を呼び出し、その結果をstring
として受け取っていました。readLine
は内部でbufio.Reader.ReadSlice
を使ってバイトスライスを読み込み、それをstring()
に変換していました。このstring()
変換が、チャンクサイズを読み込むたびに新しいメモリをヒープに割り当てていました。
変更後、chunkedReader.beginChunk()
はreadLine
から直接[]byte
を受け取るようになりました。そして、この[]byte
を新しく追加されたparseHexUint
関数に渡して16進数をパースします。parseHexUint
は[]byte
を直接操作するため、string
への変換が不要になります。これにより、チャンクサイズを読み取る際のメモリ割り当てが完全に回避され、ガベージの生成が抑制されます。
また、readLine
(旧readLineBytes
)関数内で、行末の空白文字をトリムする処理も改善されました。以前はbytes.TrimRight
を使用していましたが、これも新しいバイトスライスを生成する可能性がありました。新しく導入されたtrimTrailingWhitespace
関数は、元のバイトスライスを再スライスするだけで、新しいメモリ割り当てを発生させません。
これらの変更により、HTTPチャンク読み込み処理における不要なメモリ割り当てが大幅に削減され、特に高負荷な環境や多数のHTTPリクエストを処理するアプリケーションにおいて、ガベージコレクションの頻度と負荷が軽減され、全体的なパフォーマンスが向上します。TestChunkReaderAllocs
の追加は、このメモリ割り当て削減の目標が達成されていることを自動的に検証するための重要なテストです。
関連リンク
- Go言語のHTTPパッケージ: https://pkg.go.dev/net/http
- Go言語の
bufio
パッケージ: https://pkg.go.dev/bufio - Go言語の
strconv
パッケージ: https://pkg.go.dev/strconv - HTTP/1.1 RFC 2616 - チャンク転送エンコーディング: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
参考にした情報源リンク
- Go Issue 2205 (string to []byte conversion optimization): https://github.com/golang/go/issues/2205
- Go CL 6850067 (Change List for this commit): https://golang.org/cl/6850067
- Go Memory Model: https://go.dev/ref/mem
- Go Slices: usage and internals: https://go.dev/blog/slices
- Go strings, bytes, runes and characters: https://go.dev/blog/strings
- Understanding Go Memory Management: https://www.ardanlabs.com/blog/2018/12/understanding-go-memory-management.html
- Go's work-stealing garbage collector: https://go.dev/blog/go15gc
- Go: The Good, Bad and Ugly Parts: https://www.youtube.com/watch?v=rFejpH_tMXw (Brad FitzpatrickによるGoのメモリに関する講演など)