reduce allocations when binding string/time args

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
This commit is contained in:
Charlie Vieth 2024-11-05 12:01:31 -05:00
parent b3e6ac1fca
commit 0fdd7c95d8
No known key found for this signature in database
GPG Key ID: F6DBDE178E5DE3F0
3 changed files with 117 additions and 16 deletions

View File

@ -1921,26 +1921,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 {
@ -1951,20 +2015,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:
@ -1978,8 +2038,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()

17
unsafe_go120.go Normal file
View File

@ -0,0 +1,17 @@
//go:build !go1.21
// +build !go1.21
package sqlite3
import "unsafe"
// stringData is a safe version of unsafe.StringData that handles empty strings.
func stringData(s string) *byte {
if len(s) != 0 {
b := *(*[]byte)(unsafe.Pointer(&s))
return &b[0]
}
// The return value of unsafe.StringData
// is unspecified if the string is empty.
return &placeHolder[0]
}

23
unsafe_go121.go Normal file
View File

@ -0,0 +1,23 @@
//go:build go1.21
// +build go1.21
// The unsafe.StringData function was made available in Go 1.20 but it
// was not until Go 1.21 that Go was changed to interpret the Go version
// in go.mod (1.19 as of writing this) as the minimum version required
// instead of the exact version.
//
// See: https://github.com/golang/go/issues/59033
package sqlite3
import "unsafe"
// stringData is a safe version of unsafe.StringData that handles empty strings.
func stringData(s string) *byte {
if len(s) != 0 {
return unsafe.StringData(s)
}
// The return value of unsafe.StringData
// is unspecified if the string is empty.
return &placeHolder[0]
}