GoでORM利用の際のオーバーヘッドについて
はじめに
GoのORM的なライブラリには幾つか種類がありますが、例えばgorpで あるテーブルからレコードを取得するのは以下の流れとなります。
1)以下のようなテーブルと対になる構造体を定義
type TestStruct struct { ID int `db:"id"` Col1 sql.NullString `db:"col1"` Col2 sql.NullString `db:"col2"` Col3 sql.NullString `db:"col3"` Col4 sql.NullString `db:"col4"` Col5 sql.NullString `db:"col5"` Col6 sql.NullString `db:"col6"` Col7 sql.NullString `db:"col7"` Col8 sql.NullString `db:"col8"` Col9 sql.NullString `db:"col9"` Col10 sql.NullString `db:"col10"` }
(2)テーブルとのマッピングをライブラリに知らせる
dbMap = &gorp.DbMap{ Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}, } dbMap.AddTableWithName(TestBinder{}, "test").SetKeys(true, "ID")
(3) 構造体(のポインタ)をinterface{}
で受ける。そこにselectの結果をセット
tbl := TestStruct{}
dbMap.Get(&tbl, 1)
他のメジャーどころな物も、大枠一緒な印象です。
ところで、これらの一連の処理は、要件やテーブル構造によっては、パフォーマンスに問題を抱える可能性があります。
原因は、reflection
の利用です。
gorpではreflectionを使って、以下の処理を行っています。
- (2)でinterfaceから構造体の情報を取り出し、fieldのタグ情報からテーブルのカラムとのマッピング
- (3)でinterfaceから構造体の情報を取り出し、その構造体field一つずつに値のセット
このreflectionの処理がどれくらいのオーバーヘッドになるのか、計測してみました。
また、gorpでは
- (2)をselectを発行する前にあらかじめセットする方法 (以降 gorp-1)
- (2)と(3)とを、select時に同時に行う方法 (gorp-2)
があります。後者の方法をとった場合、reflectionの回数が増えるため、よりオーバーヘッドが顕著になることが予想されます。 そちらも合わせてベンチマークを取ってみたいと思います。
ベンチマーク
テストコード
以下3つのテストを用意しました。
BenchmarkFindByID
がgorpを使わないテストBenchmarkGorpFindByID
がgorpを使ったテストで上記gorp-1に該当BenchmarkGorpFindByIDDynamicMapping
がgorpを使ったテストで上記gorp-2に該当
package main import ( "database/sql" "fmt" "os" "testing" "github.com/go-gorp/gorp" _ "github.com/go-sql-driver/mysql" ) var db *sql.DB var dbMap *gorp.DbMap type TestStruct struct { ID int `db:"id"` Col1 sql.NullString `db:"col1"` Col2 sql.NullString `db:"col2"` Col3 sql.NullString `db:"col3"` Col4 sql.NullString `db:"col4"` Col5 sql.NullString `db:"col5"` Col6 sql.NullString `db:"col6"` Col7 sql.NullString `db:"col7"` Col8 sql.NullString `db:"col8"` Col9 sql.NullString `db:"col9"` Col10 sql.NullString `db:"col10"` } func init() { var err error // initialize sql.DB host := os.Getenv("MYSQL_HOST") database := os.Getenv("MYSQL_DATABASE") usr := os.Getenv("MYSQL_USER") pass := os.Getenv("MYSQL_PASSWORD") if db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", usr, pass, host, database)); err != nil { panic(err) } // initialize table db.Exec(`CREATE TABLE IF NOT EXISTS test( id INT PRIMARY KEY NOT NULL AUTO_INCREMENT, col1 VARCHAR(100), col2 VARCHAR(100), col3 VARCHAR(100), col4 VARCHAR(100), col5 VARCHAR(100), col6 VARCHAR(100), col7 VARCHAR(100), col8 VARCHAR(100), col9 VARCHAR(100), col10 VARCHAR(100) )`) db.Exec("TRUNCATE TABLE test") db.Exec(`INSERT INTO test VALUES( 1, '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' )`) // initialize gorp.DbMap dbMap = &gorp.DbMap{ Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}, } dbMap.AddTableWithName(TestStruct{}, "test").SetKeys(true, "ID") } func BenchmarkFindByID(b *testing.B) { tb := TestStruct{} for i := 0; i < b.N; i++ { rows, err := db.Query("SELECT * FROM test WHERE id = ?", 1) if err != nil { panic(err) } if !rows.Next() { panic(sql.ErrNoRows) } err = rows.Scan(&tb.ID, &tb.Col1, &tb.Col2, &tb.Col3, &tb.Col4, &tb.Col5, &tb.Col6, &tb.Col7, &tb.Col8, &tb.Col9, &tb.Col10) if err != nil { panic(err) } rows.Close() } } func BenchmarkGorpFindByID(b *testing.B) { tb := TestStruct{} for i := 0; i < b.N; i++ { if _, err := dbMap.Get(&tb, 1); err != nil { panic(err) } } } func BenchmarkGorpFindByIDDynamicMapping(b *testing.B) { tb := TestStruct{} for i := 0; i < b.N; i++ { if err := dbMap.SelectOne(&tb, "SELECT * FROM test WHERE id = ?", 1); err != nil { panic(err) } } }
カラム数 11個で測定
go test -bench . -benchmem PASS BenchmarkFindByID-5 10000 198918 ns/op 1776 B/op 46 allocs/op BenchmarkGorpFindByID-5 10000 206618 ns/op 2432 B/op 61 allocs/op BenchmarkGorpFindByIDDynamicMapping-5 5000 276175 ns/op 13104 B/op 592 allocs/op
1 > 2 > 3 の結果になっています。予想通りですね。 カラム数を増やすとどうなるでしょうか。
カラム数 101個で測定
go test -bench . -benchmem PASS BenchmarkFindByID-5 5000 355918 ns/op 14161 B/op 316 allocs/op BenchmarkGorpFindByID-5 3000 434415 ns/op 20049 B/op 421 allocs/op BenchmarkGorpFindByIDDynamicMapping-5 100 11087651 ns/op 699789 B/op 41632 allocs/op
順番は一緒ですが、その差がより顕著になっています。 特に3の爆発っぷりがすごいですね。ここまで差が出るとは正直予想外でした。
終わりに
この結果を受けてORMなんて使っちゃダメだ!などと結論付ける気は毛頭ありません。 ただし、要件によってはこのオーバーヘッドが無視できないケースは確実にあります。
先日あるバッチプログラムの速度を10倍に改善しなくてはならないという 問題に直面しました。
その際に色々対応はしたのですがどうしても一定以上の速度が出ず悩み、 仕方がないので今回のテストコードのように自前でmappingするコードに変えたところ あっさり目標を達成することができました。 (今回の記事を書くきっかけとなりました)
とはいえ、カラム数が増えるとこれを手で書くのは辛いし、コピペミスなどバグの温床にもなるので、
構造体からmappingの処理をいい感じにgo generate
してくれるツールでもそのうち作ろうかなぁと思ってます。
2016.06.15 追記 ツール作りました。よかったら使ってみてください。
GitHub - Jun-Chang/gdbinder: gdbinder is light-weight data mapper for Golang
今回は以上です。