Skip to content

Instantly share code, notes, and snippets.

@jupj
Created April 29, 2023 08:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jupj/e79d7f29c291fb470c35aae3642bcade to your computer and use it in GitHub Desktop.
Save jupj/e79d7f29c291fb470c35aae3642bcade to your computer and use it in GitHub Desktop.
Benchmark for data-oriented design part 2
package main
import (
"fmt"
"math/rand"
"testing"
"unsafe"
)
const (
N = 1000000
MB = 1024 * 1024
)
type user struct {
ID int
IsActive bool
Name string
Username string
PasswordHash []byte
MustChangePassword bool
Email string
EmailConfirmed bool
}
func getUnconfirmedEmailAddresses(users []user) []string {
var unconfirmed []string
for _, u := range users {
if !u.EmailConfirmed {
unconfirmed = append(unconfirmed, u.Email)
}
}
return unconfirmed
}
func getUnconfirmedEmailAddressesRef(users []*user) []string {
var unconfirmed []string
for _, u := range users {
if !u.EmailConfirmed {
unconfirmed = append(unconfirmed, u.Email)
}
}
return unconfirmed
}
type userData struct {
ID []int
IsActive []bool
Name []string
Username []string
PasswordHash [][]byte
MustChangePassword []bool
Email []string
EmailConfirmed []bool
}
func (u userData) getUnconfirmedEmailAddresses() []string {
var unconfirmed []string
for i, confirmed := range u.EmailConfirmed {
if !confirmed {
unconfirmed = append(unconfirmed, u.Email[i])
}
}
return unconfirmed
}
type userRefData struct {
ID []*int
IsActive []*bool
Name []*string
Username []*string
PasswordHash []*[]byte
MustChangePassword []*bool
Email []*string
EmailConfirmed []*bool
}
func (u userRefData) getUnconfirmedEmailAddresses() []string {
var unconfirmed []string
for i, confirmed := range u.EmailConfirmed {
if !*confirmed {
unconfirmed = append(unconfirmed, *u.Email[i])
}
}
return unconfirmed
}
// BenchmarkValueTypes benchmarks arrays of value types.
// Benchmark lists the email addresses that haven't been confirmed
func BenchmarkValueTypes(b *testing.B) {
var userArray []user
var usersStruct userData
// Initialize user data
for i := 0; i < N; i++ {
// Generate random password hash
passwordHash := make([]byte, 60)
rand.Read(passwordHash)
u := user{
ID: i,
// 95 % of users are not active
IsActive: rand.Int()%100 < 95,
Name: fmt.Sprintf("User%.4d", i),
Username: fmt.Sprintf("account%.4d", i),
PasswordHash: passwordHash,
// 1 % of users must change password
MustChangePassword: rand.Int()%100 == 0,
Email: fmt.Sprintf("user.%.4d@example.com", i),
// 99 % of users have confirmed their email address
EmailConfirmed: rand.Int()%100 < 99,
}
userArray = append(userArray, u)
usersStruct.ID = append(usersStruct.ID, u.ID)
usersStruct.IsActive = append(usersStruct.IsActive, u.IsActive)
usersStruct.Name = append(usersStruct.Name, u.Name)
usersStruct.Username = append(usersStruct.Username, u.Username)
usersStruct.PasswordHash = append(usersStruct.PasswordHash, u.PasswordHash)
usersStruct.MustChangePassword = append(usersStruct.MustChangePassword, u.MustChangePassword)
usersStruct.Email = append(usersStruct.Email, u.Email)
usersStruct.EmailConfirmed = append(usersStruct.EmailConfirmed, u.EmailConfirmed)
}
// Calculate sizes of data types
userStructSize := int(unsafe.Sizeof(user{}))
if userStructSize%int(unsafe.Alignof(user{})) != 0 {
// Add padding
userStructSize += int(unsafe.Alignof(user{})) - (userStructSize % int(unsafe.Alignof(user{})))
}
boolSize := int(unsafe.Sizeof(false))
stringSize := int(unsafe.Sizeof(""))
b.Logf("userStructSize: %dB, boolSize: %dB, stringSize: %dB\n", userStructSize, boolSize, stringSize)
b.Run("array-of-structs", func(b *testing.B) {
for i := 0; i < b.N; i++ {
getUnconfirmedEmailAddresses(userArray)
}
b.SetBytes(int64(userStructSize * len(userArray)))
b.ReportMetric(float64(userStructSize*len(userArray))/MB, "MB/op")
})
b.Run("struct-of-arrays", func(b *testing.B) {
// Run function once to get number of emails to account for
unconfirmed := usersStruct.getUnconfirmedEmailAddresses()
b.ResetTimer()
for i := 0; i < b.N; i++ {
usersStruct.getUnconfirmedEmailAddresses()
}
b.SetBytes(int64(boolSize*len(usersStruct.EmailConfirmed) + stringSize*len(unconfirmed)))
b.ReportMetric(float64(boolSize*len(usersStruct.EmailConfirmed)+stringSize*len(unconfirmed))/MB, "MB/op")
})
}
// BenchmarkRefTypes benchmarks arrays of reference types.
// Benchmark lists the email addresses that haven't been confirmed
func BenchmarkRefTypes(b *testing.B) {
var userArray []*user
var usersStruct userRefData
// Initialize user data
for i := 0; i < N; i++ {
// Generate random password hash
passwordHash := make([]byte, 60)
rand.Read(passwordHash)
u := user{
ID: i,
// 95 % of users are not active
IsActive: rand.Int()%100 < 95,
Name: fmt.Sprintf("User%.4d", i),
Username: fmt.Sprintf("account%.4d", i),
PasswordHash: passwordHash,
// 1 % of users must change password
MustChangePassword: rand.Int()%100 == 0,
Email: fmt.Sprintf("user.%.4d@example.com", i),
// 99 % of users have confirmed their email address
EmailConfirmed: rand.Int()%100 < 99,
}
userArray = append(userArray, &u)
usersStruct.ID = append(usersStruct.ID, &u.ID)
usersStruct.IsActive = append(usersStruct.IsActive, &u.IsActive)
usersStruct.Name = append(usersStruct.Name, &u.Name)
usersStruct.Username = append(usersStruct.Username, &u.Username)
usersStruct.PasswordHash = append(usersStruct.PasswordHash, &u.PasswordHash)
usersStruct.MustChangePassword = append(usersStruct.MustChangePassword, &u.MustChangePassword)
usersStruct.Email = append(usersStruct.Email, &u.Email)
usersStruct.EmailConfirmed = append(usersStruct.EmailConfirmed, &u.EmailConfirmed)
}
// Calculate sizes of data types
pointerSize := int(unsafe.Sizeof(uintptr(0)))
userStructSize := int(unsafe.Sizeof(user{}))
if userStructSize%int(unsafe.Alignof(user{})) != 0 {
// Add padding
userStructSize += int(unsafe.Alignof(user{})) - (userStructSize % int(unsafe.Alignof(user{})))
}
boolSize := int(unsafe.Sizeof(false))
stringSize := int(unsafe.Sizeof(""))
b.Logf("userStructSize: %dB, boolSize: %dB, stringSize: %dB\n", userStructSize, boolSize, stringSize)
b.Run("array-of-references", func(b *testing.B) {
for i := 0; i < b.N; i++ {
getUnconfirmedEmailAddressesRef(userArray)
}
b.SetBytes(int64((pointerSize + userStructSize) * len(userArray)))
b.ReportMetric(float64((pointerSize+userStructSize)*len(userArray))/MB, "MB/op")
})
b.Run("struct-of-ref-arrays", func(b *testing.B) {
// Run function once to get number of emails to account for
unconfirmed := usersStruct.getUnconfirmedEmailAddresses()
b.ResetTimer()
for i := 0; i < b.N; i++ {
usersStruct.getUnconfirmedEmailAddresses()
}
b.SetBytes(int64((pointerSize+boolSize)*len(usersStruct.EmailConfirmed) + (pointerSize+stringSize)*len(unconfirmed)))
b.ReportMetric(float64((pointerSize+boolSize)*len(usersStruct.EmailConfirmed)+(pointerSize+stringSize)*len(unconfirmed))/MB, "MB/op")
})
}
$ go test -bench=. data-oriented-design-benchmarks-2_test.go
Iterations Time Memory throughput Data processed
BenchmarkValueTypes/array-of-structs-4 121 9906752 ns/op 10497.89 MB/s 99.18 MB/op
BenchmarkValueTypes/struct-of-arrays-4 588 1974807 ns/op 586.80 MB/s 1.105 MB/op
BenchmarkRefTypes/array-of-references-4 127 9240654 ns/op 12120.35 MB/s 106.8 MB/op
BenchmarkRefTypes/struct-of-ref-arrays-4 123 9478477 ns/op 974.58 MB/s 8.810 MB/op
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment