[インデックス 18218] ファイルの概要
このコミットは、Go言語のdatabase/sql
パッケージにおけるパフォーマンス改善を目的としています。具体的には、fmt.Sprintf
の使用を避けることでプリンタキャッシュの競合を解消し、RawBytes
へのスキャン時における不要なメモリ割り当てを削減しています。これにより、データベース操作における効率とスケーラビリティが向上します。
コミット
commit 258ed3f2265a41a46e936e884d8afd6e6f646973
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Jan 10 12:19:36 2014 -0800
database/sql: avoiding fmt.Sprintf while scanning, avoid allocs with RawBytes
A user reported heavy contention on fmt's printer cache. Avoid
fmt.Sprint. We have to do reflection anyway, and there was
already an asString function to use strconv, so use it.
This CL also eliminates a redundant allocation + copy when
scanning into *[]byte (avoiding the intermediate string)
and avoids an extra alloc when assigning to a caller's RawBytes
(trying to reuse the caller's memory).
Fixes #7086
R=golang-codereviews, nightlyone
CC=golang-codereviews
https://golang.org/cl/50240044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/258ed3f2265a41a46e936e884d8afd6e6f646973
元コミット内容
このコミットは、Goのdatabase/sql
パッケージにおけるパフォーマンス上のボトルネックを解消することを目的としています。具体的には、以下の2つの主要な問題に対処しています。
fmt.Sprintf
によるプリンタキャッシュの競合: ユーザーからの報告により、fmt.Sprintf
が頻繁に使用されることで、内部のプリンタキャッシュに激しい競合が発生し、アプリケーションのパフォーマンスが低下していることが判明しました。*[]byte
およびRawBytes
へのスキャン時の不要なメモリ割り当て: データベースからデータを読み込み、*[]byte
型やRawBytes
型にスキャンする際に、中間的な文字列変換や余分なメモリ割り当てが発生していました。これは特に大量のデータを扱う場合に、ガベージコレクションの負荷を増大させ、パフォーマンスに悪影響を与えていました。
これらの問題は、GoのIssue #7086として報告されており、このコミットはその修正を意図しています。
変更の背景
Goのdatabase/sql
パッケージは、データベースとのやり取りを抽象化するための標準ライブラリです。このパッケージは、様々なデータベースドライバと連携して動作し、クエリの実行結果をGoの型に変換する役割を担っています。
しかし、初期の実装では、データの型変換においてfmt.Sprintf
が多用されていました。fmt.Sprintf
は非常に便利で汎用的な文字列フォーマット関数ですが、その内部ではリフレクションや、フォーマット処理を高速化するための内部キャッシュ(プリンタキャッシュ)が使用されています。高負荷な環境でfmt.Sprintf
が頻繁に呼び出されると、このプリンタキャッシュへのアクセスが競合し、複数のゴルーチンが同時にキャッシュにアクセスしようとすることでロックが発生し、結果としてCPU使用率の増加やスループットの低下を引き起こすことがありました。
また、データベースから取得したバイナリデータや文字列データを[]byte
やsql.RawBytes
型にスキャンする際にも、非効率な処理が行われていました。特にsql.RawBytes
は、データベースが所有するメモリバッファへの直接参照を保持することでメモリ割り当てを最小限に抑えることを意図した型ですが、実際には中間的な文字列変換や余分なコピーが発生し、その設計意図が十分に活かされていませんでした。これは、大量のデータを扱うアプリケーションにおいて、ガベージコレクションの頻度を増やし、全体的なパフォーマンスを低下させる要因となっていました。
これらのパフォーマンス上の問題は、Goアプリケーションがデータベースと連携する際のボトルネックとなり得るため、その解消が急務とされていました。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。
-
database/sql
パッケージ: Goの標準ライブラリの一部で、SQLデータベースと対話するための汎用的なインターフェースを提供します。データベースドライバに依存しない形で、クエリの実行、結果の取得、トランザクション管理などを行うことができます。Rows.Scan()
メソッドは、クエリ結果の各行の列データをGoの変数に変換して割り当てるために使用されます。 -
fmt.Sprintf
:fmt
パッケージは、GoにおけるフォーマットI/Oを扱うためのパッケージです。Sprintf
関数は、指定されたフォーマット文字列と引数に基づいて文字列を生成し、その結果を返します。内部的にはリフレクションを使用し、引数の型に応じて適切なフォーマット処理を行います。パフォーマンスが要求される場面では、その汎用性ゆえにオーバーヘッドが生じることがあります。特に、内部で利用されるプリンタキャッシュは、複数のゴルーチンから同時にアクセスされると競合が発生し、ボトルネックとなる可能性があります。 -
reflect
パッケージ: Goのリフレクション機能を提供するパッケージです。実行時に変数の型情報(reflect.Type
)や値情報(reflect.Value
)を取得・操作することができます。database/sql
パッケージでは、Rows.Scan()
が任意のGoの型にデータをスキャンするためにリフレクションを広く利用しています。 -
strconv
パッケージ: 文字列と基本的なデータ型(整数、浮動小数点数、真偽値など)との間で変換を行うためのパッケージです。fmt
パッケージと比較して、より特化した変換機能を提供し、多くの場合でfmt.Sprintf
よりも高速かつメモリ効率が良いです。例えば、strconv.FormatInt
、strconv.FormatUint
、strconv.FormatFloat
、strconv.FormatBool
などがあります。 -
sql.RawBytes
型:database/sql
パッケージで定義されている[]byte
のエイリアス型です。この型は、データベースから読み込んだ生のバイトデータを、コピーせずに直接参照するために設計されています。これにより、特に大きなバイナリデータや文字列データを扱う際に、メモリ割り当てとコピーのオーバーヘッドを削減し、パフォーマンスを向上させることが期待されます。しかし、その利用には注意が必要で、データが有効な期間や、再利用時の挙動を正しく理解する必要があります。 -
メモリ割り当てとガベージコレクション (GC): Goは自動メモリ管理(ガベージコレクション)を採用しています。プログラムが実行中に新しいメモリを割り当てる(アロケーション)と、そのメモリはヒープに確保されます。不要になったメモリはガベージコレクタによって回収されます。アロケーションの頻度が高いと、ガベージコレクタがより頻繁に動作する必要があり、これがアプリケーションの実行を一時停止させ(ストップ・ザ・ワールド)、全体的なパフォーマンスに悪影響を与える可能性があります。パフォーマンスチューニングにおいては、不要なメモリ割り当てを減らすことが重要な戦略の一つとなります。
技術的詳細
このコミットは、database/sql
パッケージのconvert.go
ファイルにおけるconvertAssign
関数とasString
関数、そして新しく導入されたasBytes
関数を中心に変更を加えています。
1. fmt.Sprintf
によるプリンタキャッシュ競合の解消:
- 問題点:
convertAssign
関数内で、*string
型や*[]byte
型への変換時に、数値型や真偽値型から文字列への変換にfmt.Sprintf("%v", src)
が使用されていました。このfmt.Sprintf
の頻繁な呼び出しが、内部のプリンタキャッシュに競合を引き起こし、パフォーマンスのボトルネックとなっていました。 - 解決策:
fmt.Sprintf
の使用を避け、より特化したstrconv
パッケージの関数(strconv.FormatInt
,strconv.FormatUint
,strconv.FormatFloat
,strconv.FormatBool
)をasString
関数内で直接使用するように変更されました。asString
関数は、引数のreflect.Value
のKind()
に基づいて、適切なstrconv
関数を呼び出すように拡張されました。- これにより、
fmt.Sprintf
の汎用的な処理とそれに伴うオーバーヘッド、特にプリンタキャッシュの競合が回避され、数値や真偽値から文字列への変換が高速化されました。 - 最終的に
strconv
で処理できない型の場合のみ、フォールバックとしてfmt.Sprintf
が使用されますが、これは稀なケースに限定されます。
2. *[]byte
およびRawBytes
へのスキャン時の不要なメモリ割り当ての削減:
- 問題点:
*[]byte
へのスキャン時、fmt.Sprintf("%v", src)
で文字列を生成し、それを[]byte(string)
でバイトスライスに変換していました。これは、中間的な文字列の生成と、その後のバイトスライスへのコピーという二重のメモリ割り当てとコピーを伴っていました。*RawBytes
へのスキャン時も同様にfmt.Sprintf("%v", src)
で文字列を生成し、RawBytes(string)
で変換していました。RawBytes
はメモリ割り当てを避けるための型であるにもかかわらず、この処理はその意図に反していました。
- 解決策:
- 新しく
asBytes
関数が導入されました。この関数は、strconv.AppendInt
,strconv.AppendUint
,strconv.AppendFloat
,strconv.AppendBool
などのstrconv.Append
系の関数を利用して、既存のバイトスライス(buf
)に直接データを追記する形でバイトスライスを生成します。これにより、中間的な文字列の生成と余分なメモリ割り当てが回避されます。 *[]byte
へのスキャンでは、asBytes(nil, sv)
を呼び出すことで、新しいバイトスライスを効率的に生成します。*RawBytes
へのスキャンでは、asBytes([]byte(*d)[:0], sv)
を呼び出します。ここで[]byte(*d)[:0]
は、既存のRawBytes
の基盤となる配列を再利用しつつ、長さを0にリセットしたスライスを渡すことで、メモリの再利用を試みています。これにより、RawBytes
の設計意図である「呼び出し元のメモリを再利用する」という目的が達成され、不要なメモリ割り当てが大幅に削減されます。asBytes
関数は、reflect.String
の場合もappend(buf, s...)
を使って直接バイトスライスに追記することで、効率的な変換を実現しています。
- 新しく
これらの変更により、database/sql
パッケージにおける型変換処理のパフォーマンスが向上し、特に高負荷な環境や大量のデータを扱うアプリケーションにおいて、CPU使用率の低減とガベージコレクションの負荷軽減が期待されます。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/database/sql/convert.go
ファイルに集中しています。
src/pkg/database/sql/convert.go
-
convertAssign
関数の変更:case *string:
のブロックで、*d = fmt.Sprintf("%v", src)
の行が*d = asString(src)
に変更されました。case *[]byte:
のブロックで、switch sv.Kind()
による数値型・真偽値型からの変換ロジックが削除され、代わりにif b, ok := asBytes(nil, sv); ok { *d = b; return nil }
が追加されました。case *RawBytes:
のブロックで、switch sv.Kind()
による数値型・真偽値型からの変換ロジックが削除され、代わりにif b, ok := asBytes([]byte(*d)[:0], sv); ok { *d = RawBytes(b); return nil }
が追加されました。
-
asString
関数の拡張:- 既存の
asString
関数に、reflect.ValueOf(src)
を使用して引数の型を判別し、strconv
パッケージの関数(strconv.FormatInt
,strconv.FormatUint
,strconv.FormatFloat
,strconv.FormatBool
)を呼び出すロジックが追加されました。 - これにより、数値型や真偽値型から文字列への変換が
fmt.Sprintf
を介さずに行われるようになりました。
- 既存の
-
asBytes
関数の新規追加:func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool)
という新しい関数が追加されました。- この関数は、
reflect.Value
で渡された値の型(Int
,Uint
,Float32
,Float64
,Bool
,String
)に応じて、strconv.Append
系の関数を使用して、既存のバイトスライスbuf
に直接データを追記し、新しいバイトスライスを返します。 - これにより、
[]byte
やRawBytes
への変換時に、中間的な文字列生成や余分なメモリ割り当てを避けることができます。
src/pkg/database/sql/convert_test.go
TestRawBytesAllocs
テスト関数の新規追加:RawBytes
への割り当てがメモリ割り当てを発生させないことを検証するための新しいテストケースが追加されました。testing.AllocsPerRun
を使用して、様々な数値型、真偽値型、文字列型をRawBytes
に変換する際のメモリ割り当て数を計測し、期待される割り当て数(0または1)と比較しています。これにより、RawBytes
の最適化が正しく機能していることを確認しています。
これらの変更は、database/sql
パッケージの型変換ロジックの根幹に関わるものであり、パフォーマンスに大きな影響を与える部分です。
コアとなるコードの解説
このコミットの核心は、database/sql
パッケージにおけるデータ型変換の効率化にあります。
1. asString
関数の改善:
変更前:
case *string:
*d = fmt.Sprintf("%v", src)
return nil
変更後:
case *string:
*d = asString(src)
return nil
そして、asString
関数自体が拡張されました。
変更前は、asString
はstring(v)
やfmt.Sprintf("%v", src)
のような単純な変換しか行っていませんでした。
変更後:
func asString(src interface{}) string {
// ... (既存のstring, []byteのケース)
rv := reflect.ValueOf(src)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(rv.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.FormatUint(rv.Uint(), 10)
case reflect.Float64:
return strconv.FormatFloat(rv.Float(), 'g', -1, 64)
case reflect.Float32:
return strconv.FormatFloat(rv.Float(), 'g', -1, 32)
case reflect.Bool:
return strconv.FormatBool(rv.Bool())
}
return fmt.Sprintf("%v", src) // フォールバック
}
この変更の意図は、fmt.Sprintf
が内部で持つプリンタキャッシュの競合を避けることです。fmt.Sprintf
は非常に汎用的なため、内部で多くの処理を行い、特に高頻度で呼び出されるとパフォーマンスオーバーヘッドが発生します。数値や真偽値から文字列への変換はstrconv
パッケージの関数がより特化しており、高速かつ効率的です。asString
関数がこれらのstrconv
関数を直接呼び出すことで、不要なリフレクションやキャッシュアクセスを減らし、変換処理を最適化しています。
2. asBytes
関数の導入と*[]byte
, *RawBytes
へのスキャン最適化:
変更前 (*[]byte
のケース):
case *[]byte:
sv = reflect.ValueOf(src)
switch sv.Kind() {
// ... 数値型・真偽値型をfmt.Sprintfで文字列化し、[]byteに変換
case reflect.Bool, reflect.Int, ...:
*d = []byte(fmt.Sprintf("%v", src))
return nil
}
変更後 (*[]byte
のケース):
case *[]byte:
sv = reflect.ValueOf(src)
if b, ok := asBytes(nil, sv); ok {
*d = b
return nil
}
変更前 (*RawBytes
のケース):
case *RawBytes:
sv = reflect.ValueOf(src)
switch sv.Kind() {
// ... 数値型・真偽値型をfmt.Sprintfで文字列化し、RawBytesに変換
case reflect.Bool, reflect.Int, ...:
*d = RawBytes(fmt.Sprintf("%v", src))
return nil
}
変更後 (*RawBytes
のケース):
case *RawBytes:
sv = reflect.ValueOf(src)
if b, ok := asBytes([]byte(*d)[:0], sv); ok {
*d = RawBytes(b)
return nil
}
新しく導入されたasBytes
関数:
func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) {
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.AppendInt(buf, rv.Int(), 10), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.AppendUint(buf, rv.Uint(), 10), true
case reflect.Float32:
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 32), true
case reflect.Float64:
return strconv.AppendFloat(buf, rv.Float(), 'g', -1, 64), true
case reflect.Bool:
return strconv.AppendBool(buf, rv.Bool()), true
case reflect.String:
s := rv.String()
return append(buf, s...), true
}
return // ok = false
}
この変更の目的は、*[]byte
や*RawBytes
へのスキャン時に発生していた不要なメモリ割り当てとコピーを排除することです。
- 変更前は、
fmt.Sprintf
で一度文字列を生成し、その文字列を[]byte()
やRawBytes()
で再度バイトスライスに変換していました。これは、中間的な文字列オブジェクトの生成と、その後のバイトスライスへのコピーという二重のオーバーヘッドを意味します。 asBytes
関数は、strconv.Append
系の関数を利用します。これらの関数は、既存のバイトスライス(buf
)に直接データを追記する形で動作するため、新しいバイトスライスを効率的に構築できます。*[]byte
のケースでは、asBytes(nil, sv)
とすることで、新しいバイトスライスをゼロから効率的に構築します。*RawBytes
のケースでは、asBytes([]byte(*d)[:0], sv)
とすることで、*d
(つまり既存のRawBytes
)の基盤となる配列を再利用し、その先頭からデータを書き込むようにしています。[:0]
はスライスの長さを0にリセットするイディオムであり、これにより既存のメモリ領域を上書きして再利用することが可能になります。これはRawBytes
の「呼び出し元のメモリを再利用する」という設計意図を最大限に活かすための重要な最適化です。
これらの変更により、database/sql
パッケージがデータベースからデータを読み込む際のメモリフットプリントとCPUオーバーヘッドが大幅に削減され、特に大量のデータを扱うアプリケーションのパフォーマンスが向上します。
関連リンク
- Go Issue #7086: https://github.com/golang/go/issues/7086
- Go CL 50240044: https://golang.org/cl/50240044
参考にした情報源リンク
- hackernoon.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHSLwgBUw4q69zeEGZD0y04ym62QzMLR5IHdQ1mhFvfBtLmEQdn5sq9FDJgQF_5QBBya2-MZxkWrtYkyOeFX0n-4ENenzMZxRsAFMzNt5qRPhNv-1cwWZwH8noQ1lP4w7TrZe11q_9dWb-88nDhYmNsUHXaU37DUhUKO1G-Mu-RfXsppP5d1fMBLHErX3o=
- medium.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHcgH5zEGunNJDuuZtFF4W9ZxtpXacrJJSDmiEOWhNxuEtMkItwOposWkuc6DvznQOYrYl3Jf3uAzCVZ9T-EZBgYve0RGdcOxq9R9ojSuHQqaF2brv5A-sbgEixy9mbSytWQr0jUFgoUDs4Is2uVE5P9k563Oi3jd8=
- golangbridge.org: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEOUmaVVgfLeAoooJ4XhJMcz2uRF4QoZ_DEIRk83APrBLDDeiHPv0wmUvmRaIwk2tx6Rg5iq6-LPVCsA-1WdMErmyQS2G1sO9sS0WtlSmje57Kymdu45OFztR83FlAphgy6Dx6Y-PG1Bk_XAsXZWL-QAI8DFBoipCfY4roSwcHxJ9q0FNaa
- datadoghq.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG6KfOIw4-VzEpU7c986Ftj7toSOuxXCMh_Pvyq78L_NaC8A7rLCHy5KEi_pWS-CVQHHFrTZucSIOmjsfs6nJ1B169-2zYTAUOVjPASTZ54bZoQRPAllJQHl7mXP9CxfdNjy1CojYpgr6VN2TAjw3QRcqCVqxuaHtzBR8HxNzALZtjaIWv6BgnZgcEpmQZ0uWJOOvBrWlOpLH7EhmH_C_ie_8XvonuBnFvR9HJRWOC9kIOuXxIsm8BuHgrB82JIRe7sJnM=
- stackoverflow.com: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGEB0OjcXjPzeFRxNYUEjgU0TtM4VKggIiC5YygJBVGxp43OoIYy5gmjq2Yttj75KUeLe3rppqNv-P6Q_aBQTR4fJkPI3r_LGjNRVcPS-_w2JHrOMBlBZHpphYCSkS8Ch4zTIdhfTC6FeMUKMl_TdjvkEZr1P_NmxWzjA4JisCG3cAPWBDi9AhI98xdxedXqjIbrHwG
- github.com (RawBytes issue): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHCPNVyPV6C0Z4Ts3Y09pvIo4SOyEXITl1VWPrNUP7L9SJoaIrFvXwbKvywgfso0o1eqLhaQyJa9Z12z79Rohuh2lwfCi78HipqjMr21KG4B-TRwmXEzC94f4bVox16ndtQT_Hl
- github.com (allocations issue): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHVZnqC4F5tZbE41d8pZLh2UNnGIfTxE4ai-GeOwyXkASxUHcceG0yzTE6RK-pbWiZfpmgJRX4WP-nEt7YivBJ4V6D5izl4emcxJ-ljGb6fZuTSu_oe73lUy77_eHPvCDDUIMTY=