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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるHTTPサーバーのパフォーマンス改善を目的としています。具体的には、HTTPレスポンスヘッダーの Date および Content-Length の生成時に発生するメモリ割り当てを削減することで、サーバーパスの効率化を図っています。これにより、全体的な処理速度の向上とメモリ使用量の削減が実現されています。

コミット

commit d4cbc80d106a3f3b53631aa60b400c790b14bb52
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Sun May 19 20:15:40 2013 -0700

    net/http: fewer allocations in the server path
    
    Don't allocate for the Date or Content-Length headers.
    A custom Date header formatter replaces use of time.Format.
    
    benchmark                                   old ns/op    new ns/op    delta
    BenchmarkClientServer                           67791        64424   -4.97%
    BenchmarkClientServerParallel4                  62956        58533   -7.03%
    BenchmarkClientServerParallel64                 62043        54789  -11.69%
    BenchmarkServer                                254609       229060  -10.03%
    BenchmarkServerFakeConnNoKeepAlive              17038        16316   -4.24%
    BenchmarkServerFakeConnWithKeepAlive            14184        13226   -6.75%
    BenchmarkServerFakeConnWithKeepAliveLite         8591         7532  -12.33%
    BenchmarkServerHandlerTypeLen                   10750         9961   -7.34%
    BenchmarkServerHandlerNoLen                      9535         8935   -6.29%
    BenchmarkServerHandlerNoType                     9858         9362   -5.03%
    BenchmarkServerHandlerNoHeader                   7754         6856  -11.58%
    
    benchmark                                  old allocs   new allocs    delta
    BenchmarkClientServer                              68           66   -2.94%
    BenchmarkClientServerParallel4                     68           66   -2.94%
    BenchmarkClientServerParallel64                    68           66   -2.94%
    BenchmarkServer                                    21           19   -9.52%
    BenchmarkServerFakeConnNoKeepAlive                 32           30   -6.25%
    BenchmarkServerFakeConnWithKeepAlive               27           25   -7.41%
    BenchmarkServerFakeConnWithKeepAliveLite           12           10  -16.67%
    BenchmarkServerHandlerTypeLen                      19           18   -5.26%
    BenchmarkServerHandlerNoLen                        17           15  -11.76%
    BenchmarkServerHandlerNoType                       17           16   -5.88%
    BenchmarkServerHandlerNoHeader                     12           10  -16.67%
    
    Update #5195
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/9432046

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

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

元コミット内容

このコミットの目的は、Goの net/http パッケージにおけるサーバーパスでのメモリ割り当てを削減することです。特に、HTTPレスポンスヘッダーの DateContent-Length の生成時に発生する不要なメモリ割り当てを排除することに焦点を当てています。time.Format の使用をカスタムの Date ヘッダーフォーマッタに置き換えることで、この最適化を実現しています。

ベンチマーク結果は以下の通り、ns/op (ナノ秒/操作) と allocs (メモリ割り当て数) の両方で顕著な改善を示しています。

処理速度 (ns/op) の改善:

  • BenchmarkClientServer: 約 5% 改善
  • BenchmarkClientServerParallel64: 約 11.7% 改善
  • BenchmarkServer: 約 10% 改善
  • BenchmarkServerFakeConnWithKeepAliveLite: 約 12.3% 改善

メモリ割り当て数 (allocs) の改善:

  • BenchmarkClientServer: 約 3% 改善
  • BenchmarkServer: 約 9.5% 改善
  • BenchmarkServerFakeConnWithKeepAliveLite: 約 16.7% 改善
  • BenchmarkServerHandlerNoHeader: 約 16.7% 改善

これらの結果は、本コミットがHTTPサーバーのパフォーマンスと効率性に大きな影響を与えることを示しています。

変更の背景

Goの net/http パッケージは、Go言語でWebアプリケーションを構築する際の基盤となる重要なコンポーネントです。Webサーバーは、大量のリクエストを同時に処理することが求められるため、パフォーマンスと効率性が非常に重要になります。特に、HTTPレスポンスヘッダーの生成は、すべてのリクエストに対して行われるため、ここでのわずかな非効率性も、高負荷時には大きなボトルネックとなり得ます。

従来の net/http サーバーでは、Date ヘッダーの生成に time.Format 関数が使用されていました。time.Format は柔軟な日付フォーマット機能を提供しますが、その内部で文字列の割り当て(ヒープ割り当て)が発生します。同様に、Content-Length ヘッダーも、整数値を文字列に変換する際に割り当てが発生する可能性がありました。これらの割り当ては、リクエストごとに繰り返されるため、特に多数の同時接続を処理するサーバーでは、ガベージコレクションの頻度を増加させ、CPUサイクルを消費し、結果として全体的なスループットを低下させる要因となっていました。

