From 011926c5b6222a573807b937abf171b7b4ccf915 Mon Sep 17 00:00:00 2001 From: Charlie Vieth Date: Tue, 5 Nov 2024 12:01:31 -0500 Subject: [PATCH] reduce allocations when binding string/time args MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit reduces the number of allocations required to bind args by eliminating string to byte slice conversions for string and time.Time types and by only checking for bind parameters if any of the driver.NamedValue args are named. goos: darwin goarch: arm64 pkg: github.com/mattn/go-sqlite3 cpu: Apple M1 Max │ b.10.txt │ x7.10.txt │ │ sec/op │ sec/op vs base │ CustomFunctions-10 3.230µ ± 1% 3.253µ ± 1% +0.73% (p=0.022 n=10) Suite/BenchmarkExec-10 1.240µ ± 0% 1.231µ ± 1% ~ (p=0.210 n=10) Suite/BenchmarkQuery-10 3.892µ ± 1% 3.854µ ± 1% -0.96% (p=0.009 n=10) Suite/BenchmarkParams-10 4.203µ ± 1% 4.163µ ± 1% -0.94% (p=0.011 n=10) Suite/BenchmarkStmt-10 2.814µ ± 1% 2.763µ ± 1% -1.81% (p=0.000 n=10) Suite/BenchmarkRows-10 131.2µ ± 1% 130.7µ ± 0% -0.40% (p=0.035 n=10) Suite/BenchmarkStmtRows-10 131.0µ ± 1% 128.9µ ± 1% -1.59% (p=0.043 n=10) geomean 8.485µ 8.416µ -0.82% │ b.10.txt │ x7.10.txt │ │ B/op │ B/op vs base │ CustomFunctions-10 568.0 ± 0% 568.0 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkExec-10 128.0 ± 0% 128.0 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkQuery-10 688.0 ± 0% 688.0 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkParams-10 1104.0 ± 0% 1000.0 ± 0% -9.42% (p=0.000 n=10) Suite/BenchmarkStmt-10 920.0 ± 0% 816.0 ± 0% -11.30% (p=0.000 n=10) Suite/BenchmarkRows-10 9.305Ki ± 0% 9.305Ki ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkStmtRows-10 9.289Ki ± 0% 9.289Ki ± 0% ~ (p=1.000 n=10) ¹ geomean 1.215Ki 1.177Ki -3.08% ¹ all samples are equal │ b.10.txt │ x7.10.txt │ │ allocs/op │ allocs/op vs base │ CustomFunctions-10 18.00 ± 0% 18.00 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkExec-10 7.000 ± 0% 7.000 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkQuery-10 23.00 ± 0% 23.00 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkParams-10 27.00 ± 0% 25.00 ± 0% -7.41% (p=0.000 n=10) Suite/BenchmarkStmt-10 25.00 ± 0% 23.00 ± 0% -8.00% (p=0.000 n=10) Suite/BenchmarkRows-10 525.0 ± 0% 525.0 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkStmtRows-10 524.0 ± 0% 524.0 ± 0% ~ (p=1.000 n=10) ¹ geomean 47.41 46.33 -2.26% ¹ all samples are equal --- sqlite3.go | 93 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/sqlite3.go b/sqlite3.go index e6b8c166..281bd485 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -2182,26 +2182,90 @@ func (s *SQLiteStmt) NumInput() int { var placeHolder = []byte{0} +func hasNamedArgs(args []driver.NamedValue) bool { + for _, v := range args { + if v.Name != "" { + return true + } + } + return false +} + func (s *SQLiteStmt) bind(args []driver.NamedValue) error { rv := C.sqlite3_reset(s.s) if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE { return s.c.lastError() } + if hasNamedArgs(args) { + return s.bindIndices(args) + } + + for _, arg := range args { + n := C.int(arg.Ordinal) + switch v := arg.Value.(type) { + case nil: + rv = C.sqlite3_bind_null(s.s, n) + case string: + p := stringData(v) + rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(v))) + case int64: + rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v)) + case bool: + val := 0 + if v { + val = 1 + } + rv = C.sqlite3_bind_int(s.s, n, C.int(val)) + case float64: + rv = C.sqlite3_bind_double(s.s, n, C.double(v)) + case []byte: + if v == nil { + rv = C.sqlite3_bind_null(s.s, n) + } else { + ln := len(v) + if ln == 0 { + v = placeHolder + } + rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(&v[0]), C.int(ln)) + } + case time.Time: + ts := v.Format(SQLiteTimestampFormats[0]) + p := stringData(ts) + rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(ts))) + } + if rv != C.SQLITE_OK { + return s.c.lastError() + } + } + return nil +} + +func (s *SQLiteStmt) bindIndices(args []driver.NamedValue) error { + // Find the longest named parameter name. + n := 0 + for _, v := range args { + if m := len(v.Name); m > n { + n = m + } + } + buf := make([]byte, 0, n+2) // +2 for placeholder and null terminator + bindIndices := make([][3]int, len(args)) - prefixes := []string{":", "@", "$"} for i, v := range args { bindIndices[i][0] = args[i].Ordinal if v.Name != "" { - for j := range prefixes { - cname := C.CString(prefixes[j] + v.Name) - bindIndices[i][j] = int(C.sqlite3_bind_parameter_index(s.s, cname)) - C.free(unsafe.Pointer(cname)) + for j, c := range []byte{':', '@', '$'} { + buf = append(buf[:0], c) + buf = append(buf, v.Name...) + buf = append(buf, 0) + bindIndices[i][j] = int(C.sqlite3_bind_parameter_index(s.s, (*C.char)(unsafe.Pointer(&buf[0])))) } args[i].Ordinal = bindIndices[i][0] } } + var rv C.int for i, arg := range args { for j := range bindIndices[i] { if bindIndices[i][j] == 0 { @@ -2212,20 +2276,16 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error { case nil: rv = C.sqlite3_bind_null(s.s, n) case string: - if len(v) == 0 { - rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&placeHolder[0])), C.int(0)) - } else { - b := []byte(v) - rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&b[0])), C.int(len(b))) - } + p := stringData(v) + rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(v))) case int64: rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v)) case bool: + val := 0 if v { - rv = C.sqlite3_bind_int(s.s, n, 1) - } else { - rv = C.sqlite3_bind_int(s.s, n, 0) + val = 1 } + rv = C.sqlite3_bind_int(s.s, n, C.int(val)) case float64: rv = C.sqlite3_bind_double(s.s, n, C.double(v)) case []byte: @@ -2239,8 +2299,9 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error { rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(&v[0]), C.int(ln)) } case time.Time: - b := []byte(v.Format(SQLiteTimestampFormats[0])) - rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&b[0])), C.int(len(b))) + ts := v.Format(SQLiteTimestampFormats[0]) + p := stringData(ts) + rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(ts))) } if rv != C.SQLITE_OK { return s.c.lastError()