[インデックス 13178] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおけるパフォーマンス改善を目的としています。具体的には、HTTPヘッダーのキーを正規化する CanonicalHeaderKey
関数の不要な呼び出しを削減することで、HTTPリクエストおよびレスポンス処理のホットパスにおけるCPU使用率を低減しています。
コミット
commit 1e814df79bdacc618dfe9768dfdece16c7b0a499
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon May 28 11:07:24 2012 -0700
net/http: avoid a bunch of unnecessary CanonicalHeaderKey calls
CanonicalHeaderKey didn't allocate, but it did use unnecessary
CPU in the hot path, deciding it didn't need to allocate.
I considered using constants for all these common header keys
but I didn't think it would be prettier. "Content-Length" looks
better than contentLength or hdrContentLength, etc.
R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/6255053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1e814df79bdacc618dfe9768dfdece16c7b0a499
元コミット内容
net/http: avoid a bunch of unnecessary CanonicalHeaderKey calls
CanonicalHeaderKey didn't allocate, but it did use unnecessary
CPU in the hot path, deciding it didn't need to allocate.
I considered using constants for all these common header keys
but I didn't think it would be prettier. "Content-Length" looks
better than contentLength or hdrContentLength, etc.
変更の背景
このコミットの背景には、Go言語の net/http
パッケージにおけるパフォーマンス最適化の継続的な取り組みがあります。HTTPヘッダーの処理は、Webサーバーやクライアントにおいて非常に頻繁に行われる操作であり、その効率性は全体のパフォーマンスに直結します。
net/http
パッケージでは、HTTPヘッダーのキー(例: "Content-Type", "Content-Length")は大文字・小文字を区別しないとされていますが、内部的には一貫した形式(Canonical MIME header format)に正規化して扱われます。この正規化を行うのが CanonicalHeaderKey
関数です。
コミットメッセージによると、CanonicalHeaderKey
関数はメモリ割り当て(allocation)を行わないものの、その処理自体がCPUを消費していました。特に、HTTPリクエストやレスポンスの処理パス("hot path")において、この関数が繰り返し呼び出されることで、無視できないCPUオーバーヘッドが発生していたと考えられます。
開発者は、一般的なヘッダーキーを定数として定義することも検討しましたが、コードの可読性を損なうと判断し、より透過的な方法でパフォーマンスを改善するアプローチを選択しました。それが、ヘッダーキーが既に正規化されていることが分かっている場合に CanonicalHeaderKey
の呼び出しをスキップする、という今回の変更です。
前提知識の解説
HTTPヘッダーと正規化
HTTPプロトコルでは、ヘッダーフィールド名は大文字・小文字を区別しないとRFC 7230で定義されています。例えば、Content-Type
、content-type
、CoNtEnT-TyPe
はすべて同じヘッダーを指します。しかし、プログラム内部でこれらのヘッダーを効率的に扱うためには、一貫した形式に正規化することが一般的です。
Goの net/http
パッケージでは、この正規化のために textproto.CanonicalMIMEHeaderKey
(net/http
の CanonicalHeaderKey
はこれをラップしている)を使用します。この関数は、ヘッダーキーの最初の文字とハイフン(-
)の後の文字を大文字にし、それ以外の文字を小文字にする、というルールで正規化を行います。例えば、content-type
は Content-Type
に、x-custom-header
は X-Custom-Header
に変換されます。
ホットパス (Hot Path)
ソフトウェア開発において「ホットパス」とは、プログラムの実行中に非常に頻繁に実行されるコードのセクションを指します。Webサーバーのようなシステムでは、HTTPリクエストの処理、データベースクエリの実行、データのシリアライズ/デシリアライズなどがホットパスになりがちです。ホットパスにおけるわずかな非効率性でも、システム全体のパフォーマンスに大きな影響を与える可能性があります。そのため、ホットパスの最適化は、パフォーマンスチューニングにおいて非常に重要な要素となります。
textproto.MIMEHeader
と Header
Goの net/http
パッケージでは、HTTPヘッダーは http.Header
型で表現されます。これは map[string][]string
のエイリアスであり、textproto.MIMEHeader
型も同様に map[string][]string
のエイリアスです。http.Header
は textproto.MIMEHeader
のメソッド(Get
, Set
, Add
, Del
など)を内部的に利用しています。
textproto.MIMEHeader.Get(key string)
メソッドは、内部で textproto.CanonicalMIMEHeaderKey(key)
を呼び出してキーを正規化し、その正規化されたキーを使ってマップから値を取得します。この正規化処理が、今回のコミットで最適化の対象となっています。
技術的詳細
このコミットの主要な技術的変更は、http.Header
型に新しい内部ヘルパーメソッド get(key string)
を追加し、既存の Get(key string)
メソッドの呼び出しを get(key string)
に置き換えることです。
Header.Get(key string)
の動作
従来の Header.Get(key string)
メソッドは、textproto.MIMEHeader(h).Get(key)
を呼び出していました。textproto.MIMEHeader.Get
は、引数として渡された key
を常に textproto.CanonicalMIMEHeaderKey
で正規化してから、内部のマップから値を取得します。
Header.get(key string)
の導入
新しく追加された Header.get(key string)
メソッドは、以下のように定義されています。
// get is like Get, but key must already be in CanonicalHeaderKey form.
func (h Header) get(key string) string {
if v := h[key]; len(v) > 0 {
return v[0]
}
return ""
}
このメソッドの重要な点は、コメントにもあるように「key
は既に CanonicalHeaderKey
形式である必要がある」という前提条件です。つまり、この get
メソッドはキーの正規化を行いません。直接 h[key]
を使ってマップから値を取得します。これにより、CanonicalHeaderKey
の呼び出しとそのCPUコストを回避できます。
呼び出し箇所の置き換え
コミットでは、net/http
パッケージ内の複数のファイル(request.go
, server.go
, transfer.go
)で、Header.Get("Some-Header")
のような呼び出しが Header.get("Some-Header")
に置き換えられています。これらの置き換えが行われた箇所は、コード内でヘッダーキーが既に正規化された形式(例: "Host", "Expect", "Connection", "Content-Length", "Content-Type", "Trailer")でハードコードされているか、あるいはそのコンテキストで正規化が不要であることが保証されている場所です。
これにより、これらのホットパスにおいて、ヘッダー値を取得するたびに発生していた不要な正規化処理がスキップされ、全体的なCPU使用率が削減されます。
定数化の検討と却下
コミットメッセージでは、一般的なヘッダーキーを定数として定義することも検討されたが、採用されなかったことが述べられています。例えば、Content-Length
を contentLength
や hdrContentLength
のような定数名にする案です。これは、コードの可読性を損なう("Content-Length"
の方が直感的である)という理由で却下されました。今回の get
メソッドの導入は、可読性を維持しつつパフォーマンスを改善する、より洗練されたアプローチと言えます。
コアとなるコードの変更箇所
このコミットでは、主に以下の4つのファイルが変更されています。
src/pkg/net/http/header.go
:Header.get
メソッドの追加。src/pkg/net/http/request.go
:req.Header.Get
の呼び出しをreq.Header.get
に変更。src/pkg/net/http/server.go
:w.header.Get
およびreq.Header.Get
の呼び出しをw.header.get
およびreq.Header.get
に変更。src/pkg/net/http/transfer.go
:header.Get
の呼び出しをheader.get
に変更。
src/pkg/net/http/header.go
の変更
--- a/src/pkg/net/http/header.go
+++ b/src/pkg/net/http/header.go
@@ -36,6 +36,14 @@ func (h Header) Get(key string) string {
return textproto.MIMEHeader(h).Get(key)
}
+// get is like Get, but key must already be in CanonicalHeaderKey form.
+func (h Header) get(key string) string {
+ if v := h[key]; len(v) > 0 {
+ return v[0]
+ }
+ return ""
+}
+
// Del deletes the values associated with key.
func (h Header) Del(key string) {
textproto.MIMEHeader(h).Del(key)
src/pkg/net/http/request.go
の変更例
--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -513,7 +513,7 @@ func ReadRequest(b *bufio.Reader) (req *Request, err error) {
// the same. In the second case, any Host line is ignored.
req.Host = req.URL.Host
if req.Host == "" {
- req.Host = req.Header.Get("Host")
+ req.Host = req.Header.get("Host")
}
req.Header.Del("Host")
@@ -732,16 +732,16 @@ func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, e\
}
func (r *Request) expectsContinue() bool {
- return hasToken(r.Header.Get("Expect"), "100-continue")
+ return hasToken(r.Header.get("Expect"), "100-continue")
}
func (r *Request) wantsHttp10KeepAlive() bool {
if r.ProtoMajor != 1 || r.ProtoMinor != 0 {
return false
}
- return hasToken(r.Header.Get("Connection"), "keep-alive")
+ return hasToken(r.Header.get("Connection"), "keep-alive")
}
func (r *Request) wantsClose() bool {
- return hasToken(r.Header.Get("Connection"), "close")
+ return hasToken(r.Header.get("Connection"), "close")
}
src/pkg/net/http/server.go
の変更例
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -287,7 +287,7 @@ func (w *response) WriteHeader(code int) {
// Check for a explicit (and valid) Content-Length header.
var hasCL bool
var contentLength int64
- if clenStr := w.header.Get("Content-Length"); clenStr != "" {
+ if clenStr := w.header.get("Content-Length"); clenStr != "" {
var err error
contentLength, err = strconv.ParseInt(clenStr, 10, 64)
if err == nil {
@@ -307,7 +307,7 @@ func (w *response) WriteHeader(code int) {
w.closeAfterReply = true
}
- if w.header.Get("Connection") == "close" {
+ if w.header.get("Connection") == "close" {
w.closeAfterReply = true
}
@@ -331,7 +331,7 @@ func (w *response) WriteHeader(code int) {
if code == StatusNotModified {
// Must not have body.
for _, header := range []string{"Content-Type", "Content-Length", "Transfer-Encoding"} {
- if w.header.Get(header) != "" {
+ if w.header.get(header) != "" {
// TODO: return an error if WriteHeader gets a return parameter
// or set a flag on w to make future Writes() write an error page?
// for now just log and drop the header.
@@ -341,7 +341,7 @@ func (w *response) WriteHeader(code int) {
}
} else {
// If no content type, apply sniffing algorithm to body.
- if w.header.Get("Content-Type") == "" && w.req.Method != "HEAD" {
+ if w.header.get("Content-Type") == "" && w.req.Method != "HEAD" {
w.needSniff = true
}
}
@@ -350,7 +350,7 @@ func (w *response) WriteHeader(code int) {
w.Header().Set("Date", time.Now().UTC().Format(TimeFormat))
}
- te := w.header.Get("Transfer-Encoding")
+ te := w.header.get("Transfer-Encoding")
hasTE := te != ""
if hasCL && hasTE && te != "identity" {
// TODO: return an error if WriteHeader gets a return parameter
@@ -390,7 +390,7 @@ func (w *response) WriteHeader(code int) {
return
}
- if w.closeAfterReply && !hasToken(w.header.Get("Connection"), "close") {
+ if w.closeAfterReply && !hasToken(w.header.get("Connection"), "close") {
w.header.Set("Connection", "close")
}
@@ -515,8 +515,8 @@ func (w *response) finishRequest() {
// If this was an HTTP/1.0 request with keep-alive and we sent a Content-Length
// back, we can make this a keep-alive response ...
if w.req.wantsHttp10KeepAlive() {
- sentLength := w.header.Get("Content-Length") != ""
- if sentLength && w.header.Get("Connection") == "keep-alive" {
+ sentLength := w.header.get("Content-Length") != ""
+ if sentLength && w.header.get("Connection") == "keep-alive" {
w.closeAfterReply = false
}
}
@@ -628,7 +628,7 @@ func (c *conn) serve() {
break
}
req.Header.Del("Expect")
- } else if req.Header.Get("Expect") != "" {
+ } else if req.Header.get("Expect") != "" {
// TODO(bradfitz): let ServeHTTP handlers handle
// requests with non-standard expectation[s]? Seems
// theoretical at best, and doesn't fit into the
src/pkg/net/http/transfer.go
の変更例
--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -432,7 +432,7 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,\
}
// Logic based on Content-Length
- cl := strings.TrimSpace(header.Get("Content-Length"))
+ cl := strings.TrimSpace(header.get("Content-Length"))
if cl != "" {
n, err := strconv.ParseInt(cl, 10, 64)
if err != nil || n < 0 {
@@ -454,7 +454,7 @@ func fixLength(isResponse bool, status int, requestMethod string, header Header,\
// Logic based on media type. The purpose of the following code is just
// to detect whether the unsupported "multipart/byteranges" is being
// used. A proper Content-Type parser is needed in the future.
- if strings.Contains(strings.ToLower(header.Get("Content-Type")), "multipart/byteranges") {
+ if strings.Contains(strings.ToLower(header.get("Content-Type")), "multipart/byteranges") {
return -1, ErrNotSupported
}
@@ -469,14 +469,14 @@ func shouldClose(major, minor int, header Header) bool {
if major < 1 {
return true
} else if major == 1 && minor == 0 {
- if !strings.Contains(strings.ToLower(header.Get("Connection")), "keep-alive") {
+ if !strings.Contains(strings.ToLower(header.get("Connection")), "keep-alive") {
return true
}
return false
} else {
// TODO: Should split on commas, toss surrounding white space,
// and check each field.
- if strings.ToLower(header.Get("Connection")) == "close" {
+ if strings.ToLower(header.get("Connection"), "close") {
header.Del("Connection")
return true
}
@@ -486,7 +486,7 @@ func shouldClose(major, minor int, header Header) bool {
// Parse the trailer header
func fixTrailer(header Header, te []string) (Header, error) {
- raw := header.Get("Trailer")
+ raw := header.get("Trailer")
if raw == "" {
return nil, nil
}
コアとなるコードの解説
このコミットの核心は、http.Header
型に導入された非公開メソッド get
です。
// get is like Get, but key must already be in CanonicalHeaderKey form.
func (h Header) get(key string) string {
if v := h[key]; len(v) > 0 {
return v[0]
}
return ""
}
この get
メソッドは、Header
マップから直接 key
に対応する値を取得しようとします。Get
メソッドとは異なり、key
の正規化処理(CanonicalHeaderKey
の呼び出し)を一切行いません。これにより、呼び出し元が既に正規化されたキーを使用していることが保証されている場合、不要なCPUサイクルを節約できます。
net/http
パッケージの内部では、"Host", "Content-Length", "Connection" など、特定のHTTPヘッダーキーがコード内でハードコードされて使用されることがよくあります。これらのハードコードされたキーは、Goのコーディング規約や慣習に従って、既に正規化された形式(例: "Content-Length" のように各単語の先頭が大文字)で記述されています。したがって、これらのキーに対して Get
メソッドを呼び出す際に、再度 CanonicalHeaderKey
を適用することは冗長であり、パフォーマンス上の無駄となります。
このコミットは、このような冗長な正規化処理を特定し、get
メソッドに置き換えることで、HTTP処理のパフォーマンスを向上させています。これは、マイクロ最適化の一例であり、個々の改善は小さいかもしれませんが、ホットパスで頻繁に実行されることで、全体として顕著なパフォーマンス向上に繋がります。
関連リンク
- Go言語の
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語の
textproto
パッケージのドキュメント: https://pkg.go.dev/net/textproto - RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing (特にヘッダーフィールド名のセクション): https://datatracker.ietf.org/doc/html/rfc7230#section-3.2
参考にした情報源リンク
- Goのコミット履歴とソースコード
- Go言語の公式ドキュメント
- HTTP/1.1 RFC 7230
- Go言語のパフォーマンス最適化に関する一般的な知識
textproto.CanonicalMIMEHeaderKey
の実装に関する情報 (Goのソースコードから)- Goの
net/http
パッケージの内部実装に関する議論 (golang-devメーリングリストなど)