From fba57a46dbee625201496d6e4049f3a39d1038ed Mon Sep 17 00:00:00 2001 From: "Israel G. Lugo" Date: Thu, 10 Nov 2022 03:55:05 +0000 Subject: [PATCH 1/2] Add benchmarks for some basic functionality. --- sqlite_test.go | 280 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 255 insertions(+), 25 deletions(-) diff --git a/sqlite_test.go b/sqlite_test.go index f9b380a..f0ab72d 100644 --- a/sqlite_test.go +++ b/sqlite_test.go @@ -42,20 +42,7 @@ func TestConn(t *testing.T) { } }() - stmt, _, err := c.PrepareTransient("CREATE TABLE bartable (foo1 string, foo2 integer);") - if err != nil { - t.Fatal(err) - } - hasRow, err := stmt.Step() - if err != nil { - t.Fatal(err) - } - if hasRow { - t.Errorf("CREATE TABLE reports having a row") - } - if err := stmt.Finalize(); err != nil { - t.Error(err) - } + mustCreateBarTable(t, c) fooVals := []string{ "bar", @@ -70,7 +57,7 @@ func TestConn(t *testing.T) { } stmt.SetText("$f1", val) stmt.SetInt64("$f2", int64(i)) - hasRow, err = stmt.Step() + hasRow, err := stmt.Step() if err != nil { t.Errorf("INSERT %q: %v", val, err) } @@ -79,7 +66,7 @@ func TestConn(t *testing.T) { } } - stmt, err = c.Prepare("SELECT foo1, foo2 FROM bartable;") + stmt, err := c.Prepare("SELECT foo1, foo2 FROM bartable;") if err != nil { t.Fatal(err) } @@ -129,6 +116,23 @@ func TestConn(t *testing.T) { } } +func mustCreateBarTable(tb testing.TB, c *sqlite.Conn) { + tb.Helper() + + stmt, _, err := c.PrepareTransient("CREATE TABLE bartable (foo1 string, foo2 integer);") + if err != nil { + tb.Fatal(err) + } + if hasRow, err := stmt.Step(); err != nil { + tb.Fatal(err) + } else if hasRow { + tb.Fatal("CREATE TABLE reports having a row") + } + if err := stmt.Finalize(); err != nil { + tb.Fatal(err) + } +} + func TestEarlyInterrupt(t *testing.T) { c, err := sqlite.OpenConn(":memory:", 0) if err != nil { @@ -143,18 +147,11 @@ func TestEarlyInterrupt(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) c.SetInterrupt(ctx.Done()) - stmt, _, err := c.PrepareTransient("CREATE TABLE bartable (foo1 string, foo2 integer);") - if err != nil { - t.Fatal(err) - } - if _, err := stmt.Step(); err != nil { - t.Fatal(err) - } - stmt.Finalize() + mustCreateBarTable(t, c) cancel() - stmt, err = c.Prepare("INSERT INTO bartable (foo1, foo2) VALUES ($f1, $f2);") + _, err = c.Prepare("INSERT INTO bartable (foo1, foo2) VALUES ($f1, $f2);") if err == nil { t.Fatal("Prepare err=nil, want prepare to fail") } @@ -864,3 +861,236 @@ func TestReturningClause(t *testing.T) { t.Fatalf("want returned fruit id to be 1, got %d", id) } } + +func BenchmarkPrepareTransientAndFinalize(b *testing.B) { + benchs := []struct { + name string + query string + }{ + { + name: "Select constant", + query: "SELECT 1", + }, + { + name: "Insert constants", + query: "INSERT INTO bartable (foo1, foo2) VALUES ('bar', 0);", + }, + { + name: "Insert with params", + query: "INSERT INTO bartable (foo1, foo2) VALUES ($f1, $f2);", + }, + } + + for _, bench := range benchs { + b.Run(bench.name, func(b *testing.B) { + c, err := sqlite.OpenConn(":memory:", 0) + if err != nil { + b.Fatal(err) + } + defer func() { + if err := c.Close(); err != nil { + b.Error(err) + } + }() + + mustCreateBarTable(b, c) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var err error + JunkStmt, JunkInt, err = c.PrepareTransient(bench.query) + if err != nil { + b.Fatal(err) + } + if err := JunkStmt.Finalize(); err != nil { + b.Fatal(err) + } + } + }) + } +} + +func BenchmarkPrepare(b *testing.B) { + benchs := []struct { + name string + query string + }{ + { + name: "Select constant", + query: "SELECT 1", + }, + { + name: "Insert constants", + query: "INSERT INTO bartable (foo1, foo2) VALUES ('bar', 0);", + }, + { + name: "Insert with params", + query: "INSERT INTO bartable (foo1, foo2) VALUES ($f1, $f2);", + }, + } + + for _, bench := range benchs { + b.Run(bench.name, func(b *testing.B) { + c, err := sqlite.OpenConn(":memory:", 0) + if err != nil { + b.Fatal(err) + } + defer func() { + if err := c.Close(); err != nil { + b.Error(err) + } + }() + + mustCreateBarTable(b, c) + + if _, err := c.Prepare(bench.query); err != nil { + b.Fatalf("Unable to prime Prepare cache with %q: %v", bench.query, err) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var err error + JunkStmt, err = c.Prepare(bench.query) + if err != nil { + b.Fatal(err) + } + } + }) + } +} + +func BenchmarkPrepareBindAndSelect(b *testing.B) { + const ( + existingText = "bar" + existingInt = 42 + ) + benchs := []struct { + name string + query string + bindInt bool + i int64 + bindText bool + s string + doReset bool + doClear bool + }{ + { + name: "Select constant", + query: "SELECT 18", + }, + { + name: "Select constant, explicit reset", + query: "SELECT 18", + doReset: true, + }, + { + name: "Select int param directly", + query: "SELECT $f1", + bindInt: true, + i: existingInt, + }, + { + name: "Select int param directly, explicit reset", + query: "SELECT $f1", + bindInt: true, + i: existingInt, + doReset: true, + }, + { + name: "Select from table", + query: "SELECT foo2 FROM bartable WHERE foo1=$f1", + bindText: true, + s: existingText, + }, + { + name: "Select from table, explicit reset and clear", + query: "SELECT foo2 FROM bartable WHERE foo1=$f1", + bindText: true, + s: existingText, + doReset: true, + doClear: true, + }, + } + + for _, bench := range benchs { + b.Run(bench.name, func(b *testing.B) { + c, err := sqlite.OpenConn(":memory:", 0) + if err != nil { + b.Fatal(err) + } + defer func() { + if err := c.Close(); err != nil { + b.Error(err) + } + }() + + mustCreateBarTable(b, c) + mustInsertIntoBarTable(b, c, existingText, existingInt) + + if _, err := c.Prepare(bench.query); err != nil { + b.Fatalf("Unable to prime Prepare cache with %q: %v", bench.query, err) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stmt, err := c.Prepare(bench.query) + if err != nil { + b.Fatal(err) + } + + if bench.bindInt { + stmt.SetInt64("$f1", bench.i) + } else if bench.bindText { + stmt.SetText("$f1", bench.s) + } + + if hasRow, err := stmt.Step(); err != nil { + b.Fatal(err) + } else if !hasRow { + b.Fatal("No rows") + } + JunkInt64 = stmt.ColumnInt64(0) + + if bench.doReset { + if err := stmt.Reset(); err != nil { + b.Fatal(err) + } + } + if bench.doClear { + if err := stmt.ClearBindings(); err != nil { + b.Fatal(err) + } + } + } + }) + } +} + +func mustInsertIntoBarTable(tb testing.TB, c *sqlite.Conn, s string, i int64) { + tb.Helper() + + stmt, _, err := c.PrepareTransient("INSERT INTO bartable (foo1, foo2) VALUES ($f1, $f2);") + if err != nil { + tb.Fatal(err) + } + defer func() { + if err := stmt.Finalize(); err != nil { + tb.Fatal(err) + } + }() + + stmt.SetText("$f1", s) + stmt.SetInt64("$f2", i) + if hasRow, err := stmt.Step(); err != nil { + tb.Fatalf("INSERT %q, %d: %v", s, i, err) + } else if hasRow { + tb.Fatalf("INSERT %q, %d: has row", s, i) + } +} + +// Variables set by benchmarks, to ensure things aren't optimized away +var ( + JunkStmt *sqlite.Stmt + JunkInt int + JunkInt64 int64 +) From d47dce01b2005fc5abaf0ae97a9a1c1b74cf9bc0 Mon Sep 17 00:00:00 2001 From: "Israel G. Lugo" Date: Thu, 10 Nov 2022 03:55:15 +0000 Subject: [PATCH 2/2] Only reset and clear bindings when necessary. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare shouldn't call Reset and ClearBindings unconditionally. The underlying C functions sqlite3_reset and sqlite3_clear_bindings functions are non-trivial and will cause some slow down. Neither of them is necessary if the user already called them manually (e.g. to avoid holding a transaction open, or to allow large parameters to be freed early). Also, ClearBindings is not necessary for statements that never had parameters bound to them. Benchmarks: name old time/op new time/op delta PrepareTransientAndFinalize/Select_constant-4 4.94µs ± 1% 4.91µs ± 1% -0.67% (p=0.014 n=10+10) PrepareTransientAndFinalize/Insert_constants-4 4.91µs ± 1% 4.81µs ± 0% -2.03% (p=0.000 n=9+10) PrepareTransientAndFinalize/Insert_with_params-4 5.86µs ± 0% 5.64µs ± 1% -3.77% (p=0.000 n=10+10) PreparePrimed/Select_constant-4 373ns ± 0% 13ns ± 0% -96.63% (p=0.000 n=10+10) PreparePrimed/Insert_constants-4 376ns ± 0% 13ns ± 0% -96.58% (p=0.000 n=10+10) PreparePrimed/Insert_with_params-4 380ns ± 0% 13ns ± 0% -96.62% (p=0.000 n=9+8) PrepareBindAndSelect/Select_constant-4 775ns ± 0% 589ns ± 1% -23.93% (p=0.000 n=9+10) PrepareBindAndSelect/Select_constant,_explicit_reset-4 953ns ± 0% 584ns ± 1% -38.71% (p=0.000 n=10+9) PrepareBindAndSelect/Select_int_param_directly-4 1.10µs ± 0% 1.11µs ± 0% +1.16% (p=0.000 n=10+8) PrepareBindAndSelect/Select_int_param_directly,_explicit_reset-4 1.24µs ± 0% 1.10µs ± 0% -11.04% (p=0.000 n=10+10) PrepareBindAndSelect/Select_from_table-4 2.24µs ± 0% 2.29µs ± 0% +1.97% (p=0.000 n=10+10) PrepareBindAndSelect/Select_from_table,_explicit_reset_and_clear-4 2.65µs ± 0% 2.44µs ± 0% -8.08% (p=0.000 n=10+10) Note the massive improvement for Prepare when already primed. Also a significant improvement for Prepare + Bind + Select when the user is explicitly doing Reset and/or Reset+ClearBindings. The +1% worsening on SELECT $f1 is likely just background variation. --- sqlite.go | 38 ++++++++++++++++++++++++++++++++++++-- sqlite_test.go | 2 +- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/sqlite.go b/sqlite.go index 415eb30..4244b97 100644 --- a/sqlite.go +++ b/sqlite.go @@ -369,10 +369,10 @@ func (conn *Conn) Prep(query string) *Stmt { // https://www.sqlite.org/c3ref/prepare.html func (conn *Conn) Prepare(query string) (*Stmt, error) { if stmt := conn.stmts[query]; stmt != nil { - if err := stmt.Reset(); err != nil { + if err := stmt.resetIfStepped(); err != nil { return nil, err } - if err := stmt.ClearBindings(); err != nil { + if err := stmt.clearBindingsIfNeeded(); err != nil { return nil, err } if conn.tracer != nil { @@ -532,6 +532,8 @@ type Stmt struct { bindErr error prepInterrupt bool // set if Prep was interrupted lastHasRow bool // last bool returned by Step + stepped bool // Step has been called more recently than Reset + hasBindings bool // has at least one non-NULL binding tracerTask TracerTask } @@ -584,9 +586,19 @@ func (stmt *Stmt) Reset() error { return stmt.conn.extreserr("Stmt.Reset(Wait)", stmt.query, res) } } + if res == C.SQLITE_OK { + stmt.stepped = false + } return stmt.conn.extreserr("Stmt.Reset", stmt.query, res) } +func (stmt *Stmt) resetIfStepped() error { + if !stmt.stepped { + return nil + } + return stmt.Reset() +} + // ClearBindings clears all bound parameter values on a statement. // // https://www.sqlite.org/c3ref/clear_bindings.html @@ -599,6 +611,13 @@ func (stmt *Stmt) ClearBindings() error { return stmt.conn.reserr("Stmt.ClearBindings", stmt.query, res) } +func (stmt *Stmt) clearBindingsIfNeeded() error { + if !stmt.hasBindings { + return nil + } + return stmt.ClearBindings() +} + // Step moves through the statement cursor using sqlite3_step. // // If a row of data is available, rowReturned is reported as true. @@ -651,8 +670,10 @@ func (stmt *Stmt) Step() (rowReturned bool, err error) { stmt.tracerTask = nil } } + stmt.stepped = true if err != nil { C.sqlite3_reset(stmt.stmt) + stmt.stepped = false } stmt.lastHasRow = rowReturned return rowReturned, err @@ -704,6 +725,12 @@ func (stmt *Stmt) findBindName(loc string, param string) int { return pos } +func (stmt *Stmt) setHasBindings(res C.int) { + if res == C.SQLITE_OK { + stmt.hasBindings = true + } +} + // DataCount returns the number of columns in the current row of the result // set of prepared statement. // @@ -763,6 +790,7 @@ func (stmt *Stmt) BindInt64(param int, value int64) { } res := C.sqlite3_bind_int64(stmt.stmt, C.int(param), C.sqlite3_int64(value)) stmt.handleBindErr("BindInt64", res) + stmt.setHasBindings(res) } // BindBool binds value (as an integer 0 or 1) to a numbered stmt parameter. @@ -780,6 +808,7 @@ func (stmt *Stmt) BindBool(param int, value bool) { } res := C.sqlite3_bind_int64(stmt.stmt, C.int(param), C.sqlite3_int64(v)) stmt.handleBindErr("BindBool", res) + stmt.setHasBindings(res) } // BindBytes binds value to a numbered stmt parameter. @@ -808,6 +837,7 @@ func (stmt *Stmt) BindBytes(param int, value []byte) { runtime.KeepAlive(value) // Ensure that value is not GC'd during the above C call. } stmt.handleBindErr("BindBytes", res) + stmt.setHasBindings(res) } var emptyCstr = C.CString("") @@ -831,6 +861,7 @@ func (stmt *Stmt) BindText(param int, value string) { } res := C.sqlite3_bind_text(stmt.stmt, C.int(param), v, C.int(len(value)), free) stmt.handleBindErr("BindText", res) + stmt.setHasBindings(res) } // BindFloat binds value to a numbered stmt parameter. @@ -844,6 +875,7 @@ func (stmt *Stmt) BindFloat(param int, value float64) { } res := C.sqlite3_bind_double(stmt.stmt, C.int(param), C.double(value)) stmt.handleBindErr("BindFloat", res) + stmt.setHasBindings(res) } // BindNull binds an SQL NULL value to a numbered stmt parameter. @@ -857,6 +889,7 @@ func (stmt *Stmt) BindNull(param int) { } res := C.sqlite3_bind_null(stmt.stmt, C.int(param)) stmt.handleBindErr("BindNull", res) + stmt.setHasBindings(res) } // BindZeroBlob binds a blob of zeros of length len to a numbered stmt @@ -871,6 +904,7 @@ func (stmt *Stmt) BindZeroBlob(param int, len int64) { } res := C.sqlite3_bind_zeroblob64(stmt.stmt, C.int(param), C.sqlite3_uint64(len)) stmt.handleBindErr("BindZeroBlob", res) + stmt.setHasBindings(res) } // SetInt64 binds an int64 to a parameter using a column name. diff --git a/sqlite_test.go b/sqlite_test.go index f0ab72d..a4b9e18 100644 --- a/sqlite_test.go +++ b/sqlite_test.go @@ -910,7 +910,7 @@ func BenchmarkPrepareTransientAndFinalize(b *testing.B) { } } -func BenchmarkPrepare(b *testing.B) { +func BenchmarkPreparePrimed(b *testing.B) { benchs := []struct { name string query string