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

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

このコミットは、Go言語の標準ライブラリである net パッケージ内の fd_unix.go ファイルに対する変更です。具体的には、netFD 構造体の setAddr メソッドにおける不要なメモリ割り当て(アロケーション)を削減することを目的としています。

コミット

commit 067315c647a8c973147bb53fb7a3483e99aa863e
Author: Dave Cheney <dave@cheney.net>
Date:   Fri Oct 26 19:41:21 2012 +1100

    net: avoid allocation in setAddr
    
    setAddr was showing up in profiles due to string concatenation construction the os.File name field. netFD.sysfile's Name() is never used, except in dup() so I believe it is safe to avoid this allocation.
    
    R=mikioh.mikioh, rsc
    CC=golang-dev
    https://golang.org/cl/6742058

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

https://github.com/golang/go/commit/067315c647a8c973147bb53fb7a3483e99aa863e

元コミット内容

net: avoid allocation in setAddr

setAddr メソッドが、os.Filename フィールドを構築するための文字列結合によってプロファイル上で頻繁に現れていました。netFD.sysfileName() メソッドは dup() メソッド以外では使用されないため、このアロケーションを回避することは安全であると判断されました。

変更の背景

Go言語のパフォーマンス最適化において、メモリ割り当て(アロケーション)の削減は非常に重要な要素です。特に、頻繁に呼び出される関数内で不要なアロケーションが発生すると、ガベージコレクション(GC)の頻度が増加し、プログラム全体のパフォーマンスに悪影響を及ぼす可能性があります。

このコミットの背景には、プロファイリングツール(例えば pprof)を用いた分析で、net パッケージ内の setAddr メソッドがパフォーマンスのボトルネックとして浮上したことがあります。具体的には、setAddr 内で os.NewFile を呼び出す際に、ファイル名を生成するために行われる文字列結合が、不要な一時的な文字列オブジェクトを生成し、それがアロケーションとして計上されていたのです。

コミットメッセージにある「netFD.sysfile's Name() is never used, except in dup()」という記述は、この最適化の根拠となっています。つまり、os.File オブジェクトに設定されるファイル名(Name() メソッドで取得できる値)は、netFD 構造体の内部では dup() メソッドがファイルディスクリプタを複製する際にしか利用されないという事実が判明しました。この知見に基づき、setAddr が呼び出されるたびに毎回複雑な文字列を生成する必要はないと判断されたわけです。

前提知識の解説

Go言語の net パッケージと netFD

Go言語の net パッケージは、ネットワークI/Oのプリミティブを提供します。TCP/IP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うための機能が含まれています。 netFDnet パッケージ内部で使用される構造体で、ファイルディスクリプタ(Unix系OSにおけるソケットやファイルへの参照)をラップし、ネットワーク接続の状態や関連する情報を管理します。netFD は、ソケットの読み書き、接続、バインドなどの低レベルな操作を抽象化するために利用されます。

os.FileName() メソッド

Go言語の os パッケージは、オペレーティングシステムとのインタフェースを提供します。os.File は、ファイルやソケットなどのI/Oリソースを表す構造体です。 os.NewFile(fd uintptr, name string) 関数は、既存のファイルディスクリプタ(fd)から新しい os.File オブジェクトを作成します。このとき、name 引数で指定された文字列が、作成される os.File オブジェクトの「名前」として内部的に保持されます。 (*os.File).Name() string メソッドは、この os.File オブジェクトに設定された名前文字列を返します。この名前はデバッグやロギングの目的で使われることがありますが、ファイルディスクリプタ自体の機能には直接影響しません。

メモリ割り当て(アロケーション)とガベージコレクション(GC)

Go言語はガベージコレクタを持つ言語であり、開発者が手動でメモリを解放する必要はありません。しかし、プログラムが実行中に頻繁に小さなオブジェクトを生成し、それがすぐに不要になる場合、ガベージコレクタが頻繁に動作することになります。このGCの動作は、一時的にプログラムの実行を停止させる(ストップ・ザ・ワールド)ことがあり、特に低レイテンシが求められるアプリケーションではパフォーマンスのボトルネックとなります。 「アロケーションを避ける」とは、このような一時的なオブジェクトの生成を減らし、GCの負荷を軽減することで、プログラムの実行効率を向上させることを意味します。文字列結合は、新しい文字列オブジェクトを生成するため、アロケーションの原因となる典型的な操作です。

プロファイリング

プロファイリングとは、プログラムの実行中にその性能特性(CPU使用率、メモリ使用量、関数呼び出し回数など)を測定し、ボトルネックとなっている部分を特定するプロセスです。Go言語には pprof という強力なプロファイリングツールが標準で提供されており、CPUプロファイル、メモリプロファイル、ゴルーチンプロファイルなどを取得できます。このコミットの背景にある「setAddr がプロファイル上で現れていた」という記述は、pprof のようなツールを使って性能分析が行われたことを示唆しています。

技術的詳細

このコミットの技術的な核心は、os.FileName() メソッドが実際に必要とされるタイミングを特定し、そのタイミングでのみ文字列結合によるアロケーションを行うようにコードを再構成した点にあります。