このコミットの背景には、Goの標準ライブラリが提供するHTTPサーバーの性能をさらに向上させ、より高効率なWebサービスを構築できるようにするという明確な目標がありました。メモリ割り当ての削減は、ガベージコレクションの負荷を軽減し、CPUキャッシュの効率を高めることで、サーバーの応答性とスループットを向上させるための一般的な最適化手法です。

前提知識の解説

このコミットの変更内容を理解するためには、以下の前提知識が役立ちます。

  1. HTTPヘッダー: HTTPプロトコルにおいて、クライアントとサーバー間で送受信されるメッセージのメタデータを提供する部分です。Date ヘッダーはメッセージが生成された日時を示し、Content-Length ヘッダーはメッセージボディのバイト数を示します。これらはHTTP通信において非常に頻繁に使用されます。

  2. Go言語のメモリ割り当てとガベージコレクション (GC):

    • メモリ割り当て (Allocation): Goプログラムが実行時にメモリを要求し、使用可能なメモリ領域を確保するプロセスです。Goでは、主にスタックとヒープの2種類のメモリ領域があります。
      • スタック: 関数呼び出しやローカル変数など、生存期間が短いデータが格納されます。コンパイル時にサイズが決定され、高速にアクセスできます。割り当てと解放は自動的に行われます。
      • ヒープ: プログラムの実行中に動的にサイズが変化するデータ(例: スライス、マップ、チャネル、インターフェース、大きな構造体など)が格納されます。ヒープへの割り当てはスタックよりもコストが高く、ガベージコレクションの対象となります。
    • ガベージコレクション (GC): ヒープに割り当てられたメモリのうち、もはやプログラムから参照されなくなった(不要になった)メモリ領域を自動的に解放し、再利用可能にするプロセスです。GoのGCは並行的に動作しますが、GCが実行されると少なからずCPUリソースを消費し、プログラムの実行を一時的に停止させる(ストップ・ザ・ワールド)可能性があります。GCの頻度や実行時間は、ヒープ割り当ての量に直接影響されます。割り当てが多ければ多いほど、GCの頻度が増え、パフォーマンスに悪影響を与える可能性があります。
  3. time.Format の特性: Goの time パッケージの Format メソッドは、time.Time オブジェクトを指定されたレイアウト文字列に基づいてフォーマットし、新しい文字列を返します。この新しい文字列はヒープに割り当てられるため、頻繁に呼び出されるとメモリ割り当てが蓄積され、GCの負荷が増大します。

  4. strconv パッケージ: Goの標準ライブラリで、プリミティブ型(整数、浮動小数点数、ブール値など)と文字列との間の変換を提供します。strconv.Itoa は整数を文字列に変換しますが、これも新しい文字列を割り当てます。strconv.AppendInt のように、既存のバイトスライスに追記する形式の関数は、不要な割り当てを避けるのに役立ちます。

  5. []byte と文字列 (string): Goでは、文字列は不変のバイトスライスとして扱われます。文字列操作は新しい文字列の割り当てを伴うことが多いですが、[]byte を直接操作することで、メモリ割り当てをより細かく制御し、既存のバッファを再利用することが可能になります。

  6. bufio.Writer: バッファリングされたI/O操作を提供し、書き込み操作の回数を減らすことでパフォーマンスを向上させます。小さな書き込みをまとめて一度に基になる io.Writer に書き込むことで、システムコールなどのオーバーヘッドを削減します。

これらの知識を背景に、本コミットがどのようにしてメモリ割り当てを削減し、パフォーマンスを向上させているかを深く理解することができます。

技術的詳細

