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/binding-walk-allocs.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The runtime's binding walks allocate only where a binding can be found: a feature read that climbs to its owners spells out the feature path only while an owner declares a binding, the witness draw builds its pool index on the first candidate, the sibling subsetters' set is made on the first value, and a body is scanned for declared features without listing its members first. `GRPCVerifyConstraint` allocates 14 887 times per constraint instead of 16 301, recovering most of what #1508 and #1512 added over 0.11.0's 14 091; the remaining 796 are the reads those fixes made (no parity claim). The warm gRPC instantiation (`InstantiateWarmModel`) allocates 1 996 times instead of 2 155, against 1 962 on 0.11.0.
7 changes: 5 additions & 2 deletions internal/exec/runtime/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ func (ctx *Context) resolveBindings(inst *Instance, target *FeatureValue, name s
var unmet []lower.Binding
path := name
for current := inst; current != nil; {
bindings := ctx.bindingsOf(current, path)
var bindings []lower.Binding
if ctx.declaresBindings(current) {
bindings = ctx.bindingsOf(current, path)
}
if len(bindings) != 0 {
ctx.resolvingBindings[key] = true
result, err := ctx.resolveBindingSet(current, inst, target, path, bindings, key)
Expand All @@ -237,7 +240,7 @@ func (ctx *Context) resolveBindings(inst *Instance, target *FeatureValue, name s
}
}
owner, ownerFeature := current.Owner()
if owner == nil || ownerFeature == "" {
if owner == nil || ownerFeature == "" || !ctx.ownersDeclareBindings(owner) {
break
}
path = ownerFeature + "." + path
Expand Down
72 changes: 40 additions & 32 deletions internal/exec/runtime/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -1674,44 +1674,52 @@ func isReferenceUsage(sym *symbols.Symbol) bool {
// declaresFeatures reports whether a usage's own body restates or adds features,
// which the object it materializes has to carry.
func declaresFeatures(sym *symbols.Symbol) bool {
return declaresAFeature(unwrappedDeclMembers(sym.Decl))
for member := range eachUnwrappedDeclMember(sym.Decl) {
if memberDeclaresFeature(member) {
return true
}
}
return false
}

// declaresAFeature reports whether one of a body's members declares a feature: a
// named or specializing usage, or a state body's named substate or entry, do or
// exit action, each a feature of the state declaring it (SysML v2 §7.18.2).
func declaresAFeature(members []ast.Node) bool {
for _, member := range members {
switch m := member.(type) {
case *ast.Usage:
if m.Ident.Name != "" || m.Ident.ShortName != "" || len(m.Relationships) > 0 {
return true
}
case *ast.SubjectMember:
// A subject the body values or defaults (`subject = x;`) restates the inherited parameter.
if m.BindingExpr != nil {
return true
}
case *ast.SubstateMember:
if m.Name != "" {
return true
}
case *ast.StateNode:
if m.Name != "" {
return true
}
case *ast.EntryMember:
if declaresAFeature(m.Actions) {
return true
}
case *ast.DoMember:
if declaresAFeature(m.Actions) {
return true
}
case *ast.ExitMember:
if declaresAFeature(m.Actions) {
return true
}
return slices.ContainsFunc(members, memberDeclaresFeature)
}

// memberDeclaresFeature reports whether one body member declares a feature (see declaresAFeature).
func memberDeclaresFeature(member ast.Node) bool {
switch m := member.(type) {
case *ast.Usage:
if m.Ident.Name != "" || m.Ident.ShortName != "" || len(m.Relationships) > 0 {
return true
}
case *ast.SubjectMember:
// A subject the body values or defaults (`subject = x;`) restates the inherited parameter.
if m.BindingExpr != nil {
return true
}
case *ast.SubstateMember:
if m.Name != "" {
return true
}
case *ast.StateNode:
if m.Name != "" {
return true
}
case *ast.EntryMember:
if declaresAFeature(m.Actions) {
return true
}
case *ast.DoMember:
if declaresAFeature(m.Actions) {
return true
}
case *ast.ExitMember:
if declaresAFeature(m.Actions) {
return true
}
}
return false
Expand Down
45 changes: 41 additions & 4 deletions internal/exec/runtime/invoke_calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package runtime

import (
"fmt"
"iter"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -1622,15 +1623,51 @@ func unwrappedDeclMembers(decl ast.Node) []ast.Node {
}

out := make([]ast.Node, 0, len(members))
for _, member := range members {
if membership, ok := member.(*ast.Membership); ok {
member = membership.Member
}
for member := range eachUnwrappedDeclMember(decl) {
out = append(out, member)
}
return out
}

// eachUnwrappedDeclMember yields what unwrappedDeclMembers lists without building the list,
// for the readers that stop at the first member of interest.
func eachUnwrappedDeclMember(decl ast.Node) iter.Seq[ast.Node] {
return func(yield func(ast.Node) bool) {
if oc, ok := ast.OwnedConstraintOf(decl); ok {
for _, member := range oc.Body {
if !yield(member) {
return
}
}
return
}
switch m := decl.(type) {
case *ast.AssumeMember:
for _, member := range m.Body {
if !yield(member) {
return
}
}
return
case *ast.RequireMember:
for _, member := range m.Body {
if !yield(member) {
return
}
}
return
}
for _, member := range ast.DeclMembers(decl) {
if membership, ok := member.(*ast.Membership); ok {
member = membership.Member
}
if !yield(member) {
return
}
}
}
}

// QualifiedName renders a symbol's qualified name, falling back to its
// local name when no index is reachable.
func (ctx *Context) QualifiedName(sym *symbols.Symbol) string {
Expand Down
2 changes: 1 addition & 1 deletion internal/exec/runtime/shape.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ func (ctx *Context) roleRedefiningDefault(sym, typeSym *symbols.Symbol) (ast.Nod
return nil, nil
}
scope := bodyScope(typeSym, typeSym.OwnerScope)
for _, node := range unwrappedDeclMembers(typeSym.Decl) {
for node := range eachUnwrappedDeclMember(typeSym.Decl) {
member := memberSymbol(scope, node)
if member == nil || member == sym || member.Name != "" {
continue
Expand Down
68 changes: 58 additions & 10 deletions internal/exec/runtime/subsetting.go
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,7 @@ func instancesAlsoIn(pool, els []Value) []Value {
// siblingSubsetterValues is the objects the other features subsetting what fv subsets already
// hold on inst: a value drawn for fv is one of them only where the subsetted feature has no other.
func (ctx *Context) siblingSubsetterValues(inst *Instance, fv *FeatureValue, subsetted []string) map[int64]bool {
taken := map[int64]bool{}
var taken map[int64]bool
for _, sub := range subsetted {
for _, feat := range ctx.subsettingFeaturesOf(inst, sub) {
sibling, ok := inst.FeatureValues[feat.Name]
Expand All @@ -1143,6 +1143,9 @@ func (ctx *Context) siblingSubsetterValues(inst *Instance, fv *FeatureValue, sub
}
for _, v := range elementsOf(sibling.HeldValue()) {
if v.Kind == ValInstance {
if taken == nil {
taken = map[int64]bool{}
}
taken[v.Instance] = true
}
}
Expand All @@ -1154,19 +1157,18 @@ func (ctx *Context) siblingSubsetterValues(inst *Instance, fv *FeatureValue, sub
// bindingWitnesses draws, per binding constraining the named feature, as many of its other end's
// values as the binding links, from pool where the subsetted features fix one (KerML 1.0 §7.4.6).
func (ctx *Context) bindingWitnesses(inst *Instance, name string, pool []Value, pooled bool, taken map[int64]bool) ([]Value, map[int64]bool, error) {
draw := &witnessDraw{fv: inst.FeatureValues[name], pooled: pooled, inPool: make(map[int64]bool, len(pool)), taken: taken}
for _, v := range pool {
draw.inPool[v.Instance] = true
}
draw := witnessDraw{fv: inst.FeatureValues[name], pooled: pooled, pool: pool, taken: taken}
// A binding of an owner naming the feature through its path constrains it as well.
for current, path := inst, name; current != nil; {
for _, binding := range ctx.bindingsOf(current, path) {
if err := ctx.drawBindingWitnesses(draw, inst, current, binding, path); err != nil {
return nil, nil, err
if ctx.declaresBindings(current) {
for _, binding := range ctx.bindingsOf(current, path) {
if err := ctx.drawBindingWitnesses(&draw, inst, current, binding, path); err != nil {
return nil, nil, err
}
}
}
owner, ownerFeature := current.Owner()
if owner == nil || ownerFeature == "" {
if owner == nil || ownerFeature == "" || !ctx.ownersDeclareBindings(owner) {
break
}
path = ownerFeature + "." + path
Expand All @@ -1175,17 +1177,63 @@ func (ctx *Context) bindingWitnesses(inst *Instance, name string, pool []Value,
return draw.chosen, draw.seen, nil
}

// ownersDeclareBindings reports whether inst or an owner of it declares a binding: where none
// does, a walk up from inst spells out no feature path, as no binding could name it.
func (ctx *Context) ownersDeclareBindings(inst *Instance) bool {
for current := inst; current != nil; {
if ctx.declaresBindings(current) {
return true
}
owner, ownerFeature := current.Owner()
if owner == nil || ownerFeature == "" {
return false
}
current = owner
}
return false
}

// declaresBindings reports whether any type of inst declares a binding, so that a walk over
// its owners spells out a feature path only where a binding could name it.
func (ctx *Context) declaresBindings(inst *Instance) bool {
if inst.Type != nil && len(ctx.objectBindings(inst.Type)) != 0 {
return true
}
for _, typ := range inst.classifiers {
if len(ctx.objectBindings(typ)) != 0 {
return true
}
}
return false
}

// witnessDraw is one bindingWitnesses draw: the feature drawn for, the pool it may draw
// from, what sibling subsetters hold, and what it has chosen so far.
type witnessDraw struct {
fv *FeatureValue
pooled bool
pool []Value
inPool map[int64]bool
taken map[int64]bool
chosen []Value
seen map[int64]bool
}

// admitsPooled reports whether a pooled draw may choose v: the pool index is built on the
// first candidate, so a walk that finds no binding never pays for it.
func (d *witnessDraw) admitsPooled(v Value) bool {
if !d.pooled {
return true
}
if d.inPool == nil {
d.inPool = make(map[int64]bool, len(d.pool))
for _, p := range d.pool {
d.inPool[p.Instance] = true
}
}
return d.inPool[v.Instance]
}

// drawBindingWitnesses draws from the other end of binding, when it constrains path at
// current, as many of that end's values as the binding links.
func (ctx *Context) drawBindingWitnesses(draw *witnessDraw, inst, current *Instance, binding lower.Binding, path string) error {
Expand Down Expand Up @@ -1231,7 +1279,7 @@ func (ctx *Context) drawBindingWitnesses(draw *witnessDraw, inst, current *Insta
// pick draws up to links admitted candidates, those no sibling subsetter holds first.
func (d *witnessDraw) pick(candidates []Value, links int64) {
admits := func(v Value) bool {
return v.Kind == ValInstance && (!d.pooled || d.inPool[v.Instance]) && !d.seen[v.Instance]
return v.Kind == ValInstance && d.admitsPooled(v) && !d.seen[v.Instance]
}
for n := int64(0); n < links; n++ {
pick := slices.IndexFunc(candidates, func(v Value) bool { return admits(v) && !d.taken[v.Instance] })
Expand Down
Loading