Skip to content

[Enhancement]: Move the order of struct fields for better alignment #105

@jonbarrow

Description

@jonbarrow

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    awaiting-approvalTopic has not been approved or deniedenhancementAn update to an existing part of the codebase

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions