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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージ内の src/pkg/net/http/request.go ファイルに対する変更です。このファイルは、HTTPリクエストの解析と表現に関するコアロジックを含んでおり、特にクライアントからのHTTPリクエストライン(例: GET /path HTTP/1.1)を読み込み、その構成要素(メソッド、URI、プロトコルバージョン)に分解する処理を担っています。

コミット

このコミットは、HTTPリクエストの「リクエストライン」の解析方法を改善し、リクエストごとのメモリ割り当てを削減することを目的としています。具体的には、既存の strings.SplitN 関数を用いた解析を、より効率的なカスタム関数 parseRequestLine に置き換えることで、パフォーマンスの向上とメモリ使用量の削減を実現しています。これにより、コードの可読性とテスト容易性も向上しています。

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

https://github.com/golang/go/commit/7e7f89933b9d6eba5d298d3f619b5ef4166e5052

元コミット内容

commit 7e7f89933b9d6eba5d298d3f619b5ef4166e5052
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Mar 28 14:19:51 2013 -0700

    net/http: parse Request-Line in a function, remove an allocation
    
    Removes another per-request allocation. Also makes the code more
    readable, IMO. And more testable.
    
    benchmark                                   old ns/op    new ns/op    delta
    BenchmarkServerFakeConnWithKeepAliveLite        10539        10324   -2.04%
    
    benchmark                                  old allocs   new allocs    delta
    BenchmarkServerFakeConnWithKeepAliveLite           20           19   -5.00%
    
    benchmark                                   old bytes    new bytes    delta
    BenchmarkServerFakeConnWithKeepAliveLite         1609         1559   -3.11%
    
    R=golang-dev, gri
    CC=golang-dev
    https://golang.org/cl/8118044

変更の背景

Go言語の net/http パッケージは、Webサーバーやクライアントを構築するための基盤であり、高いパフォーマンスと効率性が求められます。特にサーバーサイドでは、多数の同時リクエストを処理するため、リクエストごとのメモリ割り当て(アロケーション)を最小限に抑えることが非常に重要です。アロケーションが増えると、ガベージコレクション(GC)の頻度が増加し、アプリケーションのレイテンシやスループットに悪影響を与える可能性があります。

このコミットの背景には、HTTPリクエストラインの解析処理において、不要なメモリ割り当てが発生しているという課題がありました。以前の実装では、strings.SplitN 関数を使用してリクエストラインを空白文字で分割していましたが、この関数は新しい文字列スライスと、場合によっては新しい部分文字列を生成するため、リクエストごとに余分なメモリを割り当てていました。

このコミットは、この「リクエストごとのアロケーション」を削減し、net/http パッケージ全体の効率性をさらに向上させることを目的としています。また、解析ロジックを独立した関数に切り出すことで、コードのモジュール性、可読性、そして単体テストの容易性も改善しようとしています。

