From: Bjørn Erik Pedersen Date: Sun, 16 Nov 2025 12:28:43 +0000 (+0100) Subject: Optimize memory allocations for sites matrix vector stores X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=ca4025405cc4fd1fdb3941abcf00567642e5899d;p=brevno-suite%2Fhugo 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) ```` --- diff --git a/common/paths/pathparser.go b/common/paths/pathparser.go index 1246694cc..8029b6cda 100644 --- a/common/paths/pathparser.go +++ b/common/paths/pathparser.go @@ -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() { diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index d5d00d679..792cf1a20 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -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, } diff --git a/hugolib/content_map_page_assembler.go b/hugolib/content_map_page_assembler.go index 7396ec5e4..60d06704d 100644 --- a/hugolib/content_map_page_assembler.go +++ b/hugolib/content_map_page_assembler.go @@ -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 == "" { diff --git a/hugolib/content_map_page_contentnode.go b/hugolib/content_map_page_contentnode.go index 46f59a43f..220ba630e 100644 --- a/hugolib/content_map_page_contentnode.go +++ b/hugolib/content_map_page_contentnode.go @@ -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 n +} + +func (n contentNodesMap) ForEachVector(yield func(v sitesmatrix.Vector) bool) bool { + for v := range n { + if !yield(v) { + return false } - return true - }) + } + 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") } diff --git a/hugolib/roles/roles.go b/hugolib/roles/roles.go index 6b1b5d3f6..2aaa913c5 100644 --- a/hugolib/roles/roles.go +++ b/hugolib/roles/roles.go @@ -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 { diff --git a/hugolib/sitesmatrix/dimensions.go b/hugolib/sitesmatrix/dimensions.go index 6bae609c9..2dbc745c0 100644 --- a/hugolib/sitesmatrix/dimensions.go +++ b/hugolib/sitesmatrix/dimensions.go @@ -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,14 +169,15 @@ type ( // ForEachVector iterates over all vectors in the provider. // It returns false if the iteration was stopped early. ForEachVector(func(v Vector) bool) bool - } -) -type VectorIteratorFunc func(func(v Vector) bool) bool + // LenVectors returns the number of vectors in the provider. + LenVectors() int -func (f VectorIteratorFunc) ForEachVector(yield func(v Vector) bool) bool { - return f(yield) -} + // 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 + } +) // 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 diff --git a/hugolib/sitesmatrix/vectorstores.go b/hugolib/sitesmatrix/vectorstores.go index 4f51be7a3..015efb641 100644 --- a/hugolib/sitesmatrix/vectorstores.go +++ b/hugolib/sitesmatrix/vectorstores.go @@ -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 } diff --git a/hugolib/sitesmatrix/vectorstores_test.go b/hugolib/sitesmatrix/vectorstores_test.go index 1365c9055..45015afdd 100644 --- a/hugolib/sitesmatrix/vectorstores_test.go +++ b/hugolib/sitesmatrix/vectorstores_test.go @@ -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) + } +} diff --git a/hugolib/versions/versions.go b/hugolib/versions/versions.go index acb71d63a..18b7ec91a 100644 --- a/hugolib/versions/versions.go +++ b/hugolib/versions/versions.go @@ -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 { diff --git a/langs/config.go b/langs/config.go index b6bc0ffea..bde2c2f11 100644 --- a/langs/config.go +++ b/langs/config.go @@ -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) { diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index aa299d0d1..d90ae074c 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -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,