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

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

このコミットは、Go言語の標準ライブラリである database/sql パッケージにおけるメモリ割り当ての最適化に関するものです。具体的には、匿名関数(クロージャ)の生成をメソッド値の使用に置き換えることで、ガベージコレクション(GC)の負荷を軽減し、パフォーマンスを向上させています。

コミット

commit 0bbf0ec0ed5c17a76942d9ae8a6e6b9559dacb9e
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue May 14 16:35:31 2013 -0700

    database/sql: use method values instead of generating closures
    
    Reduces garbage.
    
    R=adg, r
    CC=dsymonds, gobot, golang-dev
    https://golang.org/cl/9088045

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

https://github.com/golang/go/commit/0bbf0ec0ed5c17a76942d9ae8a6e6b9559dacb9e

元コミット内容

database/sql: use method values instead of generating closures

Reduces garbage.

R=adg, r
CC=dsymonds, gobot, golang-dev
https://golang.org/cl/9088045

変更の背景

この変更の主な背景は、Goプログラムのパフォーマンス最適化、特にガベージコレクション(GC)の効率化にあります。database/sql パッケージは、Goアプリケーションがデータベースと対話するための標準的なインターフェースを提供します。データベース操作は頻繁に行われるため、このパッケージ内で発生する小さなメモリ割り当て(アロケーション)であっても、累積するとGCの頻度と負荷を増大させ、アプリケーション全体のレイテンシやスループットに悪影響を与える可能性があります。

以前の実装では、接続の解放処理やステートメントのクローズ処理など、特定のコールバック関数を匿名関数(クロージャ)としてその場で生成していました。クロージャは、外部スコープの変数をキャプチャするため、ヒープ上にメモリが割り当てられます。これらのクロージャが短命で頻繁に生成・破棄される場合、GCがそれらを回収するために余分な作業を行う必要があり、これが「ガベージ(ゴミ)の生成」としてパフォーマンスボトルネックとなることがありました。

このコミットは、これらのクロージャの生成を、既存の構造体のメソッドを直接参照する「メソッド値」に置き換えることで、ヒープアロケーションを削減し、結果としてGCの負担を軽減することを目的としています。

前提知識の解説

Go言語の database/sql パッケージ

database/sql パッケージは、Go言語におけるデータベース操作のための汎用的なインターフェースを提供します。これは特定のデータベースシステム(例: MySQL, PostgreSQL, SQLite)に依存せず、各データベースのドライバがこのインターフェースを実装することで、アプリケーションは統一された方法でデータベースにアクセスできます。

主要な概念:

  • DB: データベースへの接続プールを表す構造体。複数の接続を管理し、必要に応じて接続を貸し出したり、プールに戻したりします。
  • Conn (または内部的な driverConn): データベースへの単一の物理的な接続を表します。
  • Stmt: プリペアドステートメントを表します。SQLクエリを事前に準備し、繰り返し実行することでパフォーマンスを向上させます。
  • Rows: クエリ結果の行を反復処理するための構造体です。

Go言語のクロージャとメソッド値

Go言語には、関数を値として扱う機能があります。このコミットを理解するためには、特に「クロージャ」と「メソッド値」の違いを理解することが重要です。

  • クロージャ (Closure): 匿名関数の一種で、その関数が定義されたスコープ(外部スコープ)の変数を「キャプチャ」して使用できるものです。Goでは、クロージャが外部変数をキャプチャする場合、そのクロージャ自体はヒープ上に割り当てられます。これは、クロージャが外部変数のライフタイムを延長する必要があるためです。頻繁に生成されるクロージャは、そのたびにヒープアロケーションを引き起こし、ガベージコレクションの対象となります。

    例:

    func makeAdder(x int) func(int) int {
        return func(y int) int { // この匿名関数はクロージャ。xをキャプチャする。
            return x + y
        }
    }
    
  • メソッド値 (Method Value): 構造体やインターフェースのメソッドを、そのレシーバ(メソッドが呼び出されるインスタンス)にバインドした関数値です。メソッド値は、特定のインスタンスに対してメソッドを呼び出すための関数として機能します。クロージャとは異なり、メソッド値はレシーバを「キャプチャ」しますが、それ以外の外部変数をキャプチャしないため、多くの場合、ヒープアロケーションを伴わずにスタック上で効率的に作成できます。これにより、ガベージの生成を抑えることができます。

    例:

    type MyStruct struct {
        value int
    }
    
    func (m *MyStruct) GetValue() int { // GetValueはメソッド
        return m.value
    }
    
    func main() {
        m := &MyStruct{value: 10}
        f := m.GetValue // fはメソッド値。mをレシーバとしてGetValueにバインドされる。
        fmt.Println(f()) // 10
    }
    

    この例では、fmGetValue メソッドを指す関数値であり、m がレシーバとして既にバインドされています。