前提知識の解説

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

  1. HTTP/1.1 リクエストラインの構造: HTTP/1.1の仕様(RFC 2616, Section 5.1)では、リクエストラインは以下の形式で定義されています。 Method SP Request-URI SP HTTP-Version CRLF

    • Method: HTTPメソッド(例: GET, POST
    • SP: 単一のスペース文字
    • Request-URI: リクエストターゲットのURI(例: /index.html, /api/users?id=123
    • HTTP-Version: HTTPプロトコルバージョン(例: HTTP/1.1
    • CRLF: キャリッジリターンとラインフィード(改行)

    このコミットは、このリクエストラインを正確に3つの部分(Method, Request-URI, HTTP-Version)に分解する処理に焦点を当てています。

  2. Go言語におけるメモリ割り当てとガベージコレクション (GC): Goはガベージコレクタを持つ言語であり、開発者は明示的にメモリを解放する必要がありません。しかし、プログラムが頻繁に新しいオブジェクト(この場合は文字列やスライス)をヒープに割り当てると、GCがより頻繁に実行され、その結果、アプリケーションの実行が一時的に停止(ストップ・ザ・ワールド)し、パフォーマンスに影響を与える可能性があります。 特に、Webサーバーのような低レイテンシが求められるアプリケーションでは、アロケーションを減らすことがGCの負荷を軽減し、全体のスループットと応答性を向上させるための重要な最適化手法となります。

  3. Go言語の文字列操作とメモリ効率:

    • strings.SplitN(s, sep, n): この関数は、文字列 ssep で最大 n 回分割し、結果を文字列スライスとして返します。この際、分割された各部分文字列は、元の文字列のメモリ領域を共有するのではなく、新しいメモリ領域にコピーされる可能性があります(特にGo 1.2以降の最適化により、元の文字列がGCされるのを防ぐためにコピーされるケースが増えました)。また、結果を格納するための新しい文字列スライス自体もヒープに割り当てられます。これが、このコミットで削減対象となった「アロケーション」の主な原因です。
    • strings.Index(s, substr): この関数は、文字列 s 内で substr が最初に出現するインデックスを返します。この関数は新しい文字列やスライスを割り当てず、単にインデックス(整数)を返すだけです。
    • 文字列スライス: Goでは、s[start:end] のように文字列をスライスすると、新しい文字列が作成されるのではなく、元の文字列の基盤となるバイト配列への参照と、その範囲を示す情報を持つ新しい文字列ヘッダが作成されます。これにより、メモリのコピーを避けて効率的に部分文字列を扱うことができます。このコミットの変更は、この文字列スライスの特性を最大限に活用しています。
  4. Go言語のベンチマーク: Goには、コードのパフォーマンスを測定するための組み込みのベンチマークツールがあります。ベンチマーク結果は通常、以下の指標で表示されます。

    • ns/op (nanoseconds per operation): 1回の操作にかかる平均時間。値が小さいほど高速。
    • allocs/op (allocations per operation): 1回の操作で発生する平均メモリ割り当て回数。値が小さいほどメモリ効率が良い。
    • bytes/op (bytes per operation): 1回の操作で割り当てられる平均バイト数。値が小さいほどメモリ効率が良い。 このコミットのベンチマーク結果は、これらの指標が改善されたことを明確に示しています。

技術的詳細

このコミットの核心は、HTTPリクエストラインの解析において、strings.SplitN の使用を避け、より低レベルな strings.Index と文字列スライス操作に切り替えることで、メモリ割り当てを削減し、パフォーマンスを向上させる点にあります。

strings.SplitN の問題点: 元のコードでは、s := "GET /foo HTTP/1.1" のようなリクエストラインに対して strings.SplitN(s, " ", 3) を使用していました。 この関数は、以下の処理を行います。

  1. 空白文字 (" ") を区切り文字として文字列 s を最大3つの部分に分割します。
  2. 結果として []string{"GET", "/foo", "HTTP/1.1"} のような文字列スライスを返します。 この際、Goの内部実装では、この結果のスライス ([]string) 自体がヒープに割り当てられます。さらに、スライス内の各要素("GET", "/foo", "HTTP/1.1")も、元の文字列 s の基盤となるバイト配列から新しい文字列としてコピーされる可能性があります。特に、元の文字列 s がGCの対象となる可能性がある場合、部分文字列が元の文字列への参照を持ち続けることでGCを妨げないように、Goランタイムは部分文字列を新しいメモリ領域にコピーすることがあります。これらの要因が、リクエストごとの余分なアロケーションとバイト割り当てにつながっていました。

parseRequestLine 関数による最適化: 新しく導入された parseRequestLine 関数は、この問題を解決するために設計されました。

func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
    s1 := strings.Index(line, " ") // 最初の空白のインデックス
    s2 := strings.Index(line[s1+1:], " ") // 2番目の空白のインデックス (s1+1 から検索)
    if s1 < 0 || s2 < 0 { // 空白が見つからない場合は不正な形式
        return
    }
    s2 += s1 + 1 // 2番目の空白の絶対インデックスを計算
    return line[:s1], line[s1+1 : s2], line[s2+1:], true // 文字列スライスで部分文字列を抽出
}

この関数は以下の手順で解析を行います。

  1. s1 := strings.Index(line, " "): リクエストラインの最初の空白文字のインデックスを見つけます。これがメソッドとURIの区切りです。
  2. s2 := strings.Index(line[s1+1:], " "): 最初の空白の次(s1+1)から検索を開始し、2番目の空白文字のインデックスを見つけます。これがURIとプロトコルバージョンの区切りです。
  3. if s1 < 0 || s2 < 0: どちらかの空白が見つからない場合、リクエストラインが不正な形式であると判断し、ok=false で早期リターンします。
  4. s2 += s1 + 1: 2番目の strings.Indexline[s1+1:] という部分文字列に対する相対インデックスを返すため、元の line 文字列に対する絶対インデックスに変換します。
  5. return line[:s1], line[s1+1 : s2], line[s2+1:], true: ここが最も重要な部分です。Goの文字列スライス機能を利用して、元の line 文字列から直接、メソッド、URI、プロトコルバージョンを抽出します。この操作は、新しい文字列をコピーするのではなく、元の文字列の基盤となるバイト配列への参照と範囲情報を持つ新しい文字列ヘッダを作成するだけです。これにより、余分なメモリ割り当てが回避されます。

パフォーマンスの改善: コミットメッセージに記載されているベンチマーク結果は、この最適化の効果を明確に示しています。

  • BenchmarkServerFakeConnWithKeepAliveLite (1回の操作あたりの時間): 10539 ns/op から 10324 ns/op-2.04% 改善。
  • BenchmarkServerFakeConnWithKeepAliveLite (1回の操作あたりのアロケーション回数): 20 allocs から 19 allocs-5.00% 改善。
  • BenchmarkServerFakeConnWithKeepAliveLite (1回の操作あたりの割り当てバイト数): 1609 bytes から 1559 bytes-3.11% 改善。

これらの数値は、リクエストラインの解析処理がより高速になり、かつリクエストごとに割り当てられるメモリ量と回数が削減されたことを示しています。特にアロケーション回数の削減は、GCの頻度を減らし、全体的なサーバーの応答性とスループットの向上に寄与します。

コードの可読性とテスト容易性: 解析ロジックが parseRequestLine という独立した関数にカプセル化されたことで、ReadRequest 関数はより簡潔になり、その主要な責務(リクエスト全体の読み込みとパース)に集中できるようになりました。また、parseRequestLine は純粋な関数であり、外部の状態に依存しないため、単体テストが非常に容易になります。これにより、将来的なメンテナンスや機能拡張がしやすくなります。

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

src/pkg/net/http/request.go ファイルにおける変更は以下の通りです。

--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -467,6 +467,17 @@ func (r *Request) SetBasicAuth(username, password string) {
 	r.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(s)))
 }
 
