Skip to content

Commit 6db1eec

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. goos: darwin goarch: arm64 pkg: github.com/mattn/go-sqlite3 cpu: Apple M4 Pro │ base.10.txt │ new.10.txt │ │ sec/op │ sec/op vs base │ Suite/BenchmarkExec-14 775.1n ± 1% 769.8n ± 1% -0.69% (p=0.007 n=10) Suite/BenchmarkQuery-14 2.058µ ± 1% 2.047µ ± 1% -0.56% (p=0.002 n=10) Suite/BenchmarkParams-14 2.269µ ± 0% 2.247µ ± 1% -0.97% (p=0.001 n=10) Suite/BenchmarkStmt-14 1.518µ ± 1% 1.490µ ± 1% -1.91% (p=0.000 n=10) Suite/BenchmarkRows-14 77.14µ ± 1% 76.87µ ± 1% ~ (p=0.315 n=10) Suite/BenchmarkStmtRows-14 76.02µ ± 1% 76.02µ ± 1% ~ (p=0.971 n=10) Suite/BenchmarkQueryParallel-14 1.059µ ± 1% 1.054µ ± 1% ~ (p=0.492 n=10) geomean 4.442µ 4.411µ -0.70% │ base.10.txt │ new.10.txt │ │ B/op │ B/op vs base │ Suite/BenchmarkExec-14 128.0 ± 0% 128.0 ± 0% ~ (p=1.000 n=10) ¹ 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) Suite/BenchmarkRows-14 9.305Ki ± 0% 9.305Ki ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkStmtRows-14 9.289Ki ± 0% 9.289Ki ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkQueryParallel-14 592.0 ± 0% 592.0 ± 0% ~ (p=1.000 n=10) ¹ geomean 1.222Ki 1.184Ki -3.08% ¹ all samples are equal │ base.10.txt │ new.10.txt │ │ allocs/op │ allocs/op vs base │ Suite/BenchmarkExec-14 7.000 ± 0% 7.000 ± 0% ~ (p=1.000 n=10) ¹ 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) Suite/BenchmarkRows-14 525.0 ± 0% 525.0 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkStmtRows-14 524.0 ± 0% 524.0 ± 0% ~ (p=1.000 n=10) ¹ Suite/BenchmarkQueryParallel-14 16.00 ± 0% 16.00 ± 0% ~ (p=1.000 n=10) ¹ geomean 46.62 45.56 -2.26% ¹ all samples are equal
1 parent 294c2ce commit 6db1eec

File tree

3 files changed

+117
-16
lines changed

3 files changed

+117
-16
lines changed

sqlite3.go

+77-16
Original file line numberDiff line numberDiff line change
@@ -1921,26 +1921,90 @@ func (s *SQLiteStmt) NumInput() int {
19211921

19221922
var placeHolder = []byte{0}
19231923

1924+
func hasNamedArgs(args []driver.NamedValue) bool {
1925+
for _, v := range args {
1926+
if v.Name != "" {
1927+
return true
1928+
}
1929+
}
1930+
return false
1931+
}
1932+
19241933
func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
19251934
rv := C.sqlite3_reset(s.s)
19261935
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
19271936
return s.c.lastError()
19281937
}
19291938

1939+
if hasNamedArgs(args) {
1940+
return s.bindIndices(args)
1941+
}
1942+
1943+
for _, arg := range args {
1944+
n := C.int(arg.Ordinal)
1945+
switch v := arg.Value.(type) {
1946+
case nil:
1947+
rv = C.sqlite3_bind_null(s.s, n)
1948+
case string:
1949+
p := stringData(v)
1950+
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(v)))
1951+
case int64:
1952+
rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
1953+
case bool:
1954+
val := 0
1955+
if v {
1956+
val = 1
1957+
}
1958+
rv = C.sqlite3_bind_int(s.s, n, C.int(val))
1959+
case float64:
1960+
rv = C.sqlite3_bind_double(s.s, n, C.double(v))
1961+
case []byte:
1962+
if v == nil {
1963+
rv = C.sqlite3_bind_null(s.s, n)
1964+
} else {
1965+
ln := len(v)
1966+
if ln == 0 {
1967+
v = placeHolder
1968+
}
1969+
rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(&v[0]), C.int(ln))
1970+
}
1971+
case time.Time:
1972+
ts := v.Format(SQLiteTimestampFormats[0])
1973+
p := stringData(ts)
1974+
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(ts)))
1975+
}
1976+
if rv != C.SQLITE_OK {
1977+
return s.c.lastError()
1978+
}
1979+
}
1980+
return nil
1981+
}
1982+
1983+
func (s *SQLiteStmt) bindIndices(args []driver.NamedValue) error {
1984+
// Find the longest named parameter name.
1985+
n := 0
1986+
for _, v := range args {
1987+
if m := len(v.Name); m > n {
1988+
n = m
1989+
}
1990+
}
1991+
buf := make([]byte, 0, n+2) // +2 for placeholder and null terminator
1992+
19301993
bindIndices := make([][3]int, len(args))
1931-
prefixes := []string{":", "@", "$"}
19321994
for i, v := range args {
19331995
bindIndices[i][0] = args[i].Ordinal
19341996
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))
1997+
for j, c := range []byte{':', '@', '$'} {
1998+
buf = append(buf[:0], c)
1999+
buf = append(buf, v.Name...)
2000+
buf = append(buf, 0)
2001+
bindIndices[i][j] = int(C.sqlite3_bind_parameter_index(s.s, (*C.char)(unsafe.Pointer(&buf[0]))))
19392002
}
19402003
args[i].Ordinal = bindIndices[i][0]
19412004
}
19422005
}
19432006

2007+
var rv C.int
19442008
for i, arg := range args {
19452009
for j := range bindIndices[i] {
19462010
if bindIndices[i][j] == 0 {
@@ -1951,20 +2015,16 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
19512015
case nil:
19522016
rv = C.sqlite3_bind_null(s.s, n)
19532017
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-
}
2018+
p := stringData(v)
2019+
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(v)))
19602020
case int64:
19612021
rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
19622022
case bool:
2023+
val := 0
19632024
if v {
1964-
rv = C.sqlite3_bind_int(s.s, n, 1)
1965-
} else {
1966-
rv = C.sqlite3_bind_int(s.s, n, 0)
2025+
val = 1
19672026
}
2027+
rv = C.sqlite3_bind_int(s.s, n, C.int(val))
19682028
case float64:
19692029
rv = C.sqlite3_bind_double(s.s, n, C.double(v))
19702030
case []byte:
@@ -1978,8 +2038,9 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
19782038
rv = C._sqlite3_bind_blob(s.s, n, unsafe.Pointer(&v[0]), C.int(ln))
19792039
}
19802040
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)))
2041+
ts := v.Format(SQLiteTimestampFormats[0])
2042+
p := stringData(ts)
2043+
rv = C._sqlite3_bind_text(s.s, n, (*C.char)(unsafe.Pointer(p)), C.int(len(ts)))
19832044
}
19842045
if rv != C.SQLITE_OK {
19852046
return s.c.lastError()

unsafe_go120.go

+17
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

+23
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)