reduce CGO call overhead for exec and bind paths
- Add _sqlite3_exec_no_args() C function that combines prepare+step+finalize into a single CGO crossing for parameterless exec (most common case) - Add _sqlite3_reset_clear() C function that combines sqlite3_reset and sqlite3_clear_bindings into a single CGO crossing - Use semaphore channel instead of result struct channel in context-aware exec/Next paths to reduce allocations - Use time.AppendFormat with stack buffer to avoid heap allocation in time.Time binding - Optimize bindNamedIndices to reuse a single buffer instead of 3 separate C.CString allocations - Remove intermediate bindIndices slice allocation in named parameter binding path - Pass explicit query length to sqlite3_prepare_v2 to avoid C-side strlen benchstat (n=8): BenchmarkExec: -29.44% sec/op, -50% B/op, -33% allocs/op BenchmarkQuery: -9.83% sec/op BenchmarkParams: -6.38% sec/op geomean: -6.72% sec/op
This commit is contained in:
209
sqlite3.go
209
sqlite3.go
@@ -78,6 +78,16 @@ _sqlite3_bind_blob(sqlite3_stmt *stmt, int n, void *p, int np) {
|
||||
return sqlite3_bind_blob(stmt, n, p, np, SQLITE_TRANSIENT);
|
||||
}
|
||||
|
||||
static int
|
||||
_sqlite3_prepare_v2_nolen(sqlite3 *db, const char *zSql, int nBytes, sqlite3_stmt **ppStmt, const char **pzTail)
|
||||
{
|
||||
#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY
|
||||
return _sqlite3_prepare_v2_blocking(db, zSql, nBytes, ppStmt, pzTail);
|
||||
#else
|
||||
return sqlite3_prepare_v2(db, zSql, nBytes, ppStmt, pzTail);
|
||||
#endif
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int typ;
|
||||
sqlite3_int64 i64;
|
||||
@@ -126,6 +136,50 @@ _sqlite3_exec(sqlite3* db, const char* pcmd, long long* rowid, long long* change
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Combined prepare+step+finalize for simple exec without parameters.
|
||||
// Reduces CGO crossings from ~6 to 1 for the common no-args exec case.
|
||||
static int
|
||||
_sqlite3_exec_no_args(sqlite3* db, const char* zSql, int nBytes, long long* rowid, long long* changes, const char** pzTail)
|
||||
{
|
||||
sqlite3_stmt *stmt = 0;
|
||||
const char *tail = 0;
|
||||
#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY
|
||||
int rv = _sqlite3_prepare_v2_blocking(db, zSql, nBytes, &stmt, &tail);
|
||||
#else
|
||||
int rv = sqlite3_prepare_v2(db, zSql, nBytes, &stmt, &tail);
|
||||
#endif
|
||||
if (rv != SQLITE_OK) {
|
||||
*pzTail = 0;
|
||||
return rv;
|
||||
}
|
||||
if (stmt == 0) {
|
||||
// Empty statement
|
||||
*rowid = 0;
|
||||
*changes = 0;
|
||||
*pzTail = tail;
|
||||
return SQLITE_OK;
|
||||
}
|
||||
#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY
|
||||
rv = _sqlite3_step_row_blocking(stmt, rowid, changes);
|
||||
#else
|
||||
rv = sqlite3_step(stmt);
|
||||
*rowid = (long long) sqlite3_last_insert_rowid(db);
|
||||
*changes = (long long) sqlite3_changes(db);
|
||||
#endif
|
||||
sqlite3_finalize(stmt);
|
||||
*pzTail = tail;
|
||||
return rv;
|
||||
}
|
||||
|
||||
// Combined reset + clear_bindings in a single C call to reduce CGO crossings.
|
||||
static int
|
||||
_sqlite3_reset_clear(sqlite3_stmt* stmt)
|
||||
{
|
||||
int rv = sqlite3_reset(stmt);
|
||||
sqlite3_clear_bindings(stmt);
|
||||
return rv;
|
||||
}
|
||||
|
||||
#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY
|
||||
extern int _sqlite3_step_blocking(sqlite3_stmt *stmt);
|
||||
extern int _sqlite3_step_row_blocking(sqlite3_stmt* stmt, long long* rowid, long long* changes);
|
||||
@@ -886,17 +940,15 @@ func lastError(db *C.sqlite3) error {
|
||||
|
||||
// Exec implements Execer.
|
||||
func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, error) {
|
||||
list := make([]driver.NamedValue, len(args))
|
||||
for i, v := range args {
|
||||
list[i] = driver.NamedValue{
|
||||
Ordinal: i + 1,
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
return c.exec(context.Background(), query, list)
|
||||
return c.exec(context.Background(), query, valueToNamedValue(args))
|
||||
}
|
||||
|
||||
func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
// Fast path: no args, no context cancellation → single CGO call per statement
|
||||
if len(args) == 0 && ctx.Done() == nil {
|
||||
return c.execNoArgs(query)
|
||||
}
|
||||
|
||||
start := 0
|
||||
for {
|
||||
s, err := c.prepare(ctx, query)
|
||||
@@ -931,16 +983,34 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.Named
|
||||
}
|
||||
}
|
||||
|
||||
// execNoArgs executes a query with no parameters in a single CGO call per statement.
|
||||
func (c *SQLiteConn) execNoArgs(query string) (driver.Result, error) {
|
||||
var res *SQLiteResult
|
||||
for len(query) > 0 {
|
||||
var rowid, changes C.longlong
|
||||
var tail *C.char
|
||||
pquery := C.CString(query)
|
||||
rv := C._sqlite3_exec_no_args(c.db, pquery, C.int(len(query)), &rowid, &changes, &tail)
|
||||
if tail != nil && *tail != '\000' {
|
||||
query = strings.TrimSpace(C.GoString(tail))
|
||||
} else {
|
||||
query = ""
|
||||
}
|
||||
C.free(unsafe.Pointer(pquery))
|
||||
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
|
||||
return nil, c.lastError()
|
||||
}
|
||||
res = &SQLiteResult{id: int64(rowid), changes: int64(changes)}
|
||||
}
|
||||
if res == nil {
|
||||
res = &SQLiteResult{0, 0}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Query implements Queryer.
|
||||
func (c *SQLiteConn) Query(query string, args []driver.Value) (driver.Rows, error) {
|
||||
list := make([]driver.NamedValue, len(args))
|
||||
for i, v := range args {
|
||||
list[i] = driver.NamedValue{
|
||||
Ordinal: i + 1,
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
return c.query(context.Background(), query, list)
|
||||
return c.query(context.Background(), query, valueToNamedValue(args))
|
||||
}
|
||||
|
||||
func (c *SQLiteConn) query(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
|
||||
@@ -1830,7 +1900,7 @@ func (c *SQLiteConn) prepare(ctx context.Context, query string) (driver.Stmt, er
|
||||
defer C.free(unsafe.Pointer(pquery))
|
||||
var s *C.sqlite3_stmt
|
||||
var tail *C.char
|
||||
rv := C._sqlite3_prepare_v2_internal(c.db, pquery, C.int(-1), &s, &tail)
|
||||
rv := C._sqlite3_prepare_v2_internal(c.db, pquery, C.int(len(query)), &s, &tail)
|
||||
if rv != C.SQLITE_OK {
|
||||
return nil, c.lastError()
|
||||
}
|
||||
@@ -2002,7 +2072,12 @@ func bindValue(s *C.sqlite3_stmt, n C.int, value driver.Value) C.int {
|
||||
}
|
||||
return C._sqlite3_bind_blob(s, n, unsafe.Pointer(&v[0]), C.int(ln))
|
||||
case time.Time:
|
||||
return bindText(s, n, v.Format(SQLiteTimestampFormats[0]))
|
||||
var buf [64]byte
|
||||
b := v.AppendFormat(buf[:0], SQLiteTimestampFormats[0])
|
||||
if len(b) == 0 {
|
||||
return C._sqlite3_bind_text(s, n, (*C.char)(unsafe.Pointer(&placeHolder[0])), C.int(0))
|
||||
}
|
||||
return C._sqlite3_bind_text(s, n, (*C.char)(unsafe.Pointer(&b[0])), C.int(len(b)))
|
||||
default:
|
||||
return C.SQLITE_MISUSE
|
||||
}
|
||||
@@ -2015,12 +2090,17 @@ func (s *SQLiteStmt) bindNamedIndices(name string) [3]int {
|
||||
return indices
|
||||
}
|
||||
|
||||
prefixes := [3]string{":", "@", "$"}
|
||||
// Build ":name\0" once and rewrite prefix byte to avoid 3 C.CString allocs.
|
||||
buf := make([]byte, 1+len(name)+1) // prefix + name + null terminator
|
||||
copy(buf[1:], name)
|
||||
buf[len(buf)-1] = 0
|
||||
cname := (*C.char)(unsafe.Pointer(&buf[0]))
|
||||
|
||||
var indices [3]int
|
||||
for i := range prefixes {
|
||||
cname := C.CString(prefixes[i] + name)
|
||||
prefixes := [3]byte{':', '@', '$'}
|
||||
for i, p := range prefixes {
|
||||
buf[0] = p
|
||||
indices[i] = int(C.sqlite3_bind_parameter_index(s.s, cname))
|
||||
C.free(unsafe.Pointer(cname))
|
||||
}
|
||||
s.namedParams[name] = indices
|
||||
return indices
|
||||
@@ -2057,13 +2137,11 @@ func stmtArgs(args []driver.NamedValue, start, na int) []driver.NamedValue {
|
||||
}
|
||||
|
||||
func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
|
||||
rv := C.sqlite3_reset(s.s)
|
||||
rv := C._sqlite3_reset_clear(s.s)
|
||||
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
|
||||
return s.c.lastError()
|
||||
}
|
||||
|
||||
C.sqlite3_clear_bindings(s.s)
|
||||
|
||||
hasNamed := false
|
||||
for i := range args {
|
||||
if args[i].Name != "" {
|
||||
@@ -2083,23 +2161,20 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
bindIndices := make([][4]int, len(args))
|
||||
for i, v := range args {
|
||||
if v.Name == "" {
|
||||
bindIndices[i][0] = v.Ordinal
|
||||
for _, arg := range args {
|
||||
if arg.Name == "" {
|
||||
rv = bindValue(s.s, C.int(arg.Ordinal), arg.Value)
|
||||
if rv != C.SQLITE_OK {
|
||||
return s.c.lastError()
|
||||
}
|
||||
continue
|
||||
}
|
||||
indices := s.bindNamedIndices(v.Name)
|
||||
copy(bindIndices[i][1:], indices[:])
|
||||
}
|
||||
|
||||
for i, arg := range args {
|
||||
for _, idx := range bindIndices[i] {
|
||||
indices := s.bindNamedIndices(arg.Name)
|
||||
for _, idx := range indices {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
n := C.int(idx)
|
||||
rv = bindValue(s.s, n, arg.Value)
|
||||
rv = bindValue(s.s, C.int(idx), arg.Value)
|
||||
if rv != C.SQLITE_OK {
|
||||
return s.c.lastError()
|
||||
}
|
||||
@@ -2110,14 +2185,7 @@ func (s *SQLiteStmt) bind(args []driver.NamedValue) error {
|
||||
|
||||
// Query the statement with arguments. Return records.
|
||||
func (s *SQLiteStmt) Query(args []driver.Value) (driver.Rows, error) {
|
||||
list := make([]driver.NamedValue, len(args))
|
||||
for i, v := range args {
|
||||
list[i] = driver.NamedValue{
|
||||
Ordinal: i + 1,
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
return s.query(context.Background(), list)
|
||||
return s.query(context.Background(), valueToNamedValue(args))
|
||||
}
|
||||
|
||||
func (s *SQLiteStmt) query(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||
@@ -2156,6 +2224,10 @@ func (r *SQLiteResult) RowsAffected() (int64, error) {
|
||||
|
||||
// Exec execute the statement with arguments. Return result object.
|
||||
func (s *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||
return s.exec(context.Background(), valueToNamedValue(args))
|
||||
}
|
||||
|
||||
func valueToNamedValue(args []driver.Value) []driver.NamedValue {
|
||||
list := make([]driver.NamedValue, len(args))
|
||||
for i, v := range args {
|
||||
list[i] = driver.NamedValue{
|
||||
@@ -2163,7 +2235,7 @@ func (s *SQLiteStmt) Exec(args []driver.Value) (driver.Result, error) {
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
return s.exec(context.Background(), list)
|
||||
return list
|
||||
}
|
||||
|
||||
func isInterruptErr(err error) bool {
|
||||
@@ -2180,38 +2252,35 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []driver.NamedValue) (driver
|
||||
return s.execSync(args)
|
||||
}
|
||||
|
||||
type result struct {
|
||||
r driver.Result
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan result)
|
||||
defer close(resultCh)
|
||||
sema := make(chan struct{})
|
||||
var r driver.Result
|
||||
var err error
|
||||
go func() {
|
||||
r, err := s.execSync(args)
|
||||
resultCh <- result{r, err}
|
||||
r, err = s.execSync(args)
|
||||
close(sema)
|
||||
}()
|
||||
var rv result
|
||||
select {
|
||||
case rv = <-resultCh:
|
||||
case <-sema:
|
||||
return r, err
|
||||
case <-ctx.Done():
|
||||
select {
|
||||
case rv = <-resultCh: // no need to interrupt, operation completed in db
|
||||
case <-sema: // no need to interrupt, operation completed in db
|
||||
return r, err
|
||||
default:
|
||||
// this is still racy and can be no-op if executed between sqlite3_* calls in execSync.
|
||||
C.sqlite3_interrupt(s.c.db)
|
||||
rv = <-resultCh // wait for goroutine completed
|
||||
if isInterruptErr(rv.err) {
|
||||
<-sema // wait for goroutine completed
|
||||
if isInterruptErr(err) {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
}
|
||||
return rv.r, rv.err
|
||||
}
|
||||
|
||||
func (s *SQLiteStmt) execSync(args []driver.NamedValue) (driver.Result, error) {
|
||||
if err := s.bind(args); err != nil {
|
||||
C.sqlite3_reset(s.s)
|
||||
C.sqlite3_clear_bindings(s.s)
|
||||
C._sqlite3_reset_clear(s.s)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2219,8 +2288,7 @@ func (s *SQLiteStmt) execSync(args []driver.NamedValue) (driver.Result, error) {
|
||||
rv := C._sqlite3_step_row_internal(s.s, &rowid, &changes)
|
||||
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
|
||||
err := s.c.lastError()
|
||||
C.sqlite3_reset(s.s)
|
||||
C.sqlite3_clear_bindings(s.s)
|
||||
C._sqlite3_reset_clear(s.s)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2311,21 +2379,22 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
|
||||
if rc.ctx.Done() == nil {
|
||||
return rc.nextSyncLocked(dest)
|
||||
}
|
||||
resultCh := make(chan error)
|
||||
defer close(resultCh)
|
||||
sema := make(chan struct{})
|
||||
var err error
|
||||
go func() {
|
||||
resultCh <- rc.nextSyncLocked(dest)
|
||||
err = rc.nextSyncLocked(dest)
|
||||
close(sema)
|
||||
}()
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
case <-sema:
|
||||
return err
|
||||
case <-rc.ctx.Done():
|
||||
select {
|
||||
case <-resultCh: // no need to interrupt
|
||||
case <-sema: // no need to interrupt
|
||||
default:
|
||||
// this is still racy and can be no-op if executed between sqlite3_* calls in nextSyncLocked.
|
||||
C.sqlite3_interrupt(rc.s.c.db)
|
||||
<-resultCh // ensure goroutine completed
|
||||
<-sema // ensure goroutine completed
|
||||
}
|
||||
return rc.ctx.Err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user