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/runtime-subsetting-memo.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The runtime answers the subsetting walks a feature read repeats once per model — the relationships a feature declares of one kind, the features of a type they name, and whether a feature subsets another through any chain — and reading the subsetters an object holds no longer copies the list when the object holds every one. Verifying a constraint over gRPC allocates 3 547 fewer times per call (16 074 vs 19 621; 14 091 before the implicit-specialization runtime objects were added) and 822 706 bytes instead of 1 564 432 (849 347 before, same machine).
44 changes: 31 additions & 13 deletions internal/exec/runtime/kernel_collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (ctx *Context) kernelCollection(typ *symbols.Symbol, name string) (*symbols
return nil, false
}
for _, kind := range []ast.RelationshipKind{ast.RelInverseOf, ast.RelUnions} {
if len(relationshipsOfKind(sym, kind)) > 0 {
if len(ctx.relationshipsOfKind(sym, kind)) > 0 {
return nil, false
}
}
Expand Down Expand Up @@ -173,20 +173,38 @@ func (ctx *Context) shapeSubsettersOf(inst *Instance, sym *symbols.Symbol) []*Ef
// subsetsTransitively reports whether feature, a member of owner, subsets target through
// any chain of declared `:>` edges, implicit nesting subsettings and redefinitions.
func (ctx *Context) subsetsTransitively(feature, owner, target *symbols.Symbol) bool {
key := subsettingKey{feature, owner, target}
if subsets, ok := ctx.model.subsetsTransitively[key]; ok {
return subsets
}
subsets := ctx.walkSubsetsTransitively(feature, owner, target)
ctx.model.subsetsTransitively[key] = subsets
return subsets
}

// subsettingKey identifies one question subsetsTransitively answers.
type subsettingKey struct {
feature, owner, target *symbols.Symbol
}

func (ctx *Context) walkSubsetsTransitively(feature, owner, target *symbols.Symbol) bool {
seen := map[*symbols.Symbol]bool{feature: true}
for queue := []*symbols.Symbol{feature}; len(queue) > 0; {
cur := queue[0]
queue = queue[1:]
next := ctx.relatedFeatures(cur, owner, ast.RelSubsets)
next = append(next, ctx.redefinedFeatures(cur, owner)...)
next = append(next, ctx.model.semantics.ImplicitSubsettings(cur)...)
for _, sub := range next {
if sub == target {
return true
}
if sub != nil && !seen[sub] {
seen[sub] = true
queue = append(queue, sub)
for _, next := range [][]*symbols.Symbol{
ctx.relatedFeatures(cur, owner, ast.RelSubsets),
ctx.redefinedFeatures(cur, owner),
ctx.model.semantics.ImplicitSubsettings(cur),
} {
for _, sub := range next {
if sub == target {
return true
}
if sub != nil && !seen[sub] {
seen[sub] = true
queue = append(queue, sub)
}
}
}
}
Expand All @@ -197,12 +215,12 @@ func (ctx *Context) subsetsTransitively(feature, owner, target *symbols.Symbol)
// an intersection its subsetting alone implies (`subobjects`), so it states no further value.
func (ctx *Context) intersectsOnlySubsetted(sym *symbols.Symbol) bool {
subsetted := map[*symbols.Symbol]bool{}
for _, rel := range relationshipsOfKind(sym, ast.RelSubsets) {
for _, rel := range ctx.relationshipsOfKind(sym, ast.RelSubsets) {
if target := ctx.model.semantics.RelationshipTarget(sym, rel); target != nil {
subsetted[target] = true
}
}
for _, rel := range relationshipsOfKind(sym, ast.RelIntersects) {
for _, rel := range ctx.relationshipsOfKind(sym, ast.RelIntersects) {
if target := ctx.model.semantics.RelationshipTarget(sym, rel); target == nil || !subsetted[target] {
return false
}
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/runtime/kernel_members.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func (ctx *Context) kernelInverseValue(inst *Instance, sym *symbols.Symbol, feat
composite := false
owner, held := inst.Owner()
self, hasSelf := ctx.model.semantics.LookupMember(inst.Type, "self")
for _, rel := range relationshipsOfKind(sym, ast.RelInverseOf) {
for _, rel := range ctx.relationshipsOfKind(sym, ast.RelInverseOf) {
target := ctx.model.semantics.RelationshipTarget(sym, rel)
if target == nil {
continue
Expand Down
9 changes: 9 additions & 0 deletions internal/exec/runtime/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ type Model struct {
// declaredSubsettedNames; callers read the shared slices.
subsetted map[featureOfType][]string
declaredSubsetted map[featureOfType][]string
// relationships memoizes relationshipsOfKind per feature and kind, related relatedFeatures
// per feature, owner and kind; callers read the shared slices, which have no spare capacity.
relationships map[relationshipsKey][]*ast.Relationship
related map[relatedKey][]*symbols.Symbol
// subsetsTransitively memoizes the walk of the same name per feature, owner and target.
subsetsTransitively map[subsettingKey]bool

// toolExecutions memoizes toolExecutionOf per action; toolUnits the units tool
// answers spell, per scope they are read in.
Expand Down Expand Up @@ -267,6 +273,9 @@ func NewModel(sem *semantics.Model, resolver *resolve.Resolver) *Model {
subsetters: make(map[*symbols.Symbol]map[string][]EffectiveFeature),
subsetted: make(map[featureOfType][]string),
declaredSubsetted: make(map[featureOfType][]string),
relationships: make(map[relationshipsKey][]*ast.Relationship),
related: make(map[relatedKey][]*symbols.Symbol),
subsetsTransitively: make(map[subsettingKey]bool),
toolExecutions: make(map[*symbols.Symbol]*toolExecution),
toolUnits: make(map[toolUnitKey]semantics.Unit),
objectConns: make(map[*symbols.Symbol][]lower.Connection),
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/runtime/namespace_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func (ctx *Context) namespaceModelIndex() *namespaceModelIndex {
if _, ok := member.Decl.(*ast.Usage); !ok {
return true
}
for _, rel := range relationshipsOfKind(member, ast.RelSubsets) {
for _, rel := range ctx.relationshipsOfKind(member, ast.RelSubsets) {
if target := ctx.model.semantics.RelationshipTarget(member, rel); target != nil {
out.subsetters[target] = append(out.subsetters[target], member)
}
Expand Down
94 changes: 94 additions & 0 deletions internal/exec/runtime/relationships_memo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package runtime

import (
"testing"

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

const relationshipsMemoModel = `package test {
part def Rig {
part panels[*];
part solar[*] :> panels;
part front :> solar, panels;
}
part rig : Rig;
}`

// relationshipQueryAllocs is what n repeated queries for one feature's `:>`
// relationships allocate after the first answered.
func relationshipQueryAllocs(t *testing.T, n int) float64 {
t.Helper()
ctx, idx := libraryShapeContext(t, relationshipsMemoModel)
front := lookupOne(t, idx, "test::Rig::front")
if rels := ctx.relationshipsOfKind(front, ast.RelSubsets); len(rels) != 2 {
t.Fatalf("relationshipsOfKind(front, subsets) = %d relationships, want 2", len(rels))
}
return testing.AllocsPerRun(20, func() {
for i := 0; i < n; i++ {
ctx.relationshipsOfKind(front, ast.RelSubsets)
}
})
}

// A feature's relationships of a kind are read from its declaration once per
// model: twice the queries allocate no more than half of them.
func TestRelationshipsOfKindAreCollectedOncePerFeature(t *testing.T) {
small, large := relationshipQueryAllocs(t, 50), relationshipQueryAllocs(t, 100)
t.Logf("50 queries allocate %.0f objects, 100 queries %.0f", small, large)
if large > small {
t.Errorf("100 repeated relationship queries allocate %.0f objects, 50 allocate %.0f: the declaration is rescanned per query", large, small)
}
}

// subsettingWalkAllocs is what n repeated subsetting walks over one type allocate
// after the first answered: the features front subsets, whether it reaches panels,
// and the features of rig subsetting panels.
func subsettingWalkAllocs(t *testing.T, n int) (related, transitive, subsetting float64) {
t.Helper()
ctx, idx := libraryShapeContext(t, relationshipsMemoModel)
rig := instantiateQualified(t, ctx, idx, "test::rig")
typ, front, panels := lookupOne(t, idx, "test::Rig"), lookupOne(t, idx, "test::Rig::front"), lookupOne(t, idx, "test::Rig::panels")
if got := ctx.relatedFeatures(front, typ, ast.RelSubsets); len(got) != 2 {
t.Fatalf("relatedFeatures(front, Rig, subsets) = %v, want solar and panels", got)
}
if !ctx.subsetsTransitively(front, typ, panels) {
t.Fatal("subsetsTransitively(front, Rig, panels) = false, want true")
}
if got := ctx.SubsettingFeatures(rig, typ, "panels"); len(got) != 2 {
t.Fatalf("SubsettingFeatures(rig, Rig, panels) = %v, want solar and front", featureNames(got))
}
related = testing.AllocsPerRun(20, func() {
for i := 0; i < n; i++ {
ctx.relatedFeatures(front, typ, ast.RelSubsets)
}
})
transitive = testing.AllocsPerRun(20, func() {
for i := 0; i < n; i++ {
ctx.subsetsTransitively(front, typ, panels)
}
})
subsetting = testing.AllocsPerRun(20, func() {
for i := 0; i < n; i++ {
ctx.SubsettingFeatures(rig, typ, "panels")
}
})
return related, transitive, subsetting
}

// The subsetting walks a read repeats are answered once per model: twice the
// walks allocate no more than half of them.
func TestSubsettingWalksAreAnsweredOncePerModel(t *testing.T) {
r1, t1, s1 := subsettingWalkAllocs(t, 50)
r2, t2, s2 := subsettingWalkAllocs(t, 100)
t.Logf("50 walks allocate related %.0f, transitive %.0f, subsetting %.0f; 100 walks %.0f, %.0f, %.0f", r1, t1, s1, r2, t2, s2)
if r2 > r1 {
t.Errorf("100 relatedFeatures walks allocate %.0f objects, 50 allocate %.0f: the relationships are re-resolved per walk", r2, r1)
}
if t2 > t1 {
t.Errorf("100 subsetsTransitively walks allocate %.0f objects, 50 allocate %.0f: the chain is re-walked per question", t2, t1)
}
if s2 > s1 {
t.Errorf("100 SubsettingFeatures reads allocate %.0f objects, 50 allocate %.0f: the held subsetters are re-filtered per read", s2, s1)
}
}
2 changes: 1 addition & 1 deletion internal/exec/runtime/shape.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ func (ctx *Context) roleRedefiningDefault(sym, typeSym *symbols.Symbol) (ast.Nod
// inherits any redefined feature's (KerML 1.0 §7.3.4.5), though no object carries them.
func (ctx *Context) redefinedNamespaceUsages(sym *symbols.Symbol) []*symbols.Symbol {
var usages []*symbols.Symbol
for _, rel := range relationshipsOfKind(sym, ast.RelRedefines) {
for _, rel := range ctx.relationshipsOfKind(sym, ast.RelRedefines) {
resolved := ctx.model.semantics.RelationshipTarget(sym, rel)
if resolved == nil || resolved == sym {
continue
Expand Down
56 changes: 49 additions & 7 deletions internal/exec/runtime/subsetting.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,24 @@ import (
// A declaration that itself restates sym (`:>> elements` on an object, walked past
// its library chain) masks nothing on that walk: the target stays the resolved one.
func (ctx *Context) relatedFeatures(sym, owner *symbols.Symbol, kind ast.RelationshipKind) []*symbols.Symbol {
key := relatedKey{sym, owner, kind}
if features, ok := ctx.model.related[key]; ok {
return features
}
features := slices.Clip(ctx.collectRelatedFeatures(sym, owner, kind))
ctx.model.related[key] = features
return features
}

// relatedKey identifies the features of one owner a feature's relationships of a kind name.
type relatedKey struct {
sym, owner *symbols.Symbol
kind ast.RelationshipKind
}

func (ctx *Context) collectRelatedFeatures(sym, owner *symbols.Symbol, kind ast.RelationshipKind) []*symbols.Symbol {
var features []*symbols.Symbol
for _, rel := range relationshipsOfKind(sym, kind) {
for _, rel := range ctx.relationshipsOfKind(sym, kind) {
qn := ast.AsQualifiedName(rel.Target)
resolved := ctx.model.semantics.RelationshipTarget(sym, rel)
if resolved != nil && resolved != sym {
Expand Down Expand Up @@ -100,8 +116,26 @@ func (ctx *Context) ownDeclarationNamed(owner, sym *symbols.Symbol, names ...str
return nil, false
}

// relationshipsOfKind returns sym's relationships of the given kind that name a target.
func relationshipsOfKind(sym *symbols.Symbol, kind ast.RelationshipKind) []*ast.Relationship {
// relationshipsKey identifies the relationships of one kind a symbol declares.
type relationshipsKey struct {
sym *symbols.Symbol
kind ast.RelationshipKind
}

// relationshipsOfKind returns sym's relationships of the given kind that name a target,
// memoized per symbol and kind; callers read the shared slice and never append to it.
func (ctx *Context) relationshipsOfKind(sym *symbols.Symbol, kind ast.RelationshipKind) []*ast.Relationship {
key := relationshipsKey{sym, kind}
if rels, ok := ctx.model.relationships[key]; ok {
return rels
}
rels := declaredRelationshipsOfKind(sym, kind)
ctx.model.relationships[key] = rels
return rels
}

// declaredRelationshipsOfKind collects sym's relationships of the given kind that name a target.
func declaredRelationshipsOfKind(sym *symbols.Symbol, kind ast.RelationshipKind) []*ast.Relationship {
var rels []*ast.Relationship
for _, rel := range semantics.RelationshipsOf(sym) {
if rel == nil || rel.Kind != kind || rel.Target == nil {
Expand Down Expand Up @@ -604,13 +638,20 @@ func (ctx *Context) SubsettingFeatures(inst *Instance, typ *symbols.Symbol, name
if inst == nil {
return ofType
}
var subsetting []EffectiveFeature
for _, feat := range ofType {
for i, feat := range ofType {
if _, ok := inst.FeatureValues[feat.Name]; ok {
subsetting = append(subsetting, feat)
continue
}
// The first feature inst does not hold: the rest is filtered into a slice of its own.
subsetting := append(make([]EffectiveFeature, 0, len(ofType)-1), ofType[:i]...)
for _, feat := range ofType[i+1:] {
if _, ok := inst.FeatureValues[feat.Name]; ok {
subsetting = append(subsetting, feat)
}
}
return subsetting
}
return subsetting
return ofType
}

// subsettersOf is SubsettingFeatures for the type alone, memoized per type and name;
Expand All @@ -637,6 +678,7 @@ func (ctx *Context) subsettersOf(typ *symbols.Symbol, name string) []EffectiveFe
}
}
}
subsetting = slices.Clip(subsetting)
byName[name] = subsetting
return subsetting
}
Expand Down
Loading