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

[インデックス 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つの主要な問題に対処しています。

  1. fmt.Sprintfによるプリンタキャッシュの競合: ユーザーからの報告により、fmt.Sprintfが頻繁に使用されることで、内部のプリンタキャッシュに激しい競合が発生し、アプリケーションのパフォーマンスが低下していることが判明しました。
  2. *[]byteおよびRawBytesへのスキャン時の不要なメモリ割り当て: データベースからデータを読み込み、*[]byte型やRawBytes型にスキャンする際に、中間的な文字列変換や余分なメモリ割り当てが発生していました。これは特に大量のデータを扱う場合に、ガベージコレクションの負荷を増大させ、パフォーマンスに悪影響を与えていました。

これらの問題は、GoのIssue #7086として報告されており、このコミットはその修正を意図しています。

変更の背景

Goのdatabase/sqlパッケージは、データベースとのやり取りを抽象化するための標準ライブラリです。このパッケージは、様々なデータベースドライバと連携して動作し、クエリの実行結果をGoの型に変換する役割を担っています。

しかし、初期の実装では、データの型変換においてfmt.Sprintfが多用されていました。fmt.Sprintfは非常に便利で汎用的な文字列フォーマット関数ですが、その内部ではリフレクションや、フォーマット処理を高速化するための内部キャッシュ(プリンタキャッシュ)が使用されています。高負荷な環境でfmt.Sprintfが頻繁に呼び出されると、このプリンタキャッシュへのアクセスが競合し、複数のゴルーチンが同時にキャッシュにアクセスしようとすることでロックが発生し、結果としてCPU使用率の増加やスループットの低下を引き起こすことがありました。

また、データベースから取得したバイナリデータや文字列データを[]bytesql.RawBytes型にスキャンする際にも、非効率な処理が行われていました。特にsql.RawBytesは、データベースが所有するメモリバッファへの直接参照を保持することでメモリ割り当てを最小限に抑えることを意図した型ですが、実際には中間的な文字列変換や余分なコピーが発生し、その設計意図が十分に活かされていませんでした。これは、大量のデータを扱うアプリケーションにおいて、ガベージコレクションの頻度を増やし、全体的なパフォーマンスを低下させる要因となっていました。

これらのパフォーマンス上の問題は、Goアプリケーションがデータベースと連携する際のボトルネックとなり得るため、その解消が急務とされていました。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。

  1. database/sqlパッケージ: Goの標準ライブラリの一部で、SQLデータベースと対話するための汎用的なインターフェースを提供します。データベースドライバに依存しない形で、クエリの実行、結果の取得、トランザクション管理などを行うことができます。Rows.Scan()メソッドは、クエリ結果の各行の列データをGoの変数に変換して割り当てるために使用されます。

  2. fmt.Sprintf: fmtパッケージは、GoにおけるフォーマットI/Oを扱うためのパッケージです。Sprintf関数は、指定されたフォーマット文字列と引数に基づいて文字列を生成し、その結果を返します。内部的にはリフレクションを使用し、引数の型に応じて適切なフォーマット処理を行います。パフォーマンスが要求される場面では、その汎用性ゆえにオーバーヘッドが生じることがあります。特に、内部で利用されるプリンタキャッシュは、複数のゴルーチンから同時にアクセスされると競合が発生し、ボトルネックとなる可能性があります。

  3. reflectパッケージ: Goのリフレクション機能を提供するパッケージです。実行時に変数の型情報(reflect.Type)や値情報(reflect.Value)を取得・操作することができます。database/sqlパッケージでは、Rows.Scan()が任意のGoの型にデータをスキャンするためにリフレクションを広く利用しています。

  4. strconvパッケージ: 文字列と基本的なデータ型(整数、浮動小数点数、真偽値など)との間で変換を行うためのパッケージです。fmtパッケージと比較して、より特化した変換機能を提供し、多くの場合でfmt.Sprintfよりも高速かつメモリ効率が良いです。例えば、strconv.FormatIntstrconv.FormatUintstrconv.FormatFloatstrconv.FormatBoolなどがあります。

  5. sql.RawBytes: database/sqlパッケージで定義されている[]byteのエイリアス型です。この型は、データベースから読み込んだ生のバイトデータを、コピーせずに直接参照するために設計されています。これにより、特に大きなバイナリデータや文字列データを扱う際に、メモリ割り当てとコピーのオーバーヘッドを削減し、パフォーマンスを向上させることが期待されます。しかし、その利用には注意が必要で、データが有効な期間や、再利用時の挙動を正しく理解する必要があります。

  6. メモリ割り当てとガベージコレクション (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.ValueKind()に基づいて、適切な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

  1. 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 } が追加されました。
  2. asString関数の拡張:

    • 既存のasString関数に、reflect.ValueOf(src) を使用して引数の型を判別し、strconvパッケージの関数(strconv.FormatInt, strconv.FormatUint, strconv.FormatFloat, strconv.FormatBool)を呼び出すロジックが追加されました。
    • これにより、数値型や真偽値型から文字列への変換がfmt.Sprintfを介さずに行われるようになりました。
  3. asBytes関数の新規追加:

    • func asBytes(buf []byte, rv reflect.Value) (b []byte, ok bool) という新しい関数が追加されました。
    • この関数は、reflect.Valueで渡された値の型(Int, Uint, Float32, Float64, Bool, String)に応じて、strconv.Append系の関数を使用して、既存のバイトスライスbufに直接データを追記し、新しいバイトスライスを返します。
    • これにより、[]byteRawBytesへの変換時に、中間的な文字列生成や余分なメモリ割り当てを避けることができます。

src/pkg/database/sql/convert_test.go

  1. 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関数自体が拡張されました。 変更前は、asStringstring(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オーバーヘッドが大幅に削減され、特に大量のデータを扱うアプリケーションのパフォーマンスが向上します。

関連リンク

参考にした情報源リンク