+// parseRequestLine parses "GET /foo HTTP/1.1" into its three parts.
+func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
+	s1 := strings.Index(line, " ")
+	s2 := strings.Index(line[s1+1:], " ")
+	if s1 < 0 || s2 < 0 {
+		return
+	}
+	s2 += s1 + 1
+	return line[:s1], line[s1+1 : s2], line[s2+1:], true
+}
+
 // ReadRequest reads and parses a request from b.
 func ReadRequest(b *bufio.Reader) (req *Request, err error) {
 
@@ -484,13 +495,12 @@ func ReadRequest(b *bufio.Reader) (req *Request, err error) {
 		}
 	}()
 
-	var f []string
-	if f = strings.SplitN(s, " ", 3); len(f) < 3 {
+	var ok bool
+	req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
+	if !ok {
 		return nil, &badStringError{"malformed HTTP request", s}
 	}
-	req.Method, req.RequestURI, req.Proto = f[0], f[1], f[2]
 	rawurl := req.RequestURI
-	var ok bool
 	if req.ProtoMajor, req.ProtoMinor, ok = ParseHTTPVersion(req.Proto); !ok {
 		return nil, &badStringError{"malformed HTTP version", req.Proto}
 	}

コアとなるコードの解説

このコミットは、主に2つの部分で構成されています。

  1. 新しいヘルパー関数 parseRequestLine の追加:

    // parseRequestLine parses "GET /foo HTTP/1.1" into its three parts.
    func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
        s1 := strings.Index(line, " ")
        s2 := strings.Index(line[s1+1:], " ")
        if s1 < 0 || s2 < 0 {
            return
        }
        s2 += s1 + 1
        return line[:s1], line[s1+1 : s2], line[s2+1:], true
    }
    

    この関数は、HTTPリクエストライン文字列 line を受け取り、その中のメソッド、リクエストURI、プロトコルバージョンを抽出します。

    • s1 := strings.Index(line, " "): line の中で最初のスペース文字のインデックスを探します。このインデックスより前がHTTPメソッドです。
    • s2 := strings.Index(line[s1+1:], " "): 最初のスペースの直後 (line[s1+1:]) から、2番目のスペース文字のインデックスを探します。このインデックスは部分文字列 line[s1+1:] 内での相対的な位置です。
    • if s1 < 0 || s2 < 0: もしどちらかのスペースが見つからなかった場合(strings.Index は見つからないと -1 を返す)、リクエストラインが不正な形式であると判断し、okfalse のまま(Goのゼロ値)で関数を終了します。
    • s2 += s1 + 1: 2番目のスペースのインデックス s2 を、元の line 文字列に対する絶対インデックスに変換します。
    • return line[:s1], line[s1+1 : s2], line[s2+1:], true: ここがメモリ効率の鍵です。Goの文字列スライス機能を利用して、元の line 文字列から直接、メソッド (line[:s1])、リクエストURI (line[s1+1 : s2])、プロトコルバージョン (line[s2+1:]) を抽出します。この操作は新しいメモリ割り当てを伴わず、元の文字列の基盤となるバイト配列への参照を共有する新しい文字列ヘッダを作成するだけです。最後に true を返して、解析が成功したことを示します。
  2. ReadRequest 関数内の変更:

    @@ -484,13 +495,12 @@ func ReadRequest(b *bufio.Reader) (req *Request, err error) {
     		}
     	}()
     
    
  •   var f []string
    
  •   if f = strings.SplitN(s, " ", 3); len(f) < 3 {
    
  •   var ok bool
    
  •   req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
    
  •   if !ok {
      	return nil, &badStringError{"malformed HTTP request", s}
      }
    
  •   req.Method, req.RequestURI, req.Proto = f[0], f[1], f[2]
      rawurl := req.RequestURI
    
  •   var ok bool
      if req.ProtoMajor, req.ProtoMinor, ok = ParseHTTPVersion(req.Proto); !ok {
      	return nil, &badStringError{"malformed HTTP version", req.Proto}
      }
    
    `ReadRequest` 関数は、HTTPリクエスト全体を読み込み、解析する主要な関数です。この変更により、リクエストラインの解析部分が以下のように変わりました。
    *   **削除されたコード**:
        ```go
        var f []string
        if f = strings.SplitN(s, " ", 3); len(f) < 3 {
            return nil, &badStringError{"malformed HTTP request", s}
        }
        req.Method, req.RequestURI, req.Proto = f[0], f[1], f[2]
        ```
        以前は `strings.SplitN` を使用してリクエストラインを分割し、その結果を `f` という文字列スライスに格納していました。この `f` スライスとその要素の生成がメモリ割り当ての原因でした。
    *   **追加されたコード**:
        ```go
        var ok bool
        req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
        if !ok {
            return nil, &badStringError{"malformed HTTP request", s}
        }
        ```
        新しく追加された `parseRequestLine` 関数を呼び出し、その戻り値(メソッド、URI、プロトコルバージョン、および成功を示す `ok` フラグ)を直接 `req` オブジェクトのフィールドに代入しています。`parseRequestLine` が `ok=false` を返した場合、リクエストラインが不正であるとしてエラーを返します。
    
    

この変更により、ReadRequest 関数はリクエストラインの解析の詳細から解放され、よりクリーンで効率的なコードになりました。

関連リンク

  • Go言語の net/http パッケージドキュメント: https://pkg.go.dev/net/http
  • Go言語の strings パッケージドキュメント: https://pkg.go.dev/strings
  • Go言語のコードレビューシステム (Gerrit): このコミットは、GoのGerritシステムで https://golang.org/cl/8118044 としてレビューされました。Gerritは、Goプロジェクトがコード変更を管理するために使用するツールです。

参考にした情報源リンク