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

[インデックス 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ヘッダを事前に知る必要がなくなり、動的なコンテンツ生成やストリーミングが可能になります。

チャンク形式のメッセージボディは以下の構造を持ちます。

  1. チャンクサイズ: 16進数で表現されたチャンクのバイト数。その後にCRLF(\r\n)が続く。
  2. チャンクデータ: チャンクサイズで指定されたバイト数のデータ。その後にCRLFが続く。
  3. 上記1と2の繰り返し。
  4. 最終チャンク: サイズが0のチャンク。その後にCRLFが続く。
  5. トレーラーヘッダ(オプション): 追加のHTTPヘッダ。その後にCRLFが続く。
  6. 空行: 最終的な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が返した[]bytestring()に変換していました。このstring()変換が、チャンクサイズを読み取るたびに新しい文字列オブジェクトをヒープに割り当て、ガベージを生成していました。

変更後は、chunkedReader.beginChunk()readLineから[]byteを受け取るように変更され、strconv.ParseUintの代わりに新しく導入されたparseHexUint関数が[]byteを直接受け取って16進数をuint64に変換するようになりました。これにより、[]byteからstringへの変換が不要になり、メモリ割り当てが削減されます。

また、readLineBytes関数はreadLineにリネームされ、bytes.TrimRightの代わりにtrimTrailingWhitespaceという新しいヘルパー関数が導入されました。bytes.TrimRightも内部で新しいバイトスライスを生成する可能性があるため、これも最適化の一環と考えられます。trimTrailingWhitespaceは、元のスライスを再スライスするだけで、新しいメモリ割り当てを避けています。

これらの変更は、net/http/chunked.gosrc/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:

  1. chunkedReader.beginChunk() 内で、line変数の型が string から []byte に変更されました。
    -	var line string
    +	var line []byte
    
  2. strconv.ParseUint の代わりに、新しく追加された parseHexUint 関数が使用されるようになりました。
    -	cr.n, cr.err = strconv.ParseUint(line, 16, 64)
    +	cr.n, cr.err = parseHexUint(line)
    
  3. readLineBytes 関数が readLine にリネームされました。
    -func readLineBytes(b *bufio.Reader) (p []byte, err error) {
    +func readLine(b *bufio.Reader) (p []byte, err error) {
    
  4. readLine (旧 readLineBytes) 関数内で、bytes.TrimRight の代わりに trimTrailingWhitespace が使用されるようになりました。
    -	p = bytes.TrimRight(p, " \r\t\n")
    +	return trimTrailingWhitespace(p), nil
    
  5. readLineBytes, but convert the bytes into a string. というコメントと共に存在した、[]bytestringに変換するreadLine関数が削除されました。
  6. 新しいヘルパー関数 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
    }
    
  7. 新しいヘルパー関数 isASCIISpace(b byte) bool が追加されました。これは、バイトがASCIIの空白文字であるかを判定します。
    func isASCIISpace(b byte) bool {
    	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
    }
    
  8. 新しいヘルパー関数 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:

  1. TestChunkReaderAllocs という新しいテスト関数が追加されました。このテストは、チャンクリーダーがチャンクデータを読み取る際のメモリ割り当て回数を計測し、それが1回以下であることを検証します。
  2. 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の追加は、このメモリ割り当て削減の目標が達成されていることを自動的に検証するための重要なテストです。

関連リンク

参考にした情報源リンク