Skip to content

Commit 6915212

Browse files
committed
Fix exponential memory allocation in SQLiteConn.Exec
This commit fixes SQLiteConn.Exec to not duplicate the provided SQL statement and to pass the Go string to libsqlite3 instead of creating a C string copy and passing that. The issue with the previous implementation was that it would create a new C string copy of the original Go string each time it stepped through a SQL statement. This led to exponential memory usage when Exec'ing an argument that contained multiple SQL statements (which is common when initializing a database from a SQL dump). This commit is a slimmed down version of PR mattn#1133: mattn#1133 ``` goos: darwin goarch: arm64 pkg: github.com/mattn/go-sqlite3 cpu: Apple M1 Max │ b.txt │ n.txt │ │ sec/op │ sec/op vs base │ Suite/BenchmarkExec-10 1.278µ ± 1% 1.022µ ± 2% -19.99% (p=0.000 n=10) Suite/BenchmarkExecStep-10 1762.9µ ± 0% 900.9µ ± 1% -48.90% (p=0.000 n=10) geomean 47.47µ 30.35µ -36.06% │ b.txt │ n.txt │ │ B/op │ B/op vs base │ Suite/BenchmarkExec-10 128.0 ± 0% 120.0 ± 0% -6.25% (p=0.000 n=10) Suite/BenchmarkExecStep-10 5279.94Ki ± 0% 31.34Ki ± 0% -99.41% (p=0.000 n=10) geomean 25.69Ki 1.916Ki -92.54% │ b.txt │ n.txt │ │ allocs/op │ allocs/op vs base │ Suite/BenchmarkExec-10 7.000 ± 0% 6.000 ± 0% -14.29% (p=0.000 n=10) Suite/BenchmarkExecStep-10 7.000k ± 0% 3.003k ± 0% -57.10% (p=0.000 n=10) geomean 221.4 134.2 -39.36% ```
1 parent b3e6ac1 commit 6915212

File tree

4 files changed

+106
-22
lines changed

4 files changed

+106
-22
lines changed

sqlite3.go

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ _sqlite3_prepare_v2_internal(sqlite3 *db, const char *zSql, int nBytes, sqlite3_
137137
}
138138
#endif
139139
140+
static int _sqlite3_prepare_v2(sqlite3 *db, const char *zSql, int nBytes, sqlite3_stmt **ppStmt, int *oBytes) {
141+
const char *tail = NULL;
142+
int rv = _sqlite3_prepare_v2_internal(db, zSql, nBytes, ppStmt, &tail);
143+
if (rv != SQLITE_OK) {
144+
return rv;
145+
}
146+
if (tail == NULL) {
147+
return rv; // NB: this should not happen
148+
}
149+
// Set oBytes to the number of bytes consumed instead of using the **pzTail
150+
// out param since that requires storing a Go pointer in a C pointer, which
151+
// is not allowed by CGO and will cause runtime.cgoCheckPointer to fail.
152+
*oBytes = tail - zSql;
153+
return rv;
154+
}
155+
140156
void _sqlite3_result_text(sqlite3_context* ctx, const char* s) {
141157
sqlite3_result_text(ctx, s, -1, &free);
142158
}
@@ -858,51 +874,61 @@ func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, err
858874
}
859875