変更前は、setAddr メソッドが呼び出されるたびに、fd.net + ":" + ls + "->" + rs という形式でファイル名文字列が構築され、os.NewFile に渡されていました。setAddr はネットワーク接続が確立されるたびに呼び出される可能性があり、その頻度が高いと、この文字列結合によるアロケーションが累積的にパフォーマンスに影響を与えていました。

変更後のアプローチは以下の通りです。

  1. setAddr から文字列結合を削除: setAddr メソッド内では、fd.sysfile = os.NewFile(uintptr(fd.sysfd), fd.net) のように、os.NewFile に渡す名前を fd.net というシンプルな文字列に限定しました。これにより、setAddr が呼び出されても、複雑な文字列結合によるアロケーションは発生しなくなりました。
  2. name() メソッドの導入: netFD 構造体に新しく name() というメソッドが追加されました。このメソッドは、以前 setAddr 内で行われていた fd.net + ":" + ls + "->" + rs という文字列結合を実行し、その結果を返します。
  3. dup() メソッドの変更: netFDdup() メソッドは、ファイルディスクリプタを複製する際に fd.sysfile.Name() を呼び出して、新しい os.File オブジェクトの名前として利用していました。この dup() メソッドが、新しく導入された fd.name() メソッドを呼び出すように変更されました。

この変更により、複雑なファイル名文字列の生成(とそれに伴うアロケーション)は、dup() メソッドが呼び出されたときにのみ発生するようになりました。コミットメッセージが示唆するように、dup()setAddr よりもはるかに呼び出し頻度が低いと想定されるため、全体としてアロケーションの総量を削減し、結果としてガベージコレクションの頻度を減らし、パフォーマンスを向上させることが期待されます。

これは、Go言語における「必要なときにだけアロケーションを行う」という一般的な最適化パターンの一例です。

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

src/pkg/net/fd_unix.go ファイルの変更点です。

--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -312,14 +312,18 @@ func newFD(fd, family, sotype int, net string) (*netFD, error) {
 func (fd *netFD) setAddr(laddr, raddr Addr) {
 	fd.laddr = laddr
 	fd.raddr = raddr
+	fd.sysfile = os.NewFile(uintptr(fd.sysfd), fd.net)
+}
+
+func (fd *netFD) name() string {
 	var ls, rs string
-	if laddr != nil {
-		ls = laddr.String()
+	if fd.laddr != nil {
+		ls = fd.laddr.String()
 	}
-	if raddr != nil {
-		rs = raddr.String()
+	if fd.raddr != nil {
+		rs = fd.raddr.String()
 	}
-	fd.sysfile = os.NewFile(uintptr(fd.sysfd), fd.net+":"+ls+"->"+rs)
+	return fd.net + ":" + ls + "->" + rs
 }
 
 func (fd *netFD) connect(ra syscall.Sockaddr) error {
@@ -660,7 +664,7 @@ func (fd *netFD) dup() (f *os.File, err error) {
 		return nil, &OpError{"setnonblock", fd.net, fd.laddr, err}
 	}
 
-	return os.NewFile(uintptr(ns), fd.sysfile.Name()), nil
+	return os.NewFile(uintptr(ns), fd.name()), nil
 }
 
 func closesocket(s int) error {

コアとなるコードの解説

  1. setAddr メソッドの変更:

    • 変更前: fd.sysfile = os.NewFile(uintptr(fd.sysfd), fd.net+":"+ls+"->"+rs)
      • laddrraddr の文字列表現 (ls, rs) を使って、ネットワークタイプ (fd.net) と結合した詳細なファイル名を生成し、os.NewFile に渡していました。この文字列結合がアロケーションの原因でした。
    • 変更後: fd.sysfile = os.NewFile(uintptr(fd.sysfd), fd.net)
      • os.NewFile に渡すファイル名を、ネットワークタイプ (fd.net) のみに簡素化しました。これにより、setAddr が呼び出されるたびに発生していた詳細なファイル名文字列のアロケーションがなくなりました。
  2. name() メソッドの追加:

    • 新しく func (fd *netFD) name() string メソッドが追加されました。
    • このメソッドの内部では、以前 setAddr で行われていた fd.net + ":" + ls + "->" + rs という文字列結合が実行されます。
    • このメソッドは、netFD のローカルアドレス (fd.laddr) とリモートアドレス (fd.raddr) を利用して、完全なファイル名文字列を生成します。
  3. dup() メソッドの変更:

    • 変更前: return os.NewFile(uintptr(ns), fd.sysfile.Name()), nil
      • dup() は、複製されたファイルディスクリプタ ns を使って新しい os.File を作成する際に、元の fd.sysfileName() メソッドを呼び出してファイル名を取得していました。
    • 変更後: return os.NewFile(uintptr(ns), fd.name()), nil
      • dup() は、新しく導入された fd.name() メソッドを呼び出すように変更されました。これにより、dup() が呼び出されたときにのみ、詳細なファイル名文字列が生成されるようになりました。

この一連の変更により、setAddr の呼び出し頻度が高い場合でも、不要な文字列アロケーションを回避し、必要な場合にのみ(dup() が呼び出されたとき)アロケーションを行うという、効率的なメモリ管理が実現されています。

関連リンク

参考にした情報源リンク