[インデックス 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ガイド)