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

[インデックス 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言語の基本的な概念とネットワークに関する知識が必要です。

  1. Go言語の文字列のイミュータビリティとパフォーマンス:

    • Goの文字列はバイトの読み取り専用スライスであり、一度作成されると内容を変更できません。
    • s = s + "suffix"のような文字列連結は、新しい文字列をヒープに割り当て、既存の文字列の内容と追加する文字列の内容をその新しいメモリ領域にコピーします。この操作は、特にループ内で多数回行われると、大量のメモリ割り当てとコピーを引き起こし、ガベージコレクションの頻度を増やし、パフォーマンスを低下させます。
    • パフォーマンスが重要なシナリオでは、strings.Builderbytes.Buffer、または事前に容量を確保した[]byteスライスにappendしていく方法が推奨されます。これらは、内部バッファを効率的に管理し、メモリ再割り当ての回数を最小限に抑えることで、文字列構築のオーバーヘッドを大幅に削減します。
  2. net.IPnet.IPMask:

    • net.IPはGo言語のnetパッケージでIPアドレス(IPv4またはIPv6)を表すために使用される型です。これは[]byteのエイリアスであり、IPアドレスの各バイトを格納します。
    • net.IPMaskはサブネットマスクを表す型で、これも[]byteのエイリアスです。
  3. IPアドレスの文字列表現と解析:

    • IPアドレスは通常、人間が読める形式の文字列(例: "192.168.1.1" や "2001:0db8::1")で表現されます。
    • IP.String()メソッドはnet.IP型の値をこの文字列形式に変換します。
    • ParseIP()関数は、この文字列形式からnet.IP型の値に変換します。
    • IPv6アドレスの文字列表現には、連続するゼロのブロックを::で短縮するルール(RFC 4291)があります。IP.String()はこのルールに従って最適な短縮形式を生成する必要があります。
  4. ベンチマーク:

    • Go言語には、コードのパフォーマンスを測定するための組み込みのベンチマーク機能があります。go test -bench=.コマンドで実行でき、操作あたりのナノ秒(ns/op)や割り当てられたメモリ量(B/op)などの指標を提供します。
    • このコミットメッセージに記載されているベンチマーク結果は、変更前(old)と変更後(new)のパフォーマンスを比較し、改善率(delta)を示しています。

これらの知識を前提として、コミットがどのようにパフォーマンスを改善したかを詳細に見ていきます。

技術的詳細

このコミットは、netパッケージ内のIPアドレス関連の文字列処理関数において、主に以下の3つの技術的な最適化を導入しています。

  1. IP.String()およびIPMask.String()における文字列構築の最適化:

    • 変更前: これらの関数は、var s stringで空の文字列を初期化し、ループ内でs += ...のように文字列連結を繰り返していました。前述の通り、これはGoの文字列のイミュータビリティにより、各連結操作で新しい文字列が生成され、メモリの再割り当てとコピーが頻繁に発生するため、非常に非効率です。
    • 変更後:
      • IP.String()では、まずmaxLen(IPv6アドレスの最長文字列表現の長さ)を考慮して、b := make([]byte, 0, maxLen)のように、最終的な文字列の長さに十分な容量を持つバイトスライスを事前に確保しています。
      • その後、IPアドレスの各部分を文字列に変換し、その結果をappend(b, ...)を使ってこのバイトスライスに追加していきます。appendは、スライスの容量が足りなくなった場合にのみ新しい基底配列を割り当ててコピーするため、文字列連結よりもはるかに効率的です。
      • 最終的に、構築されたバイトスライスbstring(b)として文字列に変換して返します。この変換は、バイトスライスの基底配列を指す新しい文字列ヘッダを作成するだけで、データのコピーは発生しないため、非常に高速です。
      • IPMask.String()も同様に、make([]byte, len(m)*2)で事前にバイトスライスを確保し、各バイトの16進数表現を直接バイトスライスに書き込むことで最適化されています。
    • itox関数の削除とappendHex関数の導入:
      • 変更前は、数値を16進数文字列に変換するためにitox関数が使用されていました。この関数も内部で文字列を構築し返していました。
      • 変更後、itoxは削除され、代わりにappendHex(dst []byte, i uint32) []byteという新しいヘルパー関数が導入されました。この関数は、与えられたuint32値を16進数表現に変換し、その結果を既存のバイトスライスdstappendして返します。これにより、中間的な文字列の生成が不要になり、さらに効率が向上します。
  2. 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.goIP.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.goIPMask.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.goParseIP() 関数の変更

--- 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.goitox の削除と 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言語の標準ライブラリが、細部にわたるパフォーマンスチューニングによって継続的に改善されていることを示しています。

関連リンク

参考にした情報源リンク