[インデックス 17337] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http パッケージにおける ResponseWriter の機能拡張と最適化に関するものです。具体的には、http.ResponseWriter インターフェースを実装する response 型に WriteString メソッドを追加し、文字列データを直接書き込む際のメモリ割り当てを削減することを目的としています。
コミット
commit 33d531dfa49b7477dd51b54b870508b8b7eafee2
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Aug 19 22:56:54 2013 -0700
net/http: support WriteString on the ResponseWriter
Fixes #5377
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/12991046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/33d531dfa49b7477dd51b54b870508b8b7eafee2
元コミット内容
net/http: support WriteString on the ResponseWriter
このコミットは、net/http パッケージの ResponseWriter が WriteString メソッドをサポートするように変更します。これにより、文字列をHTTPレスポンスボディに書き込む際のパフォーマンスが向上します。
変更の背景
Goの io.Writer インターフェースは Write([]byte) メソッドのみを定義しています。しかし、多くの io.Writer の実装(特に bufio.Writer のようなバッファリングを行うライター)は、内部的に WriteString(string) メソッドも持っており、文字列を直接書き込む方が []byte に変換してから書き込むよりも効率的である場合があります。
従来の http.ResponseWriter は、文字列を書き込む際に []byte(string) のようにバイトスライスに変換してから Write メソッドを呼び出していました。この変換は、特に頻繁に小さな文字列を書き込む場合に、不要なメモリ割り当て(ヒープアロケーション)を引き起こし、ガベージコレクションの負荷を増大させる可能性がありました。
この問題は、GoのIssue #5377 で報告されており、WriteString メソッドの追加によってこの非効率性を解消することが提案されていました。このコミットは、その提案に対応し、net/http パッケージのパフォーマンスを向上させることを目的としています。
前提知識の解説
http.ResponseWriter: Goのnet/httpパッケージにおけるHTTPレスポンスを書き込むためのインターフェースです。通常、HTTPハンドラ関数はこれを通じてクライアントにデータを送信します。このインターフェースはio.Writerを埋め込んでいるため、Write([]byte)メソッドを持っています。io.Writer: データを書き込むための基本的なインターフェースで、Write([]byte) (n int, err error)メソッドを定義しています。io.StringWriter: Go 1.15で導入されたインターフェースで、WriteString(s string) (n int, err error)メソッドを定義しています。このコミットが作成された2013年時点ではまだ存在していませんでしたが、このコミットの変更は、後のio.StringWriterの概念に先行するものです。- メモリ割り当て (Allocation): プログラムが実行時にメモリを確保することです。Goでは、ヒープにメモリを割り当てるとガベージコレクタがそのメモリを管理する必要があり、頻繁な割り当てはパフォーマンスに影響を与える可能性があります。
[]byte(string)変換: Goでは、文字列をバイトスライスに変換する際に、新しいバイトスライスが作成され、文字列の内容がそこにコピーされます。これはメモリ割り当てを伴います。bufio.Writer: バッファリングされたI/Oを提供するライターです。通常、io.Writerをラップして、小さな書き込みをまとめて効率化します。bufio.Writerは内部的にWriteStringメソッドを持っており、これを利用することで文字列のコピーを避けることができます。
技術的詳細
このコミットの主要な技術的変更点は以下の通りです。
-
response.WriteStringメソッドの追加:src/pkg/net/http/server.go内のresponse型にWriteString(data string) (n int, err error)メソッドが追加されました。このメソッドは、文字列データを直接受け取り、内部のwriteヘルパー関数を呼び出します。 -
response.writeヘルパー関数の導入とリファクタリング: 既存のresponse.Write(data []byte)メソッドと新しく追加されたresponse.WriteString(data string)メソッドの両方から呼び出される共通のヘルパー関数write(lenData int, dataB []byte, dataS string) (n int, err error)が導入されました。 このwrite関数は、dataB(バイトスライス) またはdataS(文字列) のいずれか一方のみが非ゼロであることを想定しています。write関数内で、実際の書き込み処理は、dataBが存在する場合はw.w.Write(dataB)を呼び出し、dataSが存在する場合はw.w.WriteString(dataS)を呼び出すように分岐しています。ここでw.wは、response型がラップしている実際のio.Writer(通常はbufio.Writerなど) を指します。 -
メモリ割り当ての最適化:
response.WriteStringが呼び出された場合、文字列データはバイトスライスに変換されることなく、直接下層のio.WriterのWriteStringメソッドに渡されます。これにより、不要なバイトスライスの割り当てとコピーが回避され、ガベージコレクションの負担が軽減されます。 -
テストケースの追加:
src/pkg/net/http/serve_test.goにTestResponseWriterWriteStringAllocsという新しいテスト関数が追加されました。このテストは、WriteStringを使用した場合のメモリ割り当てが、Write([]byte("..."))を使用した場合よりも少ないことをtesting.AllocsPerRunを用いて検証します。これは、この変更が意図したパフォーマンス向上をもたらすことを確認するための重要なテストです。
コアとなるコードの変更箇所
src/pkg/net/http/server.go
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -953,6 +953,15 @@ func (w *response) bodyAllowed() bool {
// bufferBeforeChunkingSize smaller and having bufio's fast-paths deal
// with this instead.
func (w *response) Write(data []byte) (n int, err error) {
+ return w.write(len(data), data, "")
+}
+
+func (w *response) WriteString(data string) (n int, err error) {
+ return w.write(len(data), nil, data)
+}
+
+// either dataB or dataS is non-zero.
+func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
if w.conn.hijacked() {
log.Print("http: response.Write on hijacked connection")
return 0, ErrHijacked
@@ -960,18 +969,22 @@ func (w *response) Write(data []byte) (n int, err error) {
if !w.wroteHeader {
w.WriteHeader(StatusOK)
}
- if len(data) == 0 {
+ if lenData == 0 {
return 0, nil
}
if !w.bodyAllowed() {
return 0, ErrBodyNotAllowed
}
- w.written += int64(len(data)) // ignoring errors, for errorKludge
+ w.written += int64(lenData) // ignoring errors, for errorKludge
if w.contentLength != -1 && w.written > w.contentLength {
return 0, ErrContentLength
}
- return w.w.Write(data)
+ if dataB != nil {
+ return w.w.Write(dataB)
+ } else {
+ return w.w.WriteString(dataS)
+ }
}
func (w *response) finishRequest() {
src/pkg/net/http/serve_test.go
--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -1994,6 +1994,21 @@ func TestNoContentTypeOnNotModified(t *testing.T) {
}
}
+func TestResponseWriterWriteStringAllocs(t *testing.T) {
+ ht := newHandlerTest(HandlerFunc(func(w ResponseWriter, r *Request) {
+ if r.URL.Path == "/s" {
+ io.WriteString(w, "Hello world")
+ } else {
+ w.Write([]byte("Hello world"))
+ }
+ }))
+ before := testing.AllocsPerRun(25, func() { ht.rawResponse("GET / HTTP/1.0") })
+ after := testing.AllocsPerRun(25, func() { ht.rawResponse("GET /s HTTP/1.0") })
+ if int(after) >= int(before) {
+ t.Errorf("WriteString allocs of %v >= Write allocs of %v", after, before)
+ }
+}
+
func BenchmarkClientServer(b *testing.B) {
b.ReportAllocs()
b.StopTimer()
コアとなるコードの解説
src/pkg/net/http/server.go の変更は、response 型に WriteString メソッドを追加し、既存の Write メソッドと共通の write ヘルパー関数に処理を委譲することで、コードの重複を避けつつ最適化を実現しています。
-
func (w *response) Write(data []byte) (n int, err error): このメソッドは、これまでバイトスライスを受け取って直接書き込みを行っていましたが、変更後はw.write(len(data), data, "")を呼び出すように変更されました。これにより、実際の書き込みロジックがwriteヘルパー関数に集約されます。 -
func (w *response) WriteString(data string) (n int, err error): 新しく追加されたメソッドです。文字列を受け取り、w.write(len(data), nil, data)を呼び出します。ここでnilがdataBに渡されることで、write関数内で文字列パスが選択されるようになります。 -
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error): このヘルパー関数が、実際の書き込み処理の大部分を担います。lenData: 書き込むデータの長さ。dataBまたはdataSのどちらが使われるかにかかわらず、データの長さを一元的に扱います。dataB: バイトスライスデータ。Writeメソッドから呼び出された場合に設定されます。dataS: 文字列データ。WriteStringメソッドから呼び出された場合に設定されます。if dataB != nil { return w.w.Write(dataB) } else { return w.w.WriteString(dataS) }: この条件分岐が最適化の核心です。dataBがnilでない(つまりWrite([]byte)が呼び出された)場合は、下層のio.WriterのWriteメソッドを呼び出します。そうでない場合(つまりWriteString(string)が呼び出された)は、下層のio.WriterのWriteStringメソッドを呼び出します。これにより、下層のライターがWriteStringをサポートしていれば、文字列からバイトスライスへの不要な変換を避けることができます。
src/pkg/net/http/serve_test.go の TestResponseWriterWriteStringAllocs テストは、この最適化が実際に機能していることを確認します。
testing.AllocsPerRunを使用して、特定の操作が実行される際のメモリ割り当ての回数を測定します。/パスへのリクエストではw.Write([]byte("Hello world"))を使用し、/sパスへのリクエストではio.WriteString(w, "Hello world")を使用します。- テストの目的は、
io.WriteStringを使用した場合(つまりresponse.WriteStringが呼び出されるパス)の割り当て回数 (after) が、w.Write([]byte(...))を使用した場合 (before) よりも少ないことを検証することです。これにより、WriteStringの導入がメモリ効率の向上に寄与していることが証明されます。
関連リンク
- Go Issue #5377:
net/http: ResponseWriter should implement io.StringWriter- このコミットの背景となったIssueです。 https://github.com/golang/go/issues/5377 - Go CL 12991046:
net/http: support WriteString on the ResponseWriter- このコミットに対応するGoのコードレビューシステム (Gerrit) のチェンジリストです。 https://golang.org/cl/12991046
参考にした情報源リンク
- Goの
ioパッケージのドキュメント:io.Writerやio.StringWriterについての公式情報。 https://pkg.go.dev/io - Goの
net/httpパッケージのドキュメント:http.ResponseWriterについての公式情報。 https://pkg.go.dev/net/http testing.AllocsPerRunのドキュメント: Goのベンチマークテストにおけるメモリ割り当て測定について。 https://pkg.go.dev/testing#AllocsPerRun- Goの文字列とバイトスライスの変換に関する情報: https://go.dev/blog/strings (Goの文字列に関するブログ記事) https://go.dev/blog/slices (Goのスライスに関するブログ記事)
- Goのガベージコレクションに関する情報: https://go.dev/doc/gc-guide (GoのGCガイド)