このコミットの技術的詳細な変更点は、主に src/pkg/net/http/server.go ファイルに集中しており、HTTPレスポンスヘッダーの DateContent-Length の生成と書き込み方法を最適化しています。

  1. response 構造体へのバッファ追加:

    • response 構造体(HTTPレスポンスを構築するための内部構造体)に、Date ヘッダーと Content-Length ヘッダーの値を格納するための固定サイズのバイト配列が追加されました。
      type response struct {
          // ...
          // Buffers for Date and Content-Length
          dateBuf [len(TimeFormat)]byte
          clenBuf [10]byte
      }
      
    • dateBufTimeFormat (例: "Mon, 02 Jan 2006 15:04:05 GMT") の長さに合わせて確保され、clenBuf は最大10桁のContent-Length (約10GBまで) を格納できるように確保されています。これにより、これらのヘッダー値を文字列としてヒープに割り当てる必要がなくなり、スタック上に直接バッファを確保して再利用できるようになります。
  2. カスタム appendTime 関数の導入:

    • Date ヘッダーのフォーマットのために、time.Now().UTC().Format(TimeFormat) の代わりに、新しい appendTime 関数が導入されました。
      func appendTime(b []byte, t time.Time) []byte {
          // ... 手動での日付フォーマットロジック ...
          return append(b, /* ... */)
      }
      
    • この関数は、time.Time オブジェクトから年、月、日、時、分、秒、曜日などを抽出し、それらを直接バイトスライス b に追記することで、time.Format が内部で行う文字列割り当てを完全に回避します。append 関数は、必要に応じてスライスの容量を拡張しますが、dateBuf[:0] のように既存のバッファを再利用することで、ほとんどの場合で新たな割り当てを発生させません。
  3. extraHeader 構造体の変更:

    • HTTPレスポンスの追加ヘッダーを管理する extraHeader 構造体において、datecontentLength フィールドの型が string から []byte に変更されました。
      type extraHeader struct {
          // ...
          date          []byte // written if not nil
          contentLength []byte // written if not nil
      }
      
    • これにより、ヘッダー値を文字列として保持する際の割り当てが不要になり、直接バイトスライスとして扱うことで、メモリ効率が向上します。
  4. extraHeader.Write メソッドの最適化:

    • extraHeader の内容を io.Writer に書き込む Write メソッドが変更されました。
      • 変更前は、DateContent-Length を含むすべてのヘッダーが []string のループで処理され、io.WriteString を使用していました。
      • 変更後は、DateContent-Length は個別に、かつ []byte 型のフィールドとして直接 w.Write で書き込まれるようになりました。
        func (h extraHeader) Write(w *bufio.Writer) { // w の型が io.Writer から *bufio.Writer に変更
            if h.date != nil {
                w.Write(headerDate) // headerDate は []byte("Date: ")
                w.Write(h.date)
                w.Write(crlf)
            }
            if h.contentLength != nil {
                w.Write(headerContentLength) // headerContentLength は []byte("Content-Length: ")
                w.Write(h.contentLength)
                w.Write(crlf)
            }
            // その他のヘッダーはループで処理
            for i, v := range []string{h.contentType, h.connection, h.transferEncoding} {
                if v != "" {
                    w.Write(extraHeaderKeys[i])
                    w.Write(colonSpace)
                    w.WriteString(v) // io.WriteString から w.WriteString に変更
                    w.Write(crlf)
                }
            }
        }
        
    • w の型が io.Writer から *bufio.Writer に変更されたことで、WriteString メソッドが *bufio.Writer のメソッドとして直接呼び出せるようになり、インターフェース呼び出しによるオーバーヘッドが削減されます。
    • headerDateheaderContentLength のようなヘッダー名とコロン・スペースの組み合わせも []byte 定数として定義され、再利用されることで、ここでの割り当ても回避されています。
  5. ヘッダー値の設定方法の変更:

    • Content-Length の設定:
      // 変更前: setHeader.contentLength = strconv.Itoa(len(p))
      // 変更後: setHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10)
      
      strconv.Itoa は新しい文字列を割り当てますが、strconv.AppendInt は既存のバイトスライス (cw.res.clenBuf[:0]) に整数値を追記するため、割り当てを回避できます。
    • Date の設定:
      // 変更前: setHeader.date = time.Now().UTC().Format(TimeFormat)
      // 変更後: setHeader.date = appendTime(cw.res.dateBuf[:0], time.Now())
      
      前述のカスタム appendTime 関数を使用することで、time.Format による割り当てを排除しています。

これらの変更は、HTTPレスポンスヘッダーの生成パスにおけるヒープ割り当てを徹底的に排除し、サーバーのパフォーマンスを向上させることを目的としています。特に、リクエストごとに繰り返し実行される処理であるため、これらの最適化は高負荷環境下でのスループットとレイテンシに大きな影響を与えます。

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

