Optimize memory allocations for sites matrix vector stores

By

* Caching common sites matrix setups (e.g. the single site in single site setups).
* Adding a fast path to IntSets.HasAnyVector for the common case of single vector input.

```
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10   31.62m ± 46%   30.68m ± 42%  ~ (p=0.310 n=6)

                                                                                   │ master.bench │          perfcommon.bench          │
                                                                                   │     B/op     │     B/op      vs base              │
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10   31.98Mi ± 0%   31.24Mi ± 0%  -2.30% (p=0.002 n=6)

                                                                                   │ master.bench │         perfcommon.bench          │
                                                                                   │  allocs/op   │  allocs/op   vs base              │
AssembleDeepSiteWithManySections/depth=3/sectionsPerLevel=2/pagesPerSection=100-10    460.9k ± 0%   419.9k ± 0%  -8.90% (p=0.002 n=6)
````
This commit is contained in:
Bjørn Erik Pedersen
2025-11-16 13:28:43 +01:00
parent 44b5f13f86
commit ca4025405c
11 changed files with 182 additions and 81 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ type PathParser struct {
// Below gets created on demand.
initOnce sync.Once
sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language code to sites matrix vector store.
sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
}
func (pp *PathParser) init() {
+4
View File
@@ -878,6 +878,10 @@ func (c *Configs) Init(logger loggers.Logger) error {
ConfiguredRoles: c.Base.Roles.Config,
}
if err := c.ConfiguredDimensions.Init(); err != nil {
return err
}
intSetsCfg := sitesmatrix.IntSetsConfig{
ApplyDefaults: sitesmatrix.IntSetsConfigApplyDefaultsIfNotSet,
}
+1 -1
View File
@@ -575,7 +575,7 @@ func (a *allPagesAssembler) doCreatePages(prefix string) error {
func() error {
if i := len(missingVectorsForHomeOrRootSection); i > 0 {
// Pick one, the rest will be created later.
vec := missingVectorsForHomeOrRootSection.Sample()
vec := missingVectorsForHomeOrRootSection.VectorSample()
kind := kinds.KindSection
if s == "" {
+21 -8
View File
@@ -426,12 +426,25 @@ func (n contentNodesMap) sample() contentNode {
}
func (n contentNodesMap) siteVectors() sitesmatrix.VectorIterator {
return sitesmatrix.VectorIteratorFunc(func(yield func(v sitesmatrix.Vector) bool) bool {
for k := range n {
if !yield(k) {
return false
}
}
return true
})
return n
}
func (n contentNodesMap) ForEachVector(yield func(v sitesmatrix.Vector) bool) bool {
for v := range n {
if !yield(v) {
return false
}
}
return true
}
func (n contentNodesMap) LenVectors() int {
return len(n)
}
func (n contentNodesMap) VectorSample() sitesmatrix.Vector {
for v := range n {
return v
}
panic("no vectors")
}
+4
View File
@@ -76,6 +76,10 @@ type RolesInternal struct {
Sorted []RoleInternal
}
func (r RolesInternal) Len() int {
return len(r.Sorted)
}
func (r RolesInternal) IndexDefault() int {
for i, role := range r.Sorted {
if role.Default {
+34 -16
View File
@@ -76,9 +76,13 @@ func (v1 Vector) HasVector(v2 Vector) bool {
}
func (v1 Vector) HasAnyVector(vp VectorProvider) bool {
if vp.LenVectors() == 0 {
n := vp.LenVectors()
if n == 0 {
return false
}
if n == 1 {
return v1 == vp.VectorSample()
}
return !vp.ForEachVector(func(v2 Vector) bool {
if v1 == v2 {
@@ -144,12 +148,16 @@ func (vs Vectors) ForEachVector(yield func(v Vector) bool) bool {
return true
}
func (vs Vectors) LenVectors() int {
return len(vs)
}
func (vs Vectors) ToVectorStore() VectorStore {
return newVectorStoreMapFromVectors(vs)
}
// Sample returns one of the vectors in the set.
func (vs Vectors) Sample() Vector {
// VectorSample returns one of the vectors in the set.
func (vs Vectors) VectorSample() Vector {
for v := range vs {
return v
}
@@ -161,15 +169,16 @@ type (
// ForEachVector iterates over all vectors in the provider.
// It returns false if the iteration was stopped early.
ForEachVector(func(v Vector) bool) bool
// LenVectors returns the number of vectors in the provider.
LenVectors() int
// VectorSample returns one of the vectors in the provider, usually the first or the only one.
// This will panic if the provider is empty.
VectorSample() Vector
}
)
type VectorIteratorFunc func(func(v Vector) bool) bool
func (f VectorIteratorFunc) ForEachVector(yield func(v Vector) bool) bool {
return f(yield)
}
// Bools holds boolean values for each dimension in the Hugo build matrix.
type Bools [3]bool
@@ -198,13 +207,6 @@ type VectorProvider interface {
// HasAnyVector returns true if any of the vectors in the provider matches any of the vectors in v.
HasAnyVector(v VectorProvider) bool
// LenVectors returns the number of vectors in the provider.
LenVectors() int
// VectorSample returns one of the vectors in the provider, usually the first or the only one.
// This will panic if the provider is empty.
VectorSample() Vector
// Equals returns true if this provider is equal to the other provider.
EqualsVector(other VectorProvider) bool
}
@@ -227,6 +229,22 @@ type ToVectorStoreProvider interface {
ToVectorStore() VectorStore
}
func VectorIteratorToStore(vi VectorIterator) VectorStore {
switch v := vi.(type) {
case VectorStore:
return v
case ToVectorStoreProvider:
return v.ToVectorStore()
}
vectors := make(Vectors)
vi.ForEachVector(func(v Vector) bool {
vectors[v] = struct{}{}
return true
})
return vectors.ToVectorStore()
}
type weightedVectorStore struct {
VectorStore
weight int
+61 -8
View File
@@ -71,10 +71,6 @@ func (s *vectorStoreMap) setVector(vec Vector) {
s.sets[vec] = struct{}{}
}
func (s *vectorStoreMap) Ordinal() int {
return 0
}
func (s *vectorStoreMap) KeysSorted() ([]int, []int, []int) {
var k0, k1, k2 []int
for v := range s.sets {
@@ -271,12 +267,45 @@ type ConfiguredDimension interface {
ResolveIndex(string) int
ResolveName(int) string
ForEachIndex() iter.Seq[int]
Len() int
}
// ConfiguredDimensions holds the configured dimensions for the site matrix.
type ConfiguredDimensions struct {
ConfiguredLanguages ConfiguredDimension
ConfiguredVersions ConfiguredDimension
ConfiguredRoles ConfiguredDimension
CommonSitesMatrix CommonSitestMatrix
singleVectorStoreCache *maps.Cache[Vector, *IntSets]
}
func (c *ConfiguredDimensions) IsSingleVector() bool {
return c.ConfiguredLanguages.Len() == 1 && c.ConfiguredRoles.Len() == 1 && c.ConfiguredVersions.Len() == 1
}
// GetOrCreateSingleVectorStore returns a VectorStore for the given vector.
func (c *ConfiguredDimensions) GetOrCreateSingleVectorStore(vec Vector) *IntSets {
store, _ := c.singleVectorStoreCache.GetOrCreate(vec, func() (*IntSets, error) {
is := &IntSets{}
is.setValuesInNilSets(vec, true, true, true)
return is, nil
})
return store
}
func (c *ConfiguredDimensions) Init() error {
c.singleVectorStoreCache = maps.NewCache[Vector, *IntSets]()
b := NewIntSetsBuilder(c).WithDefaultsIfNotSet().Build()
defaultVec := b.VectorSample()
c.singleVectorStoreCache.Set(defaultVec, b)
c.CommonSitesMatrix.DefaultSite = b
return nil
}
type CommonSitestMatrix struct {
DefaultSite VectorStore
}
func (c *ConfiguredDimensions) ResolveNames(v Vector) types.Strings3 {
@@ -317,6 +346,8 @@ type IntSets struct {
h *hashOnce
}
var NilStore *IntSets = nil
type hashOnce struct {
once sync.Once
hash uint64
@@ -363,7 +394,7 @@ func (s *IntSets) Intersects(other *IntSets) bool {
// Complement returns a new VectorStore that contains all vectors in s that are not in any of ss.
func (s *IntSets) Complement(ss ...VectorProvider) VectorStore {
if len(ss) == 0 || (len(ss) == 1 && ss[0] == s) {
return nil
return NilStore
}
for _, v := range ss {
@@ -372,8 +403,7 @@ func (s *IntSets) Complement(ss ...VectorProvider) VectorStore {
continue
}
if vv.IsSuperSet(s) {
var s *IntSets
return s
return NilStore
}
}
@@ -484,6 +514,9 @@ func (s *IntSets) HasLanguage(lang int) bool {
return s.languages.Has(lang)
}
// LenVectors returns the total number of vectors represented by the IntSets.
// This is the Cartesian product of the lengths of the individual sets.
// This will be 0 if s is nil or any of the sets is empty.
func (s *IntSets) LenVectors() int {
if s == nil {
return 0
@@ -526,6 +559,10 @@ func (s *IntSets) HasAnyVector(v VectorProvider) bool {
if s.LenVectors() == 0 || v.LenVectors() == 0 {
return false
}
if v.LenVectors() == 1 {
// Fast path.
return s.HasVector(v.VectorSample())
}
if vs, ok := v.(*IntSets); ok {
// Fast path.
@@ -688,6 +725,14 @@ type IntSetsBuilder struct {
func (b *IntSetsBuilder) Build() *IntSets {
b.s.init()
if b.s.LenVectors() == 1 {
// Cache it or use the existing cached version, which will allow b.s to be GCed.
bb, _ := b.cfg.singleVectorStoreCache.GetOrCreate(b.s.VectorSample(), func() (*IntSets, error) {
return b.s, nil
})
return bb
}
return b.s
}
@@ -889,6 +934,10 @@ type testDimension struct {
names []string
}
func (m testDimension) Len() int {
return len(m.names)
}
func (m testDimension) IndexDefault() int {
return 0
}
@@ -933,9 +982,13 @@ func (m *testDimension) IndexMatch(match predicate.P[string]) (iter.Seq[int], er
// NewTestingDimensions creates a new ConfiguredDimensions for testing.
func NewTestingDimensions(languages, versions, roles []string) *ConfiguredDimensions {
return &ConfiguredDimensions{
c := &ConfiguredDimensions{
ConfiguredLanguages: &testDimension{names: languages},
ConfiguredVersions: &testDimension{names: versions},
ConfiguredRoles: &testDimension{names: roles},
}
if err := c.Init(); err != nil {
panic(err)
}
return c
}
+28 -28
View File
@@ -14,20 +14,21 @@
package sitesmatrix_test
import (
"fmt"
"testing"
"github.com/bits-and-blooms/bitset"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
)
var testDims = sitesmatrix.NewTestingDimensions([]string{"en", "no"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
func newTestDims() *sitesmatrix.ConfiguredDimensions {
return sitesmatrix.NewTestingDimensions([]string{"en", "no"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
}
func TestIntSets(t *testing.T) {
c := qt.New(t)
testDims := newTestDims()
sets := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
maps.NewOrderedIntSet(1, 2),
@@ -103,31 +104,6 @@ func TestIntSets(t *testing.T) {
c.Assert(allCount, qt.Equals, 18)
}
func TestBitSetExperiments(t *testing.T) {
c := qt.New(t)
a, b, d := bitset.New(10), bitset.New(10), bitset.New(10)
ints := func(v *bitset.BitSet) []uint {
var ints []uint
for i := range v.EachSet() {
ints = append(ints, i)
}
return ints
}
a.Set(3).Set(4)
b.Set(4).Set(5).Set(6).Set(7)
d.Set(3).Set(4)
c.Assert(b.Test(5), qt.Equals, true)
fmt.Println("a:", ints(a), "==>", ints(b), "=> Intersection:", ints(a.Intersection(b)), "=> Symdiff:", ints(a.SymmetricDifference(b)))
fmt.Println("===>", ints(a.Difference(b)))
fmt.Println("===>", ints(b.Difference(a)))
fmt.Println("===> d", d.Difference(a).Count())
}
func TestIntSetsComplement(t *testing.T) {
c := qt.New(t)
@@ -147,6 +123,8 @@ func TestIntSetsComplement(t *testing.T) {
runOne := func(c *qt.C, test test) {
c.Helper()
testDims := newTestDims()
self := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
maps.NewOrderedIntSet(test.left.v0...),
maps.NewOrderedIntSet(test.left.v1...),
@@ -281,6 +259,8 @@ func TestIntSetsComplement(t *testing.T) {
func TestIntSetsComplementOfComplement(t *testing.T) {
c := qt.New(t)
testDims := newTestDims()
sets1 := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
maps.NewOrderedIntSet(1),
maps.NewOrderedIntSet(1),
@@ -313,6 +293,8 @@ func TestIntSetsComplementOfComplement(t *testing.T) {
}
func BenchmarkIntSetsComplement(b *testing.B) {
testDims := newTestDims()
sets1 := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
maps.NewOrderedIntSet(1, 2, 3),
maps.NewOrderedIntSet(1, 2, 3),
@@ -376,6 +358,8 @@ func BenchmarkIntSetsComplement(b *testing.B) {
}
func BenchmarkSets(b *testing.B) {
testDims := newTestDims()
sets1 := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
maps.NewOrderedIntSet(1, 2),
maps.NewOrderedIntSet(1, 2, 3),
@@ -485,6 +469,7 @@ func BenchmarkSets(b *testing.B) {
func TestVectorStoreMap(t *testing.T) {
c := qt.New(t)
testDims := newTestDims()
c.Run("Complement", func(c *qt.C) {
v1 := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
@@ -509,3 +494,18 @@ func TestVectorStoreMap(t *testing.T) {
})
})
}
func BenchmarkHasAnyVectorSingle(b *testing.B) {
testDims := newTestDims()
set := sitesmatrix.NewIntSetsBuilder(testDims).WithSets(
maps.NewOrderedIntSet(0),
maps.NewOrderedIntSet(0),
maps.NewOrderedIntSet(0),
).Build()
v := sitesmatrix.Vector{0, 0, 0}
for b.Loop() {
_ = set.HasAnyVector(v)
}
}
+4
View File
@@ -78,6 +78,10 @@ type VersionsInternal struct {
Sorted []VersionInternal
}
func (r VersionsInternal) Len() int {
return len(r.Sorted)
}
func (r VersionsInternal) IndexDefault() int {
for i, version := range r.Sorted {
if version.Default {
+4
View File
@@ -92,6 +92,10 @@ func (ls LanguagesInternal) ResolveIndex(name string) int {
panic(fmt.Sprintf("no language found for name %q", name))
}
func (ls LanguagesInternal) Len() int {
return len(ls.Sorted)
}
// IndexMatch returns an iterator for the roles that match the filter.
func (ls LanguagesInternal) IndexMatch(match predicate.P[string]) (iter.Seq[int], error) {
return func(yield func(i int) bool) {
+20 -19
View File
@@ -245,7 +245,13 @@ func buildSitesComplementsFromSitesConfig(
conf config.AllProvider,
fim *hugofs.FileMeta,
sitesConfig sitesmatrix.Sites,
) *sitesmatrix.IntSets {
) sitesmatrix.VectorStore {
if sitesConfig.Complements.IsZero() {
if fim != nil && fim.SitesComplements != nil {
return fim.SitesComplements
}
return sitesmatrix.NilStore
}
intsetsCfg := sitesmatrix.IntSetsConfig{
Globs: sitesConfig.Complements,
}
@@ -259,27 +265,22 @@ func buildSitesComplementsFromSitesConfig(
return sitesComplements.Build()
}
func buildSitesMatrixFromVectorIterator(
sitesMatrix sitesmatrix.VectorIterator,
) sitesmatrix.VectorStore {
switch v := sitesMatrix.(type) {
case sitesmatrix.VectorStore:
return v
case sitesmatrix.ToVectorStoreProvider:
return v.ToVectorStore()
default:
if v == nil {
return nil
}
panic(fmt.Sprintf("unsupported type %T", v))
}
}
func buildSitesMatrixFromSitesConfig(
conf config.AllProvider,
sitesMatrixBase sitesmatrix.VectorIterator,
sitesConfig sitesmatrix.Sites,
) *sitesmatrix.IntSets {
) sitesmatrix.VectorStore {
if sitesConfig.Matrix.IsZero() && sitesMatrixBase != nil {
if sitesMatrixBase.LenVectors() == 1 {
return conf.ConfiguredDimensions().GetOrCreateSingleVectorStore(sitesMatrixBase.VectorSample())
}
return sitesmatrix.VectorIteratorToStore(sitesMatrixBase)
}
if conf.ConfiguredDimensions().IsSingleVector() {
return conf.ConfiguredDimensions().CommonSitesMatrix.DefaultSite
}
intsetsCfg := sitesmatrix.IntSetsConfig{
Globs: sitesConfig.Matrix,
}
@@ -317,7 +318,7 @@ func (p *PageConfigEarly) CompileEarly(pi *paths.Path, cascades *page.PageMatche
}
if sitesMatrixBaseOnly {
p.SitesMatrix = buildSitesMatrixFromVectorIterator(sitesMatrixBase)
p.SitesMatrix = sitesmatrix.VectorIteratorToStore(sitesMatrixBase)
} else {
p.SitesMatrix = buildSitesMatrixFromSitesConfig(
conf,