ガベージコレクション (GC) とメモリ最適化

Go言語は自動メモリ管理(ガベージコレクション)を採用しています。開発者は手動でメモリを解放する必要がありませんが、GCはプログラムの実行中に不要になったメモリを自動的に回収するプロセスです。GCが動作する際には、一時的にプログラムの実行が停止したり(ストップ・ザ・ワールド)、CPUリソースを消費したりするため、GCの頻度や実行時間を減らすことは、アプリケーションのパフォーマンス(特にレイテンシ)を向上させる上で非常に重要です。

メモリ最適化の一般的な戦略の一つは、「アロケーションの削減」です。ヒープへのメモリ割り当てが少なければ少ないほど、GCが回収すべき「ゴミ」が減り、GCの実行頻度や負荷が軽減されます。このコミットは、まさにこのアロケーション削減の原則に基づいています。

技術的詳細

このコミットは、database/sql パッケージ内のいくつかの箇所で、頻繁に生成されていたクロージャをメソッド値に置き換えることで、ヒープアロケーションを削減しています。

具体的な変更点は以下の3つの主要なパターンに集約されます。

  1. driverConn.releaseConn メソッドの導入: 以前は、データベース接続をプールに返す処理 (db.putConn(ci, err)) が、query メソッド内で匿名関数として定義され、releaseConn という変数に代入されていました。この匿名関数は ci (現在の driverConn インスタンス) をキャプチャするため、クロージャとなり、呼び出しごとにヒープアロケーションが発生していました。 この変更では、driverConn 型に releaseConn という新しいメソッドを追加しました。このメソッドは、自身のレシーバ (dc) を使って dc.db.putConn(dc, err) を呼び出します。

    // 変更前 (概念):
    // func (db *DB) query(...) {
    //     ci := ... // driverConn
    //     releaseConn := func(err error) { db.putConn(ci, err) } // クロージャ
    //     db.queryConn(ci, releaseConn, ...)
    // }
    
    // 変更後:
    func (dc *driverConn) releaseConn(err error) {
        dc.db.putConn(dc, err)
    }
    // func (db *DB) query(...) {
    //     ci := ... // driverConn
    //     db.queryConn(ci, ci.releaseConn, ...) // メソッド値
    // }
    

    これにより、query メソッド内で ci.releaseConn を直接渡すことができるようになり、クロージャの生成が不要になりました。

  2. db.removeDepLocked での finalClose の直接利用: db.removeDepLocked 関数は、依存関係がなくなった際に最終的なクローズ処理を行うための関数を返します。以前は、x.finalClose() を呼び出す匿名関数を返していました。ここでも、x をキャプチャするクロージャが生成されていました。 変更後は、x.finalClose を直接返しています。これは、finalClosefinalCloser インターフェースのメソッドであり、x がそのインターフェースを実装しているため、x.finalClose はメソッド値として機能します。

    // 変更前:
    // return func() error { return x.finalClose() } // クロージャ
    
    // 変更後:
    // return x.finalClose // メソッド値
    

    これにより、finalClose 処理のためのクロージャ生成が回避されました。

  3. Stmt.connStmt での releaseConn の置き換え: Stmt.connStmt メソッドも、接続を解放するための関数を返していました。ここでも、s.db.putConn(conn, err) を呼び出す匿名関数が使用されており、conn をキャプチャするクロージャが生成されていました。 この箇所も、conn.releaseConn というメソッド値に置き換えられました。

    // 変更前:
    // releaseConn = func(err error) { s.db.putConn(conn, err) } // クロージャ
    // return conn, releaseConn, cs.si, nil
    
    // 変更後:
    // return conn, conn.releaseConn, cs.si, nil // メソッド値
    

    これにより、Stmt のコンテキストでの接続解放処理におけるクロージャ生成も排除されました。

これらの変更は、database/sql パッケージのような低レベルで頻繁に利用されるコードにおいて、小さなアロケーションを積み重ねてしまうことを防ぐための典型的な最適化手法です。メソッド値はクロージャよりも軽量であり、多くの場合、ヒープアロケーションなしで作成できるため、GCの負担を大幅に軽減できます。

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

