[インデックス 19523] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージにおけるIPアドレスの文字列変換(IP.String()
、IPMask.String()
)および文字列からのIPアドレス解析(ParseIP()
)の効率化を目的としています。特に、文字列操作における非効率なメモリ割り当てとコピーを削減し、ベンチマークで示されるように大幅なパフォーマンス改善を達成しています。
コミット
commit f7c99f3377b2d75a5fd7913e04034fb0741edf82
Author: Rui Ueyama <ruiu@google.com>
Date: Wed Jun 11 20:40:00 2014 -0700
net: efficient text processing
Optimize IP.String, IPMask.String and ParseIP.
benchmark old ns/op new ns/op delta
BenchmarkParseIP 2216 1849 -16.56%
BenchmarkIPString 7828 2486 -68.24%
BenchmarkIPMaskString 3872 659 -82.98%
LGTM=mikioh.mikioh, dave, bradfitz
R=golang-codereviews, mikioh.mikioh, dave, bradfitz
CC=golang-codereviews
https://golang.org/cl/95750043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f7c99f3377b2d75a5fd7913e04034fb0741edf82
元コミット内容
net: efficient text processing
Optimize IP.String, IPMask.String and ParseIP.
benchmark old ns/op new ns/op delta
BenchmarkParseIP 2216 1849 -16.56%
BenchmarkIPString 7828 2486 -68.24%
BenchmarkIPMaskString 3872 659 -82.98%
LGTM=mikioh.mikioh, dave, bradfitz
R=golang-codereviews, mikioh.mikioh, dave, bradfitz
CC=golang-codereviews
https://golang.org/cl/95750043
変更の背景
このコミットが行われた背景には、Go言語における文字列操作のパフォーマンス特性と、net
パッケージがネットワークアプリケーションにおいて頻繁に利用されるという事実があります。
Go言語の文字列はイミュータブル(不変)です。これは、一度作成された文字列の内容を変更できないことを意味します。そのため、+
演算子を使って文字列を連結するたびに、新しい文字列が作成され、古い文字列の内容が新しい文字列にコピーされます。ループ内で頻繁に文字列連結を行うと、この新しい文字列の作成とコピーが繰り返され、大量のメモリ割り当てとガベージコレクションのオーバーヘッドが発生し、パフォーマンスが著しく低下します。
net
パッケージのIP.String()
やIPMask.String()
のような関数は、IPアドレスやネットマスクを人間が読める形式の文字列に変換するために頻繁に呼び出されます。これらの関数が内部で非効率な文字列連結を行っていると、ネットワーク通信が多発するアプリケーション全体のスループットに悪影響を及ぼす可能性があります。
また、ParseIP()
関数は文字列からIPアドレスを解析する逆の操作を行いますが、これもまた頻繁に呼び出される可能性があります。従来のParseIP()
の実装では、IPv4とIPv6のどちらであるかを判断するために、両方の解析ロジックを試行する可能性があり、これもまた不要な処理コストとなっていました。
このコミットは、これらのパフォーマンスボトルネックを解消し、net
パッケージの基本的な文字列処理操作をより効率的にすることで、Go言語で構築されたネットワークアプリケーション全体のパフォーマンス向上に貢献することを目的としています。ベンチマーク結果が示すように、特にIP.String()
とIPMask.String()
では劇的な改善が見られます。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の基本的な概念とネットワークに関する知識が必要です。
-
Go言語の文字列のイミュータビリティとパフォーマンス:
- Goの文字列はバイトの読み取り専用スライスであり、一度作成されると内容を変更できません。
s = s + "suffix"
のような文字列連結は、新しい文字列をヒープに割り当て、既存の文字列の内容と追加する文字列の内容をその新しいメモリ領域にコピーします。この操作は、特にループ内で多数回行われると、大量のメモリ割り当てとコピーを引き起こし、ガベージコレクションの頻度を増やし、パフォーマンスを低下させます。- パフォーマンスが重要なシナリオでは、
strings.Builder
やbytes.Buffer
、または事前に容量を確保した[]byte
スライスにappend
していく方法が推奨されます。これらは、内部バッファを効率的に管理し、メモリ再割り当ての回数を最小限に抑えることで、文字列構築のオーバーヘッドを大幅に削減します。
-
net.IP
とnet.IPMask
:net.IP
はGo言語のnet
パッケージでIPアドレス(IPv4またはIPv6)を表すために使用される型です。これは[]byte
のエイリアスであり、IPアドレスの各バイトを格納します。net.IPMask
はサブネットマスクを表す型で、これも[]byte
のエイリアスです。
-
IPアドレスの文字列表現と解析:
- IPアドレスは通常、人間が読める形式の文字列(例: "192.168.1.1" や "2001:0db8::1")で表現されます。
IP.String()
メソッドはnet.IP
型の値をこの文字列形式に変換します。ParseIP()
関数は、この文字列形式からnet.IP
型の値に変換します。- IPv6アドレスの文字列表現には、連続するゼロのブロックを
::
で短縮するルール(RFC 4291)があります。IP.String()
はこのルールに従って最適な短縮形式を生成する必要があります。
-
ベンチマーク:
- Go言語には、コードのパフォーマンスを測定するための組み込みのベンチマーク機能があります。
go test -bench=.
コマンドで実行でき、操作あたりのナノ秒(ns/op)や割り当てられたメモリ量(B/op)などの指標を提供します。 - このコミットメッセージに記載されているベンチマーク結果は、変更前(old)と変更後(new)のパフォーマンスを比較し、改善率(delta)を示しています。
- Go言語には、コードのパフォーマンスを測定するための組み込みのベンチマーク機能があります。
これらの知識を前提として、コミットがどのようにパフォーマンスを改善したかを詳細に見ていきます。
技術的詳細
このコミットは、net
パッケージ内のIPアドレス関連の文字列処理関数において、主に以下の3つの技術的な最適化を導入しています。
-
IP.String()
およびIPMask.String()
における文字列構築の最適化:- 変更前: これらの関数は、
var s string
で空の文字列を初期化し、ループ内でs += ...
のように文字列連結を繰り返していました。前述の通り、これはGoの文字列のイミュータビリティにより、各連結操作で新しい文字列が生成され、メモリの再割り当てとコピーが頻繁に発生するため、非常に非効率です。 - 変更後:
IP.String()
では、まずmaxLen
(IPv6アドレスの最長文字列表現の長さ)を考慮して、b := make([]byte, 0, maxLen)
のように、最終的な文字列の長さに十分な容量を持つバイトスライスを事前に確保しています。- その後、IPアドレスの各部分を文字列に変換し、その結果を
append(b, ...)
を使ってこのバイトスライスに追加していきます。append
は、スライスの容量が足りなくなった場合にのみ新しい基底配列を割り当ててコピーするため、文字列連結よりもはるかに効率的です。 - 最終的に、構築されたバイトスライス
b
をstring(b)
として文字列に変換して返します。この変換は、バイトスライスの基底配列を指す新しい文字列ヘッダを作成するだけで、データのコピーは発生しないため、非常に高速です。 IPMask.String()
も同様に、make([]byte, len(m)*2)
で事前にバイトスライスを確保し、各バイトの16進数表現を直接バイトスライスに書き込むことで最適化されています。
itox
関数の削除とappendHex
関数の導入:- 変更前は、数値を16進数文字列に変換するために
itox
関数が使用されていました。この関数も内部で文字列を構築し返していました。 - 変更後、
itox
は削除され、代わりにappendHex(dst []byte, i uint32) []byte
という新しいヘルパー関数が導入されました。この関数は、与えられたuint32
値を16進数表現に変換し、その結果を既存のバイトスライスdst
にappend
して返します。これにより、中間的な文字列の生成が不要になり、さらに効率が向上します。
- 変更前は、数値を16進数文字列に変換するために
- 変更前: これらの関数は、
-
ParseIP()
における早期判定による最適化:- 変更前:
ParseIP(s string) IP
関数は、まずparseIPv4(s)
を試行し、それがnil
(IPv4として解析失敗)であれば、次にparseIPv6(s, false)
を試行していました。これは、入力文字列がIPv4形式であるかIPv6形式であるかを事前に判断せず、常に両方の解析ロジックを試す可能性があるため、非効率でした。 - 変更後:
ParseIP()
関数は、入力文字列s
の最初の文字を検査するループを追加しました。- もし最初の文字が
.
であれば、それはIPv4アドレスである可能性が高いと判断し、すぐにparseIPv4(s)
を呼び出します。 - もし最初の文字が
:
であれば、それはIPv6アドレスである可能性が高いと判断し、すぐにparseIPv6(s, false)
を呼び出します。 - これにより、入力文字列の形式に応じて適切な解析関数を早期に選択できるようになり、不要な解析ロジックの実行を回避できます。これにより、特にIPv6アドレスの解析において、IPv4解析の試行という無駄なオーバーヘッドがなくなります。
- もし最初の文字が
- 変更前:
これらの変更は、Go言語のパフォーマンス最適化における一般的なパターン(文字列連結の回避、バイトスライスへの直接書き込み、早期リターンによる不要な処理のスキップ)を適用したものであり、ベンチマーク結果が示すように、顕著な性能向上をもたらしました。
コアとなるコードの変更箇所
src/pkg/net/ip.go
の IP.String()
メソッドの変更
--- a/src/pkg/net/ip.go
+++ b/src/pkg/net/ip.go
@@ -295,21 +296,23 @@ func (ip IP) String() string {\n \t\te1 = -1\n \t}\n \n+\tconst maxLen = len(\"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff\")\n+\tb := make([]byte, 0, maxLen)\n+\n \t// Print with possible :: in place of run of zeros\n-\tvar s string\n \tfor i := 0; i < IPv6len; i += 2 {\n \t\tif i == e0 {\n-\t\t\ts += \"::\"\n+\t\t\tb = append(b, \':\', \':')\n \t\t\ti = e1\n \t\t\tif i >= IPv6len {\n \t\t\t\tbreak\n \t\t\t}\n \t\t} else if i > 0 {\n-\t\t\ts += \":\"\n+\t\t\tb = append(b, \':')\n \t\t}\n-\t\ts += itox((uint(p[i])<<8)|uint(p[i+1]), 1)\n+\t\tb = appendHex(b, (uint32(p[i])<<8)|uint32(p[i+1]))\n \t}\n-\treturn s\n+\treturn string(b)\n }
src/pkg/net/ip.go
の IPMask.String()
メソッドの変更
--- a/src/pkg/net/ip.go
+++ b/src/pkg/net/ip.go
@@ -419,14 +422,14 @@ func (m IPMask) Size() (ones, bits int) {\n \n // String returns the hexadecimal form of m, with no punctuation.\n func (m IPMask) String() string {\n-\ts := \"\"\n-\tfor _, b := range m {\n-\t\ts += itox(uint(b), 2)\n-\t}\n-\tif len(s) == 0 {\n+\tif len(m) == 0 {\n \t\treturn \"<nil>\"\n \t}\n-\treturn s\n+\tbuf := make([]byte, len(m)*2)\n+\tfor i, b := range m {\n+\t\tbuf[i*2], buf[i*2+1] = hexDigit[b>>4], hexDigit[b&0xf]\n+\t}\n+\treturn string(buf)\n }
src/pkg/net/ip.go
の ParseIP()
関数の変更
--- a/src/pkg/net/ip.go
+++ b/src/pkg/net/ip.go
@@ -646,11 +649,16 @@ func (e *ParseError) Error() string {\n // If s is not a valid textual representation of an IP address,\n // ParseIP returns nil.\n func ParseIP(s string) IP {\n-\tif ip := parseIPv4(s); ip != nil {\n-\t\treturn ip\n+\tfor i := 0; i < len(s); i++ {\n+\t\tswitch s[i] {\n+\t\tcase \'.\':\n+\t\t\treturn parseIPv4(s)\n+\t\tcase \':':\n+\t\t\tip, _ := parseIPv6(s, false)\n+\t\t\treturn ip\n+\t\t}\n \t}\n-\tip, _ := parseIPv6(s, false)\n-\treturn ip\n+\treturn nil\n }
src/pkg/net/parse.go
の itox
の削除と appendHex
の追加
--- a/src/pkg/net/parse.go
+++ b/src/pkg/net/parse.go
@@ -210,18 +210,18 @@ func itod(i uint) string {\n \treturn string(b[bp:])\n }\n \n-// Convert i to hexadecimal string.\n-func itox(i uint, min int) string {\n-\t// Assemble hexadecimal in reverse order.\n-\tvar b [32]byte\n-\tbp := len(b)\n-\tfor ; i > 0 || min > 0; i /= 16 {\n-\t\tbp--\n-\t\tb[bp] = \"0123456789abcdef\"[byte(i%16)]\n-\t\tmin--\n+// Convert i to a hexadecimal string. Leading zeros are not printed.\n+func appendHex(dst []byte, i uint32) []byte {\n+\tif i == 0 {\n+\t\treturn append(dst, '0')\n \t}\n-\n-\treturn string(b[bp:])\n+\tfor j := 7; j >= 0; j-- {\n+\t\tv := i >> uint(j*4)\n+\t\tif v > 0 {\n+\t\t\tdst = append(dst, hexDigit[v&0xf])\n+\t\t}\n+\t}\n+\treturn dst\n }
コアとなるコードの解説
IP.String()
および IPMask.String()
の最適化
- 文字列連結からバイトスライスへのアペンドへの移行:
- 変更前は、
var s string
で空の文字列を宣言し、ループ内でs += ...
のように文字列を連結していました。Goの文字列は不変であるため、この操作は毎回新しい文字列オブジェクトを生成し、古い内容を新しいオブジェクトにコピーするというオーバーヘッドを伴います。これは、特に多くの連結が発生する場合に、メモリ割り当てとガベージコレクションの負荷を増大させ、パフォーマンスを低下させる主要な原因でした。 - 変更後は、
make([]byte, 0, maxLen)
(IP.String()
の場合)やmake([]byte, len(m)*2)
(IPMask.String()
の場合)のように、事前に十分な容量を持つバイトスライスを確保しています。そして、append(b, ...)
やbuf[i*2], buf[i*2+1] = ...
のように、直接バイトスライスにデータを追加・書き込んでいます。バイトスライスは可変であるため、容量が足りない場合にのみ再割り当てが発生し、文字列連結に比べてはるかに効率的です。 - 最終的に
string(b)
やstring(buf)
としてバイトスライスを文字列に変換していますが、この変換はバイトスライスの基底配列を指す新しい文字列ヘッダを作成するだけで、データのコピーは発生しないため、非常に高速です。
- 変更前は、
itox
の削除とappendHex
の導入:itox
関数は数値を16進数文字列に変換していましたが、これも内部で文字列を生成していました。- 新しく導入された
appendHex
関数は、数値を16進数に変換し、その結果を既存のバイトスライスに直接アペンドします。これにより、中間的な文字列オブジェクトの生成が完全に不要になり、さらなるパフォーマンス向上が図られています。
これらの変更により、IP.String()
は68.24%、IPMask.String()
は82.98%もの大幅な高速化を達成しました。これは、文字列操作のボトルネックが解消されたことによる直接的な効果です。
ParseIP()
の最適化
- 早期判定による不要な解析のスキップ:
- 変更前は、
ParseIP
関数はまずIPv4として解析を試み、失敗した場合にIPv6として解析を試みていました。これは、入力文字列がIPv6形式である場合でも、常にIPv4解析のロジックを一度実行するという無駄なオーバーヘッドがありました。 - 変更後は、入力文字列の最初の文字を検査するシンプルなループを追加しました。
- もし最初の文字が
.
であれば、それはIPv4アドレスの形式である可能性が非常に高いため、すぐにparseIPv4
を呼び出します。 - もし最初の文字が
:
であれば、それはIPv6アドレスの形式である可能性が非常に高いため、すぐにparseIPv6
を呼び出します。
- もし最初の文字が
- この変更により、入力文字列の形式に応じて適切な解析関数を早期に選択できるようになり、不要な解析ロジックの実行を回避できます。これにより、
ParseIP
は16.56%の高速化を達成しました。
- 変更前は、
これらの最適化は、Go言語の標準ライブラリが、細部にわたるパフォーマンスチューニングによって継続的に改善されていることを示しています。
関連リンク
- Go Change-ID:
95750043
(Gerrit CL)
参考にした情報源リンク
- Go's
net
package and related functionalities have undergone various optimizations, particularly concerning IP address handling and string manipulations. - Go String Concatenation vs. Byte Slice Append Performance
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFKNlrsrU3uUNWlMt_9jDN4ft43hQwg0e2c-O8hgvSGHPNMQ37QVNFhMhFDwUrMFIK-AI3j0mM5_r--Nr1f8fxeAPSp0xeJ9on0xMFB8qrPgrrhpfhNQO4H7WhdBYnM9k43YED2eLVKb0DpQcrPK77X17nmVwZamahiSsFiGDAeGCzqQ7VtnxjiqYozm1BU
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG0zGL6ogMQ3gZpiixMQzuy-N5WwzYVsxu8YTKf01_q8e2CGP07lg9cyr_X-CzfUo-fp4DOT2qT_fhECE95g34xTj8mEJrDMzSzfKrzEaKNszBcmofideGbBOh2hP3yO57nLJgUvVc0HWWkwXOqqjCzL7UXBpAOn_joP_cf
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHkrfJbXQiS_guBIBMyTlgrL9Ps-UzBDOtQ1-shXjvFuJpEXDVC1TCIIDz3MzMkMKjEpvw_-rbYDhkRCoH5bEUXY1WfV3vI_dQE6-Z0aQhQ5iYqoRAyIfaThsVvlECI5q6x04pXXO5W22X16x9FlNVQ5uunikPWTZ8qS04Bdd4QELBGy4OP
- Go IP Address Parsing Optimization
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQErp_O8zeAjcJGEIJbipAyt2HSk-imPYwGQOelsaAUSXUGdvsKEyOBv8OKJ4ZKLHE0UDDHKb2OWIIBJBtCsBFZ26TGaNt8nFfDlMH_8htJrl0Z4=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFSy4GmeY4kCHvDOMtD797bXpDI3zUsjPt6P8Q60lNiZciWNVqJK-9Jc9LQwp-LVd1DklgM2LW0L5z3n84UoOayha4fH25Hp7x3o5mUDYbqRjruNlOXn22fLRceisTkTOBPqiEctNoEk9rTV6zJUDfk8sWM29w3nqqkkGCEMJPhSbzNdISiOrLFz2fm80RI