Skip to content

Commit 833702e

Browse files
committed
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. It also changes bind to only reset the sqlite3 statement when necessary - previously the statement was always reset even on first use (this yields a 3-4% performance boost). goos: darwin goarch: arm64 pkg: github.com/mattn/go-sqlite3 cpu: Apple M4 Pro │ y1.txt │ y2.txt │ │ sec/op │ sec/op vs base │ Suite/BenchmarkQuery-14 2.080µ ± 3% 1.995µ ± 0% -4.13% (p=0.000 n=10) Suite/BenchmarkParams-14 2.282µ ± 1% 2.181µ ± 2% -4.43% (p=0.000 n=10) Suite/BenchmarkStmt-14 1.537µ ± 1% 1.489µ ± 1% -3.16% (p=0.000 n=10) geomean 1.939µ 1.864µ -3.91% │ y1.txt │ y2.txt │ │ B/op │ B/op vs base │ Suite/BenchmarkQuery-14 688.0 ± 0% 688.0 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkParams-14 1104.0 ± 0% 1000.0 ± 0% -9.42% (p=0.000 n=10) Suite/BenchmarkStmt-14 920.0 ± 0% 816.0 ± 0% -11.30% (p=0.000 n=10) geomean 887.4 824.9 -7.04% ¹ all samples are equal │ y1.txt │ y2.txt │ │ allocs/op │ allocs/op vs base │ Suite/BenchmarkQuery-14 23.00 ± 0% 23.00 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkParams-14 27.00 ± 0% 25.00 ± 0% -7.41% (p=0.000 n=10) Suite/BenchmarkStmt-14 25.00 ± 0% 23.00 ± 0% -8.00% (p=0.000 n=10) geomean 24.95 23.65 -5.20% ¹ all samples are equal
1 parent 7658c06 commit 833702e

File tree

3 files changed

+128
-19
lines changed

3 files changed

+128
-19
lines changed

sqlite3.go

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ type SQLiteStmt struct {
382382
t string
383383
closed bool
384384
cls bool // True if the statement was created by SQLiteConn.Query
385+
reset bool // True if the statement needs to reset before reuse
385386
}
386387