変更は src/pkg/database/sql/sql.go ファイルの1箇所のみです。

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -219,6 +219,10 @@ type driverConn struct {\n 	dbmuClosed bool     // same as closed, but guarded by db.mu, for connIfFree\n }\n \n+func (dc *driverConn) releaseConn(err error) {\n+\tdc.db.putConn(dc, err)\n+}\n+\n func (dc *driverConn) removeOpenStmt(si driver.Stmt) {\n 	dc.Lock()\n 	defer dc.Unlock()\n@@ -367,10 +371,7 @@ func (db *DB) removeDepLocked(x finalCloser, dep interface{}) func() error {\n 	if !done {\n 		return func() error { return nil }\n 	}\n-\treturn func() error {\n-\t\t//println(fmt.Sprintf(\"calling final close on %T %v (%#v)\", x, x, x))\n-\t\treturn x.finalClose()\n-\t}\n+\treturn x.finalClose\n }\n \n // Open opens a database specified by its database driver name and a\n@@ -710,9 +711,7 @@ func (db *DB) query(query string, args []interface{}) (*Rows, error) {\n 		return nil, err\n 	}\n \n-\treleaseConn := func(err error) { db.putConn(ci, err) }\n-\n-\treturn db.queryConn(ci, releaseConn, query, args)\n+\treturn db.queryConn(ci, ci.releaseConn, query, args)\n }\n \n // queryConn executes a query on the given connection.\n@@ -1154,8 +1153,7 @@ func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.St\n \t}\n \n \tconn := cs.dc\n-\treleaseConn = func(err error) { s.db.putConn(conn, err) }\n-\treturn conn, releaseConn, cs.si, nil\n+\treturn conn, conn.releaseConn, cs.si, nil\n }\n \n // Query executes a prepared query statement with the given arguments\n```

## コアとなるコードの解説

### 1. `driverConn` 構造体への `releaseConn` メソッドの追加

```go
func (dc *driverConn) releaseConn(err error) {
	dc.db.putConn(dc, err)
}

driverConn はデータベースへの単一の接続を表す内部構造体です。この新しいメソッド releaseConn は、特定の driverConn インスタンス (dc) をレシーバとして受け取り、その接続をデータベース接続プール (dc.db) に戻す処理 (putConn) を行います。これにより、接続解放のロジックが driverConn 型にカプセル化され、再利用可能なメソッドとして提供されます。

2. db.removeDepLocked 関数の変更

-	return func() error {
-		//println(fmt.Sprintf("calling final close on %T %v (%#v)", x, x, x))\n
-		return x.finalClose()
-	}
+	return x.finalClose

db.removeDepLocked は、依存関係がなくなったオブジェクトの最終クローズ処理を管理する関数です。

  • 変更前: x.finalClose() を呼び出す匿名関数を返していました。この匿名関数は x をキャプチャするため、クロージャとしてヒープに割り当てられていました。
  • 変更後: x.finalClose というメソッド値を直接返しています。xfinalCloser インターフェースを実装しており、finalClose はそのメソッドです。これにより、クロージャの生成に伴うヒープアロケーションがなくなります。

3. db.query 関数の変更

-	releaseConn := func(err error) { db.putConn(ci, err) }
-
-	return db.queryConn(ci, releaseConn, query, args)
+	return db.queryConn(ci, ci.releaseConn, query, args)

db.query はデータベースに対してクエリを実行する主要な関数です。

  • 変更前: クエリ実行後に接続を解放するための releaseConn という匿名関数(クロージャ)を定義し、それを db.queryConn に渡していました。このクロージャは ci (現在の driverConn インスタンス) をキャプチャしていました。
  • 変更後: 新しく追加された driverConn のメソッド ci.releaseConn を直接 db.queryConn に渡しています。ci.releaseConn はメソッド値であるため、クロージャの生成が不要になり、ヒープアロケーションが削減されます。

4. Stmt.connStmt 関数の変更

-	releaseConn = func(err error) { s.db.putConn(conn, err) }
-	return conn, releaseConn, cs.si, nil
+	return conn, conn.releaseConn, cs.si, nil

Stmt.connStmt は、プリペアドステートメントに関連付けられた接続と、その接続を解放するための関数を返します。

  • 変更前: 接続を解放するための releaseConn という匿名関数(クロージャ)を定義し、それを返していました。このクロージャは conn (現在の driverConn インスタンス) をキャプチャしていました。
  • 変更後: conn.releaseConn というメソッド値を直接返しています。これにより、ここでもクロージャの生成が回避され、メモリ効率が向上します。

これらの変更は、Go言語の database/sql パッケージのような、頻繁に呼び出される可能性のある低レベルのコードにおいて、小さなメモリ割り当てを削減し、ガベージコレクションの効率を高めるための典型的な最適化パターンを示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびソースコード
  • Go言語のガベージコレクションに関する一般的な情報源
  • Go言語におけるクロージャとメソッド値のパフォーマンス特性に関する技術記事
  • Gerrit Code Review (golang.org/cl/9088045)