Skip to content

Performance: reduce reflection and fmt.Sprintf allocations in SQL building paths #7791

@pageton

Description

@pageton

Describe the feature

Reduce reflection-induced heap allocations and unnecessary copies in the SQL building and query execution paths.

Motivation

1. reflect.Value.Interface() boxing in AddVarstatement.go:259,399,469

stmt.AddVar(writer, rv.Index(i).Interface())  // line 259
values[i] = reflectValue.Index(i).Interface() // lines 399, 469

Called for every element in a slice/array argument to WHERE clauses. rv.Index(i).Interface() boxes each element into interface{} — a heap allocation. For WHERE id IN (1,2,...100), that's 100 allocations.

Fix: Add fast-path type switches for common kinds (int, string, float64, etc.) before falling back to reflect.Value.Interface():

switch rv.Type().Elem().Kind() {
case reflect.Int:
    for i := 0; i < rv.Len(); i++ {
        stmt.Vars = append(stmt.Vars, rv.Index(i).Int())
    }
default:
    // existing reflect path
}

2. fmt.Sprintf("%p", ...) in InstanceSet/InstanceGetgorm.go:386,393

tx.Statement.Settings.Store(fmt.Sprintf("%p", tx.Statement)+key, value)

fmt.Sprintf allocates a string on every call. Used in transaction callbacks (callbacks/transaction.go:12db.InstanceSet("gorm:started_transaction", true)), so every create/update/delete with default transactions hits this.

Fix: Use unsafe.Pointer with strconv.AppendUint:

var buf [20]byte
key = string(strconv.AppendUint(buf[:0], uint64(uintptr(unsafe.Pointer(tx.Statement))), 16)) + key

3. Statement.clone() copies sync.Map via Range/Storestatement.go:575-578

stmt.Settings.Range(func(k, v interface{}) bool {
    newStmt.Settings.Store(k, v)
    return true
})

sync.Map.Range + Store is O(n) with synchronization overhead. Runs on every getInstance() call, even when Settings are empty.

Fix: If Settings are rarely used, skip the copy when the map is empty (add a hasSettings bool flag). Or share the sync.Map pointer with copy-on-write semantics.

4. strings.Replace in loop for subquery variable replacement — statement.go:228

for _, vv := range vars {
    bindvar := strings.Builder{}
    cv.BindVarTo(&bindvar, subdb.Statement, vv)
    sql = strings.Replace(sql, bindvar.String(), "?", 1)
}

Each strings.Replace creates a new string allocation. The sql string is reassigned in a loop, with each iteration allocating a new backing array.

Fix: Build a replacement map first, then do a single-pass replacement using strings.Builder.

5. reflect.ValueOf(tx.Tx).IsNil() in Commit/Rollback — prepare_stmt.go:157-168

if tx.Tx != nil && !reflect.ValueOf(tx.Tx).IsNil() {

reflect.ValueOf allocates on every Commit() and Rollback().

Fix: Store a committed bool flag or use an interface nil check without reflect.

Estimated Impact

Fix Impact Path
Type-switch in AddVar N allocs → 0 for common types WHERE-IN queries
Replace fmt.Sprintf 1-2 allocs → 0 Every transaction
Skip empty Settings copy O(n) → O(1) when empty Every query
Single-pass subquery replace N allocs → 1 Subqueries
Remove reflect in Commit 1 alloc → 0 Every transaction commit

Related Issues

  • Performance architecture review of GORM codebase

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions