Skip to content

Commit

Permalink
enhancement: Avoid calling reflect.New() when passing in slice of val…
Browse files Browse the repository at this point in the history
…ues to `Scan()` (#5388)

* fix: reduce allocations when slice of values

* chore[test]: Add benchmark for scan

* chore[test]: add bench for scan slice

* chore[test]: add bench for slice pointer and improve tests

* chore[test]: make sure database is empty when doing slice tests

* fix[test]: correct sql delete statement

* enhancement: skip new if rows affected = 0
  • Loading branch information
Bexanderthebex committed Jun 1, 2022
1 parent f4e9904 commit d01de72
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 1 deletion.
7 changes: 6 additions & 1 deletion scan.go
Expand Up @@ -237,6 +237,7 @@ func Scan(rows Rows, db *DB, mode ScanMode) {
switch reflectValue.Kind() {
case reflect.Slice, reflect.Array:
var elem reflect.Value
recyclableStruct := reflect.New(reflectValueType)

if !update || reflectValue.Len() == 0 {
update = false
Expand All @@ -261,7 +262,11 @@ func Scan(rows Rows, db *DB, mode ScanMode) {
}
}
} else {
elem = reflect.New(reflectValueType)
if isPtr && db.RowsAffected > 0 {
elem = reflect.New(reflectValueType)
} else {
elem = recyclableStruct
}
}

db.scanIntoStruct(rows, elem, values, fields, joinFields)
Expand Down
40 changes: 40 additions & 0 deletions tests/benchmark_test.go
@@ -1,6 +1,7 @@
package tests_test

import (
"fmt"
"testing"

. "gorm.io/gorm/utils/tests"
Expand All @@ -24,6 +25,45 @@ func BenchmarkFind(b *testing.B) {
}
}

func BenchmarkScan(b *testing.B) {
user := *GetUser("scan", Config{})
DB.Create(&user)

var u User
b.ResetTimer()
for x := 0; x < b.N; x++ {
DB.Raw("select * from users where id = ?", user.ID).Scan(&u)
}
}

func BenchmarkScanSlice(b *testing.B) {
DB.Exec("delete from users")
for i := 0; i < 10_000; i++ {
user := *GetUser(fmt.Sprintf("scan-%d", i), Config{})
DB.Create(&user)
}

var u []User
b.ResetTimer()
for x := 0; x < b.N; x++ {
DB.Raw("select * from users").Scan(&u)
}
}

func BenchmarkScanSlicePointer(b *testing.B) {
DB.Exec("delete from users")
for i := 0; i < 10_000; i++ {
user := *GetUser(fmt.Sprintf("scan-%d", i), Config{})
DB.Create(&user)
}

var u []*User
b.ResetTimer()
for x := 0; x < b.N; x++ {
DB.Raw("select * from users").Scan(&u)
}
}

func BenchmarkUpdate(b *testing.B) {
user := *GetUser("find", Config{})
DB.Create(&user)
Expand Down

3 comments on commit d01de72

@znbang
Copy link

@znbang znbang commented on d01de72 Jun 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointer in a struct will be copied when append recyclableStruct to the slice.
For example

type User struct {
  gorm.Model
  Name string
  Manager *User
}

db.Joins("Manager").Find(&users)

All users in the slice will point to the same instance of Manager and the Manager is populated with data from last user's manager.

@stone-stones
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

//BusProduct model 定义
type BusProduct struct {
	*ModelBase
	Product string
}

a model defines like BusProduct will get the same model when querying
here is part of my log

find dest:{ModelBase:0xc000993950 Product:EIAM },base:recnwYXFRCvHc 
find dest:{ModelBase:0xc000993950 Product:保利威 },base:recnwYXFRCvHc
 find dest:{ModelBase:0xc000993950 Product:兔展 },base:recnwYXFRCvHc,

@tab1293
Copy link

@tab1293 tab1293 commented on d01de72 Aug 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed an issue for this change in behavior: #5575

Please sign in to comment.