860876
func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
861-
start := 0
877+
var (
878+
stmtArgs []driver.NamedValue
879+
start int
880+
s SQLiteStmt // escapes to the heap so reuse it
881+
sz C.int // number of query bytes consumed: escapes to the heap
882+
)
883+
query = strings.TrimSpace(query)
862884
for {
863-
s, err := c.prepare(ctx, query)
864-
if err != nil {
865-
return nil, err
885+
s = SQLiteStmt{c: c} // reset
886+
sz = 0
887+
rv := C._sqlite3_prepare_v2(c.db, (*C.char)(unsafe.Pointer(stringData(query))),
888+
C.int(len(query)), &s.s, &sz)
889+
if rv != C.SQLITE_OK {
890+
return nil, c.lastError()
866891
}
892+
query = strings.TrimSpace(query[sz:])
893+
867894
var res driver.Result
868-
if s.(*SQLiteStmt).s != nil {
869-
stmtArgs := make([]driver.NamedValue, 0, len(args))
895+
if s.s != nil {
870896
na := s.NumInput()
871897
if len(args)-start < na {
872-
s.Close()
898+
s.finalize()
873899
return nil, fmt.Errorf("not enough args to execute query: want %d got %d", na, len(args))
874900
}
875901
// consume the number of arguments used in the current
876902
// statement and append all named arguments not
877903
// contained therein
878-
if len(args[start:start+na]) > 0 {
879-
stmtArgs = append(stmtArgs, args[start:start+na]...)
880-
for i := range args {
881-
if (i < start || i >= na) && args[i].Name != "" {
882-
stmtArgs = append(stmtArgs, args[i])
883-
}
884-
}
885-
for i := range stmtArgs {
886-
stmtArgs[i].Ordinal = i + 1
904+
if stmtArgs == nil {
905+
stmtArgs = make([]driver.NamedValue, 0, na)
906+
}
907+
stmtArgs = append(stmtArgs[:0], args[start:start+na]...)
908+
for i := range args {
909+
if (i < start || i >= na) && args[i].Name != "" {
910+
stmtArgs = append(stmtArgs, args[i])
887911
}
888912
}
889-
res, err = s.(*SQLiteStmt).exec(ctx, stmtArgs)
913+
for i := range stmtArgs {
914+
stmtArgs[i].Ordinal = i + 1
915+
}
916+
var err error
917+
res, err = s.exec(ctx, stmtArgs)
890918
if err != nil && err != driver.ErrSkip {
891-
s.Close()
919+
s.finalize()
892920
return nil, err
893921
}
894922
start += na
895923
}
896-
tail := s.(*SQLiteStmt).t
897-
s.Close()
898-
if tail == "" {
924+
s.finalize()
925+
if len(query) == 0 {
899926
if res == nil {
900927
// https://github.com/mattn/go-sqlite3/issues/963
901928
res = &SQLiteResult{0, 0}
902929
}
903930
return res, nil
904931
}
905-
query = tail
906932
}
907933
}
908934

@@ -1914,6 +1940,13 @@ func (s *SQLiteStmt) Close() error {
19141940
return nil
19151941
}
19161942

1943+
func (s *SQLiteStmt) finalize() {
1944+
if s.s != nil {
1945+
C.sqlite3_finalize(s.s)
1946+
s.s = nil
1947+
}
1948+
}
1949+
19171950
// NumInput return a number of parameters.
19181951
func (s *SQLiteStmt) NumInput() int {
19191952
return int(C.sqlite3_bind_parameter_count(s.s))

sqlite3_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2106,6 +2106,7 @@ var tests = []testing.InternalTest{
21062106

21072107
var benchmarks = []testing.InternalBenchmark{
21082108
{Name: "BenchmarkExec", F: benchmarkExec},
2109+
{Name: "BenchmarkExecStep", F: benchmarkExecStep},
21092110
{Name: "BenchmarkQuery", F: benchmarkQuery},
21102111
{Name: "BenchmarkParams", F: benchmarkParams},
21112112
{Name: "BenchmarkStmt", F: benchmarkStmt},
@@ -2465,6 +2466,16 @@ func benchmarkExec(b *testing.B) {
24652466
}
24662467
}
24672468

2469+
var largeSelectStmt = strings.Repeat("select 1;\n", 1_000)
2470+
2471+
func benchmarkExecStep(b *testing.B) {
2472+
for n := 0; n < b.N; n++ {
2473+
if _, err := db.Exec(largeSelectStmt); err != nil {
2474+
b.Fatal(err)
2475+
}
2476+
}
2477+
}
2478+
24682479
// benchmarkQuery is benchmark for query
24692480
func benchmarkQuery(b *testing.B) {
24702481
for i := 0; i < b.N; i++ {

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)