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 AddVar — statement.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/InstanceGet — gorm.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:12 — db.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/Store — statement.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
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 inAddVar—statement.go:259,399,469Called for every element in a slice/array argument to WHERE clauses.
rv.Index(i).Interface()boxes each element intointerface{}— a heap allocation. ForWHERE 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():2.
fmt.Sprintf("%p", ...)inInstanceSet/InstanceGet—gorm.go:386,393fmt.Sprintfallocates a string on every call. Used in transaction callbacks (callbacks/transaction.go:12—db.InstanceSet("gorm:started_transaction", true)), so every create/update/delete with default transactions hits this.Fix: Use
unsafe.Pointerwithstrconv.AppendUint:3.
Statement.clone()copiessync.MapviaRange/Store—statement.go:575-578sync.Map.Range+Storeis O(n) with synchronization overhead. Runs on everygetInstance()call, even when Settings are empty.Fix: If Settings are rarely used, skip the copy when the map is empty (add a
hasSettings boolflag). Or share thesync.Mappointer with copy-on-write semantics.4.
strings.Replacein loop for subquery variable replacement —statement.go:228Each
strings.Replacecreates a new string allocation. Thesqlstring 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-168reflect.ValueOfallocates on everyCommit()andRollback().Fix: Store a
committed boolflag or use an interface nil check without reflect.Estimated Impact
Related Issues