Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/unreleased/chain-declarations-memo.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The runtime resolves the declarations a feature chain's segments name once per model and base feature instead of on every evaluation, and noting them for a chain no longer allocates a restore closure. A calc whose loop assigns four attributes from a calc usage's outputs allocates 800 fewer times per hundred steps (8 556 vs 9 356; 8 541 before feature values were held by feature rather than by name), 971 987 bytes instead of 1 000 812 (922 360 before).
59 changes: 59 additions & 0 deletions internal/exec/runtime/chain_declarations_memo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package runtime

import (
"testing"

"github.com/Open-MBEE/OpenSysML/internal/syntax/ast"
)

const chainDeclarationsModel = `package test {
part def Panel { attribute area : ScalarValues::Real; }
part def Rig { part panel : Panel; }
part rig : Rig;
}`

// chainNoteAllocs is what noting the declarations of one nested chain n times
// allocates beyond flattening it, as a loop re-evaluating the chain does both.
func chainNoteAllocs(t *testing.T, n int) (float64, int) {
t.Helper()
ctx, idx := libraryShapeContext(t, chainDeclarationsModel)
ec := newEvalContext(ctx, lookupOne(t, idx, "test").Scope)
chain, ok := parseExpr(t, "rig.panel.area").(*ast.FeatureChainExpr)
if !ok {
t.Fatal("rig.panel.area did not parse as a feature chain")
}
if _, nested := chain.Operand.(*ast.FeatureChainExpr); !nested {
t.Fatal("rig.panel.area did not parse as a nested chain")
}
base, parts := ast.FeatureChainBase(chain)
if decls := ec.chainDeclarations(chain, base, parts); len(decls) != 2 || decls[0] != lookupOne(t, idx, "test::Rig::panel") || decls[1] != lookupOne(t, idx, "test::Panel::area") {
t.Fatalf("chainDeclarations(rig, panel.area) = %v, want Rig::panel and Panel::area", decls)
}
flatten := testing.AllocsPerRun(20, func() {
for i := 0; i < n; i++ {
ast.FeatureChainBase(chain)
}
})
note := testing.AllocsPerRun(20, func() {
for i := 0; i < n; i++ {
base, parts := ast.FeatureChainBase(chain)
ec.restoreChain(ec.noteChainMembers(chain, base, parts))
}
})
return note - flatten, len(ctx.model.chainDeclarations)
}

// The declarations a chain's segments name are resolved once per model and chain
// expression, however often the nested chain is flattened afresh: noting them for
// twice the evaluations allocates no more, and the memo holds one entry.
func TestChainDeclarationsAreResolvedOncePerModel(t *testing.T) {
small, _ := chainNoteAllocs(t, 50)
large, entries := chainNoteAllocs(t, 100)
t.Logf("50 notes allocate %.0f objects beyond flattening, 100 notes %.0f; %d memo entries", small, large, entries)
if large > small {
t.Errorf("100 chain evaluations allocate %.0f objects beyond flattening, 50 allocate %.0f: the segments are re-resolved per evaluation", large, small)
}
if entries != 1 {
t.Errorf("memo holds %d entries for one chain expression, want 1", entries)
}
}
35 changes: 29 additions & 6 deletions internal/exec/runtime/chain_member_slot.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,43 @@ type chainStatic struct {
}

// noteChainMembers records the declarations a chain's segments name for the
// reads of its parts, and returns the restore of what was recorded before.
func (ec *EvalContext) noteChainMembers(base ast.Node, parts []ast.NameSegment) func() {
// reads of its parts, and returns what was recorded before for restoreChain.
func (ec *EvalContext) noteChainMembers(chain *ast.FeatureChainExpr, base ast.Node, parts []ast.NameSegment) chainStatic {
previous := ec.chain
ec.chain = chainStatic{parts: parts, decls: ec.chainDeclarations(base, parts)}
return func() { ec.chain = previous }
ec.chain = chainStatic{parts: parts, decls: ec.chainDeclarations(chain, base, parts)}
return previous
}

// restoreChain records previous again once the chain it was noted for is evaluated.
func (ec *EvalContext) restoreChain(previous chainStatic) {
ec.chain = previous
}

// chainDeclarationsKey identifies the declarations one chain expression's
// segments name from one base feature or type; the chain node is immutable AST.
type chainDeclarationsKey struct {
base *symbols.Symbol
chain *ast.FeatureChainExpr
}

// chainDeclarations resolves each segment in the namespace of the one before
// it, from the feature or cast type the base names; nil when the base names none.
func (ec *EvalContext) chainDeclarations(base ast.Node, parts []ast.NameSegment) []*symbols.Symbol {
// The resolution is memoized per model, so a chain a loop re-evaluates reads it.
func (ec *EvalContext) chainDeclarations(chain *ast.FeatureChainExpr, base ast.Node, parts []ast.NameSegment) []*symbols.Symbol {
cur, ok := ec.chainBaseSymbol(base)
if !ok {
if !ok || len(parts) == 0 {
return nil
}
key := chainDeclarationsKey{base: cur, chain: chain}
if decls, ok := ec.ctx.model.chainDeclarations[key]; ok {
return decls
}
decls := ec.resolveChainDeclarations(cur, parts)
ec.ctx.model.chainDeclarations[key] = decls
return decls
}

func (ec *EvalContext) resolveChainDeclarations(cur *symbols.Symbol, parts []ast.NameSegment) []*symbols.Symbol {
decls := make([]*symbols.Symbol, len(parts))
for i := range parts {
decl, found := ec.ctx.declaredMember(cur, parts[i].Text)
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/runtime/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -1574,7 +1574,7 @@ func (ec *EvalContext) evalFeatureChain(n *ast.FeatureChainExpr) (Value, error)
return Value{}, fmt.Errorf("empty member chain")
}
base, parts := ast.FeatureChainBase(n)
defer ec.noteChainMembers(base, parts)()
defer ec.restoreChain(ec.noteChainMembers(n, base, parts))

// A chain's members resolve in the namespace of the feature before them, where
// a redefined feature is not inherited under its name (KerML §7.3.4.5).
Expand Down
3 changes: 3 additions & 0 deletions internal/exec/runtime/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ type Model struct {
// redefined memoizes, per feature of a type, the features it redefines
// transitively; callers read the shared slice and never append to it.
redefined map[featureOfType][]*symbols.Symbol
// chainDeclarations memoizes the declarations a chain expression's segments name per base.
chainDeclarations map[chainDeclarationsKey][]*symbols.Symbol

// writeTargets memoizes the declaration an assignment's target names, per
// scope the statement was written in: what a value written must conform to.
Expand Down Expand Up @@ -248,6 +250,7 @@ func NewModel(sem *semantics.Model, resolver *resolve.Resolver) *Model {
holders: make(map[*symbols.Symbol]map[string][]string),
returnedParams: make(map[*calcShape]*returnedAnalysis),
redefined: make(map[featureOfType][]*symbols.Symbol),
chainDeclarations: make(map[chainDeclarationsKey][]*symbols.Symbol),
writeTargets: make(map[writeTargetKey]*writeTarget),
calcShapes: make(map[*symbols.Symbol]*calcShape),
predicateShapes: make(map[*symbols.Symbol]*calcShape),
Expand Down
Loading