387388
// SQLiteResult implements sql.Result.
@@ -1921,26 +1922,97 @@ func (s *SQLiteStmt) NumInput() int {
19211922

19221923
var placeHolder = []byte{0}
19231924

1925+
func hasNamedArgs(args []driver.NamedValue) bool {
1926+
for _, v := range args {
1927+
if v.Name != "" {
1928+
return true
1929+
}
1930+
}
1931+
return false
1932+
}
1933+
19241934
func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
1925-
rv := C.sqlite3_reset(s.s)
1926-
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
1927-
return s.c.lastError()
1935+
if s.reset {
1936+
// The statement was previously used so we need to reset it.
1937+
rv := C.sqlite3_reset(s.s)
1938+
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
1939+
return s.c.lastError()
1940+
}
1941+
} else {
1942+
// First call to bind, future calls will need to reset the statement.
1943+
s.reset = true
1944+
}
1945+
1946+
if hasNamedArgs(args) {
1947+
return s.bindIndices(args)
1948+
}
1949+
1950+
var rv C.int
1951+
for _, arg := range args {
1952+
n := C.int(arg.Ordinal)
1953+
switch v := arg.Value.(type) {
1954+
case nil:
1955+
rv = C.sqlite3_bind_null(s.s, n)
1956+
case string:
1957+
p := stringData(v)
1958+
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(v)))
1959+
case int64:
1960+
rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
1961+
case bool:
1962+
val := 0
1963+
if v {
1964+
val = 1
1965+
}
1966+
rv = C.sqlite3_bind_int(s.s, n, C.int(val))
1967+
case float64:
1968+
rv = C.sqlite3_bind_double(s.s, n, C.double(v))
1969+
case []byte:
1970+
if v == nil {
1971+
rv = C.sqlite3_bind_null(s.s, n)
1972+
} else {
1973+
ln := len(v)
1974+
if ln == 0 {
1975+
v = placeHolder
1976+
}
1977+
rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(&v[0]), C.int(ln))
1978+
}
1979+
case time.Time:
1980+
ts := v.Format(SQLiteTimestampFormats[0])
1981+
p := stringData(ts)
1982+
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(ts)))
1983+
}
1984+
if rv != C.SQLITE_OK {
1985+
return s.c.lastError()
1986+
}
19281987
}
1988+
return nil
1989+
}
1990+
1991+
func (s *SQLiteStmt) bindIndices(args []driver.NamedValue) error {
1992+
// Find the longest named parameter name.
1993+
n := 0
1994+
for _, v := range args {
1995+
if m := len(v.Name); m > n {
1996+
n = m
1997+
}
1998+
}
1999+
buf := make([]byte, 0, n+2) // +2 for placeholder and null terminator
19292000

19302001
bindIndices := make([][3]int, len(args))
1931-
prefixes := []string{":", "@", "$"}
19322002
for i, v := range args {
19332003
bindIndices[i][0] = args[i].Ordinal
19342004
if v.Name != "" {
1935-
for j := range prefixes {
1936-
cname := C.CString(prefixes[j] + v.Name)
1937-
bindIndices[i][j] = int(C.sqlite3_bind_parameter_index(s.s, cname))
1938-
C.free(unsafe.Pointer(cname))
2005+
for j, c := range []byte{':', '@', '$'} {
2006+
buf = append(buf[:0], c)
2007+
buf = append(buf, v.Name...)
2008+
buf = append(buf, 0)
2009+
bindIndices[i][j] = int(C.sqlite3_bind_parameter_index(s.s, (*C.char)(unsafe.Pointer(&buf[0]))))
19392010
}
19402011
args[i].Ordinal = bindIndices[i][0]
19412012
}
19422013
}
19432014

2015+
var rv C.int
19442016
for i, arg := range args {
19452017
for j := range bindIndices[i] {
19462018
if bindIndices[i][j] == 0 {
@@ -1951,20 +2023,16 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
19512023
case nil:
19522024
rv = C.sqlite3_bind_null(s.s, n)
19532025
case string:
1954-
if len(v) == 0 {
1955-
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&placeHolder[0])), C.int(0))
1956-
} else {
1957-
b := []byte(v)
1958-
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&b[0])), C.int(len(b)))
1959-
}
2026+
p := stringData(v)
2027+
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(v)))
19602028
case int64:
19612029
rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
19622030
case bool:
2031+
val := 0
19632032
if v {
1964-
rv = C.sqlite3_bind_int(s.s, n, 1)
1965-
} else {
1966-
rv = C.sqlite3_bind_int(s.s, n, 0)
2033+
val = 1
19672034
}
2035+
rv = C.sqlite3_bind_int(s.s, n, C.int(val))
19682036
case float64:
19692037
rv = C.sqlite3_bind_double(s.s, n, C.double(v))
19702038
case []byte:
@@ -1978,8 +2046,9 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
19782046
rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(&v[0]), C.int(ln))
19792047
}
19802048
case time.Time:
1981-
b := []byte(v.Format(SQLiteTimestampFormats[0]))
1982-
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(&b[0])), C.int(len(b)))
2049+
ts := v.Format(SQLiteTimestampFormats[0])
2050+
p := stringData(ts)
2051+
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(ts)))
19832052
}
19842053
if rv != C.SQLITE_OK {
19852054
return s.c.lastError()

unsafe_go120.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//go:build !go1.21
2+
// +build !go1.21
3+
4+
package sqlite3
5+
6+
import "unsafe"
7+
8+
// stringData is a safe version of unsafe.StringData that handles empty strings.
9+
func stringData(s string) *byte {
10+
if len(s) != 0 {
11+
b := *(*[]byte)(unsafe.Pointer(&s))
12+
return &b[0]
13+
}
14+
// The return value of unsafe.StringData
15+
// is unspecified if the string is empty.
16+
return &placeHolder[0]
17+
}

unsafe_go121.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//go:build go1.21
2+
// +build go1.21
3+
4+
// The unsafe.StringData function was made available in Go 1.20 but it
5+
// was not until Go 1.21 that Go was changed to interpret the Go version
6+
// in go.mod (1.19 as of writing this) as the minimum version required
7+
// instead of the exact version.
8+
//
9+
// See: https://github.com/golang/go/issues/59033
10+
11+
package sqlite3
12+
13+
import "unsafe"
14+
15+
// stringData is a safe version of unsafe.StringData that handles empty strings.
16+
func stringData(s string) *byte {
17+
if len(s) != 0 {
18+
return unsafe.StringData(s)
19+
}
20+
// The return value of unsafe.StringData
21+
// is unspecified if the string is empty.
22+
return &placeHolder[0]
23+
}

0 commit comments

Comments
 (0)