-
Notifications
You must be signed in to change notification settings - Fork 14
Description
Checked Existing
- I have checked the repository for duplicate issues.
What enhancement would you like to see?
Move the order of certain struct fields to create better memory alignment. I had thought Go handled this under the hood, but apparently not
Any other details to share? (OPTIONAL)
In Go every type has it's own alignment requirements (uint8 is 1 byte, uint64 is 8, etc.). When a struct is allocated Go takes the largest alignment requirement and aligns every field with that size, adding padding bytes. As a small example:
package main
import (
"fmt"
"unsafe"
)
type A struct {
a uint8
b uint64
c uint8
d uint8
e uint8
f uint8
g uint8
h uint8
i uint8
}
type B struct {
b uint64
a uint8
c uint8
d uint8
e uint8
f uint8
g uint8
h uint8
i uint8
}
func main() {
fmt.Println(unsafe.Sizeof(A{})) // 24 bytes
fmt.Println(unsafe.Sizeof(B{})) // 16 bytes
}This might look contrived, but there are some actual real world gains here for us. Both Gathering and MatchmakeParam are already at their minimum sizes, but we can actually optimize MakemakeSession. Currently we organize struct fields by the order they are encoded:
type MatchmakeSession struct {
types.Structure
Gathering
GameMode types.UInt32
Attributes types.List[types.UInt32]
OpenParticipation types.Bool
MatchmakeSystemType constants.MatchmakeSystemType
ApplicationBuffer types.Buffer
ParticipationCount types.UInt32
ProgressScore types.UInt8 // * NEX v3.4.0
SessionKey types.Buffer // * NEX v3.0.0
Option0 constants.MatchmakeSessionOption0 // * NEX v3.5.0
MatchmakeParam MatchmakeParam // * NEX v3.6.0
StartedTime types.DateTime // * NEX v3.6.0
UserPassword types.String // * NEX v3.7.0
ReferGID types.UInt32 // * NEX v3.8.0
UserPasswordEnabled types.Bool // * NEX v3.8.0
SystemPasswordEnabled types.Bool // * NEX v3.8.0
CodeWord types.String // * NEX v4.0.0
}This works great for readability, and for things like code gen, but this is actually unoptimized. If instead we order it like so:
type MatchmakeSessionOptimized struct {
StartedTime types.DateTime
Attributes types.List[types.UInt32]
ApplicationBuffer types.Buffer
SessionKey types.Buffer
UserPassword types.String
CodeWord types.String
Gathering
MatchmakeParam MatchmakeParam
GameMode types.UInt32
MatchmakeSystemType types.UInt32
ParticipationCount types.UInt32
Option types.UInt32
ReferGID types.UInt32
OpenParticipation types.Bool
ProgressScore types.UInt8
UserPasswordEnabled types.Bool
SystemPasswordEnabled types.Bool
types.Structure
}We get a noticable decrease in the structs size:
func main() {
originalSize := unsafe.Sizeof(MatchmakeSession{})
optimizedSize := unsafe.Sizeof(MatchmakeSessionOptimized{})
reduction := float64(originalSize-optimizedSize) / float64(originalSize) * 100
fmt.Printf("Original MatchmakeSession: %d bytes\n", originalSize)
fmt.Printf("Optimized MatchmakeSession: %d bytes\n", optimizedSize)
fmt.Printf("Size difference: %d bytes\n", int(originalSize)-int(optimizedSize))
fmt.Printf("Memory reduction: %.1f%%\n", reduction)
}Original MatchmakeSession: 240 bytes
Optimized MatchmakeSession: 224 bytes
Size difference: 16 bytes
Memory reduction: 6.7%
This might not seem like a lot, only saving 16 bytes, but this is 16 bytes per allocation, and this struct is one of the most common ones we'd allocate. And this is just from me eyeballing the changes, I could have messed up and gotten even lower sizes if I tried. If we apply these kinds of optimizations to all the types across all protocols, this would very likely result in noticable gains. Though this does come at the cost of some readability since fields will no longer be in their encoded order
Additionally, tools like golangci-lint support finding these issues automatically (in this case using the fieldalignment formatter). We already use golangci-lint so it would just be a matter of enabling this in our settings