このコミットのコアとなるコードの変更は、主に src/pkg/net/http/server.go にあります。

  1. response 構造体へのバッファ追加:

    --- a/src/pkg/net/http/server.go
    +++ b/src/pkg/net/http/server.go
    @@ -320,6 +320,10 @@ type response struct {
     	requestBodyLimitHit bool
     
     	handlerDone bool // set true when the handler exits
    +
    +	// Buffers for Date and Content-Length
    +	dateBuf [len(TimeFormat)]byte
    +	clenBuf [10]byte
     }
    
  2. カスタム appendTime 関数の追加:

    --- a/src/pkg/net/http/server.go
    +++ b/src/pkg/net/http/server.go
    @@ -525,6 +529,27 @@ func (ecr *expectContinueReader) Close() error {
     // It is like time.RFC1123 but hard codes GMT as the time zone.
     const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
     
    +// appendTime is a non-allocating version of []byte(time.Now().UTC().Format(TimeFormat))
    +func appendTime(b []byte, t time.Time) []byte {
    +	const days = "SunMonTueWedThuFriSat"
    +	const months = "JanFebMarAprMayJunJulAugSepOctNovDec"
    +
    +	yy, mm, dd := t.Date()
    +	hh, mn, ss := t.Clock()
    +	day := days[3*t.Weekday():]
    +	mon := months[3*(mm-1):]
    +
    +	return append(b,
    +		day[0], day[1], day[2], ',', ' ',
    +		byte('0'+dd/10), byte('0'+dd%10), ' ',
    +		mon[0], mon[1], mon[2], ' ',
    +		byte('0'+yy/1000), byte('0'+(yy/100)%10), byte('0'+(yy/10)%10), byte('0'+yy%10), ' ',
    +		byte('0'+hh/10), byte('0'+hh%10), ':',
    +		byte('0'+mn/10), byte('0'+mn%10), ':',
    +		byte('0'+ss/10), byte('0'+ss%10), ' ',
    +		'G', 'M', 'T')
    +}
    +
     var errTooLarge = errors.New("http: request too large")
    
  3. extraHeader 構造体のフィールド型変更と Write メソッドの最適化:

    --- a/src/pkg/net/http/server.go
    +++ b/src/pkg/net/http/server.go
    @@ -620,27 +645,45 @@ func (w *response) WriteHeader(code int) {
     // the response Header map and all its 1-element slices.
     type extraHeader struct {
     	contentType      string
    -	contentLength    string
      	connection       string
    -	date             string
      	transferEncoding string
    +	date             []byte // written if not nil
    +	contentLength    []byte // written if not nil
     }
     
     // Sorted the same as extraHeader.Write's loop.
     var extraHeaderKeys = [][]byte{
    -	[]byte("Content-Type"), []byte("Content-Length"),
    -	[]byte("Connection"), []byte("Date"), []byte("Transfer-Encoding"),
    +	[]byte("Content-Type"),
    +	[]byte("Connection"),
    +	[]byte("Transfer-Encoding"),
     }
     
    -// The value receiver, despite copying 5 strings to the stack,
    -// prevents an extra allocation. The escape analysis isn't smart
    -// enough to realize this doesn't mutate h.
    -func (h extraHeader) Write(w io.Writer) {
    -	for i, v := range []string{h.contentType, h.contentLength, h.connection, h.date, h.transferEncoding} {
    +var (
    +	headerContentLength = []byte("Content-Length: ")
    +	headerDate          = []byte("Date: ")
    +)
    +
    +// Write writes the headers described in h to w.
    +//
    +// This method has a value receiver, despite the somewhat large size
    +// of h, because it prevents an allocation. The escape analysis isn't
    +// smart enough to realize this function doesn't mutate h.
    +func (h extraHeader) Write(w *bufio.Writer) {
    +	if h.date != nil {
    +		w.Write(headerDate)
    +		w.Write(h.date)
    +		w.Write(crlf)
    +	}
    +	if h.contentLength != nil {
    +		w.Write(headerContentLength)
    +		w.Write(h.contentLength)
    +		w.Write(crlf)
    +	}
    +	for i, v := range []string{h.contentType, h.connection, h.transferEncoding} {
      		if v != "" {
      			w.Write(extraHeaderKeys[i])
      			w.Write(colonSpace)
    -			io.WriteString(w, v)
    +			w.WriteString(v)
      			w.Write(crlf)
      		}
      	}
    
  4. Content-LengthDate ヘッダーの設定ロジックの変更:

    --- a/src/pkg/net/http/server.go
    +++ b/src/pkg/net/http/server.go
    @@ -694,7 +737,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
      	// "keep-alive" connections alive.
      	if w.handlerDone && header.get("Content-Length") == "" && w.req.Method != "HEAD" {
      		w.contentLength = int64(len(p))
    -		setHeader.contentLength = strconv.Itoa(len(p))
    +		setHeader.contentLength = strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10)
      	}
      
      	// If this was an HTTP/1.0 request with keep-alive and we sent a
    @@ -755,7 +798,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
      	}
      
      	if _, ok := header["Date"]; !ok {
    -		setHeader.date = time.Now().UTC().Format(TimeFormat)
    +		setHeader.date = appendTime(cw.res.dateBuf[:0], time.Now())
      	}
      
      	te := header.get("Transfer-Encoding")
    @@ -806,7 +849,7 @@ func (cw *chunkWriter) writeHeader(p []byte) {
      
      	io.WriteString(w.conn.buf, statusLine(w.req, code))
      	cw.header.WriteSubset(w.conn.buf, excludeHeader)
    -	setHeader.Write(w.conn.buf)
    +	setHeader.Write(w.conn.buf.Writer)
      	w.conn.buf.Write(crlf)
      }
    

コアとなるコードの解説

このコミットの核心は、HTTPレスポンスヘッダー、特に DateContent-Length の生成と書き込みにおけるメモリ割り当てを排除することです。

  1. response 構造体内の固定バッファ (dateBuf, clenBuf):

    • response 構造体は、各HTTPリクエストの処理中に生成されるレスポンスの状態を保持します。ここに dateBufclenBuf という固定サイズのバイト配列が追加されたことで、DateContent-Length ヘッダーの値を文字列としてヒープに割り当てる必要がなくなりました。
    • これらのバッファはスタック上に確保されるか、または response 構造体自体がヒープに割り当てられる場合でも、その内部に埋め込まれるため、ヘッダー値ごとに個別の割り当てが発生するのを防ぎます。これにより、ガベージコレクションの対象となるオブジェクトの数が減少し、GCの頻度と負荷が軽減されます。
  2. appendTime 関数の役割:

    • Goの time.Format 関数は非常に便利ですが、内部で新しい文字列を生成するため、ヒープ割り当てが発生します。HTTPサーバーのように Date ヘッダーを頻繁に生成する場面では、これがパフォーマンスのボトルネックになり得ます。
    • appendTime 関数は、time.Time オブジェクトから日付と時刻の各要素(年、月、日、時、分、秒、曜日)を抽出し、それらを直接バイトスライスに変換して既存のバッファ (dateBuf) に追記します。この手動でのフォーマットにより、time.Format が引き起こす文字列割り当てを完全に回避し、ゼロアロケーションで Date ヘッダーを生成できるようになります。
  3. extraHeader[]byte 化と Write メソッドの最適化:

    • extraHeader 構造体は、HTTPレスポンスに含める追加のヘッダーを効率的に管理するためのものです。datecontentLength フィールドが string から []byte に変更されたことで、ヘッダー値を文字列として保持する際の割り当てが不要になりました。
    • extraHeader.Write メソッドでは、DateContent-Length ヘッダーが、事前に定義されたバイトスライス (headerDate, headerContentLength) と、response 構造体内のバッファ (h.date, h.contentLength) を直接 w.Write で書き込むように変更されました。これにより、文字列からバイトスライスへの変換や、io.WriteString のようなヘルパー関数を介した間接的な書き込みが不要になり、より直接的かつ効率的なバイト列の書き込みが可能になります。
    • また、w の型が io.Writer から *bufio.Writer に変更されたことで、WriteString メソッドが *bufio.Writer の具体的なメソッドとして呼び出されるようになり、インターフェース呼び出しによるわずかなオーバーヘッドも削減されています。
  4. strconv.AppendInt の活用:

    • Content-Length ヘッダーの値を設定する際に、strconv.Itoa (整数を文字列に変換し、新しい文字列を割り当てる) の代わりに strconv.AppendInt が使用されるようになりました。
    • strconv.AppendInt(cw.res.clenBuf[:0], int64(len(p)), 10) は、response 構造体内の clenBuf バッファを再利用し、そこに Content-Length の整数値を10進数文字列として追記します。これにより、Content-Length ヘッダーの生成時にもヒープ割り当てが発生しなくなります。

これらの変更は、GoのHTTPサーバーが各リクエストを処理する際のメモリフットプリントを最小限に抑え、ガベージコレクションの負担を軽減することで、全体的なスループットと応答性を向上させることに貢献しています。これは、Go言語の「ゼロアロケーション」または「低アロケーション」プログラミングの原則を実践した良い例であり、高性能なシステムを構築する上で重要な最適化手法です。

関連リンク

参考にした情報源リンク