Skip to content

Commit abf8897

Browse files
committed
Scalar-replace nonescaping local atoms
1 parent 74d52bb commit abf8897

10 files changed

Lines changed: 506 additions & 16 deletions

File tree

benchmark/aot/fixtures/constant-arithmetic.glj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
(defn count-input [value]
99
(count value))
1010

11+
(defn local-counter []
12+
(let [state (atom 0)]
13+
(swap! state inc)
14+
(swap! state #(+ % 40))
15+
(reset! state (+ @state 1))
16+
@state))
17+
1118
(defn run []
1219
(loop [i 0
1320
total 0]

benchmark/aot/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ func writeBenchmark(temp string, fixtures []fixture) {
205205
source.WriteString("\tif got := lang.Apply1(countCaller, lang.NewVector(1, 2, 3)); !lang.Equals(got, int64(99)) {\n")
206206
source.WriteString("\t\tt.Fatalf(\"direct core call ignored count redefinition: got %v, want 99\", got)\n")
207207
source.WriteString("\t}\n")
208+
source.WriteString("\tlocalCounter := constant_arithmeticNS.FindInternedVar(lang.NewSymbol(\"local-counter\")).Get().(lang.IFn)\n")
209+
source.WriteString("\tif got := localCounter.Invoke(); !lang.Equals(got, int64(42)) {\n")
210+
source.WriteString("\t\tt.Fatalf(\"scalar-replaced local atom = %v, want 42\", got)\n")
211+
source.WriteString("\t}\n")
208212
source.WriteString("}\n\n")
209213
for _, fixture := range fixtures {
210214
benchmarkName := exportedName(strings.TrimPrefix(fixture.nsName, "bench."))

pkg/lang/atom.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ func (a *Atom) CompareAndSet(oldv, newv interface{}) bool {
119119

120120
func (a *Atom) compareAndSetBox(old *Box, newv interface{}) bool {
121121
// TODO: validate
122+
if Identical(old.val, newv) &&
123+
(a.watches == nil || a.watches.Count() == 0) {
124+
return a.state.CompareAndSwap(old, old)
125+
}
122126
swapped := a.state.CompareAndSwap(old, NewBox(newv))
123127
if swapped {
124128
a.notifyWatches(old.val, newv)
@@ -128,10 +132,19 @@ func (a *Atom) compareAndSetBox(old *Box, newv interface{}) bool {
128132

129133
func (a *Atom) Reset(newVal interface{}) interface{} {
130134
// TODO: validate
131-
132-
old := a.state.Swap(NewBox(newVal))
133-
a.notifyWatches(old.val, newVal)
134-
return newVal
135+
for {
136+
old := a.state.Load()
137+
if Identical(old.val, newVal) &&
138+
(a.watches == nil || a.watches.Count() == 0) {
139+
if a.state.CompareAndSwap(old, old) {
140+
return newVal
141+
}
142+
continue
143+
}
144+
old = a.state.Swap(NewBox(newVal))
145+
a.notifyWatches(old.val, newVal)
146+
return newVal
147+
}
135148
}
136149

137150
func (a *Atom) Meta() IPersistentMap {

pkg/lang/atom_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,42 @@ func TestAtomFixedAritySwapAllocatesOnlyNewState(t *testing.T) {
2929
t.Fatalf("Swap0 allocated %v objects, want at most 1", got)
3030
}
3131
}
32+
33+
func TestAtomIdenticalUpdatesAvoidNewStateAllocation(t *testing.T) {
34+
value := &struct{}{}
35+
atom := NewAtom(value)
36+
37+
if got := testing.AllocsPerRun(1_000, func() {
38+
atom.Reset(value)
39+
}); got != 0 {
40+
t.Fatalf("identical Reset allocated %v objects, want 0", got)
41+
}
42+
if got := testing.AllocsPerRun(1_000, func() {
43+
if !atom.CompareAndSet(value, value) {
44+
t.Fatal("identical CompareAndSet failed")
45+
}
46+
}); got != 0 {
47+
t.Fatalf("identical CompareAndSet allocated %v objects, want 0", got)
48+
}
49+
}
50+
51+
func TestAtomIdenticalUpdatesStillNotifyWatches(t *testing.T) {
52+
value := &struct{}{}
53+
atom := NewAtom(value)
54+
calls := 0
55+
atom.AddWatch("watch", FnFunc4(func(_, _, oldValue, newValue any) any {
56+
calls++
57+
if oldValue != value || newValue != value {
58+
t.Fatal("watch received the wrong value")
59+
}
60+
return nil
61+
}))
62+
63+
atom.Reset(value)
64+
if !atom.CompareAndSet(value, value) {
65+
t.Fatal("identical CompareAndSet failed")
66+
}
67+
if calls != 2 {
68+
t.Fatalf("watch called %d times, want 2", calls)
69+
}
70+
}

pkg/runtime/codegen.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ import (
3232

3333
// varScope represents a variable allocation scope
3434
type varScope struct {
35-
nextNum int
36-
names map[string]string // maps Clojure names to Go variable names
35+
nextNum int
36+
names map[string]string // maps Clojure names to Go variable names
37+
localAtoms map[string]bool // local atoms proven not to escape
3738
}
3839

3940
// recurContext represents the context for a loop/recur form
@@ -1669,6 +1670,9 @@ func (g *Generator) generateVarDeref(node *ast.Node) string {
16691670
// generateInvoke generates code for an Invoke node
16701671
func (g *Generator) generateInvoke(node *ast.Node) string {
16711672
invokeNode := node.Sub.(*ast.InvokeNode)
1673+
if result, ok := g.generateLocalAtomInvoke(invokeNode); ok {
1674+
return result
1675+
}
16721676
if plan := analyzeReducePipeline(invokeNode); plan != nil {
16731677
return g.generateAOTReducePipeline(invokeNode, plan)
16741678
}
@@ -2031,7 +2035,7 @@ func (g *Generator) generateLet(node *ast.Node, isLoop bool) string {
20312035
}
20322036

20332037
// Emit bindings directly to g.w
2034-
for _, binding := range letNode.Bindings {
2038+
for bindingIndex, binding := range letNode.Bindings {
20352039
bindingNode := binding.Sub.(*ast.BindingNode)
20362040
name := bindingNode.Name.Name()
20372041
init := bindingNode.Init
@@ -2040,9 +2044,25 @@ func (g *Generator) generateLet(node *ast.Node, isLoop bool) string {
20402044
g.writef("// let binding \"%s\"\n", name)
20412045

20422046
// Generate initialization code
2043-
initCode := g.generateASTNode(init)
2047+
var localAtomInit *ast.Node
2048+
if !isLoop {
2049+
localAtomInit = scalarReplaceableAtomInit(
2050+
bindingNode,
2051+
letNode.Bindings[bindingIndex+1:],
2052+
letNode.Body,
2053+
)
2054+
}
2055+
initCode := ""
2056+
if localAtomInit != nil {
2057+
initCode = g.generateASTNode(localAtomInit)
2058+
} else {
2059+
initCode = g.generateASTNode(init)
2060+
}
20442061
varName := g.allocateLocal(name)
20452062
g.writef("var %s any = %s\n", varName, initCode)
2063+
if localAtomInit != nil {
2064+
g.markLocalAtom(name)
2065+
}
20462066
g.writeAssign("_", varName) // Prevent unused variable warning
20472067

20482068
// Collect binding variables for loop
@@ -2754,8 +2774,9 @@ func (g *Generator) pushVarScope() {
27542774

27552775
// Push new scope onto the stack
27562776
g.varScopes = append(g.varScopes, varScope{
2757-
nextNum: nextNum,
2758-
names: make(map[string]string),
2777+
nextNum: nextNum,
2778+
names: make(map[string]string),
2779+
localAtoms: make(map[string]bool),
27592780
})
27602781
}
27612782

@@ -2866,6 +2887,20 @@ func (g *Generator) getLocal(name string) string {
28662887
panic(fmt.Sprintf("variable %s not found in any scope", name))
28672888
}
28682889

2890+
func (g *Generator) markLocalAtom(name string) {
2891+
g.varScopes[len(g.varScopes)-1].localAtoms[name] = true
2892+
}
2893+
2894+
func (g *Generator) getLocalAtom(name string) (string, bool) {
2895+
for i := len(g.varScopes) - 1; i >= 0; i-- {
2896+
scope := &g.varScopes[i]
2897+
if varName, ok := scope.names[name]; ok {
2898+
return varName, scope.localAtoms[name]
2899+
}
2900+
}
2901+
return "", false
2902+
}
2903+
28692904
// allocateTempVar allocates a fresh temporary variable without name tracking
28702905
func (g *Generator) allocateTempVar() string {
28712906
if len(g.varScopes) == 0 {

pkg/runtime/codegen_direct.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ func (g *Generator) aotExternalInvokeTarget(
204204
}
205205
arity := len(invoke.Args)
206206
intrinsic := aotExternalIntrinsic(vr, arity)
207-
if arity > 5 || !aotSupportsArity(codegenVarValue(vr), arity) {
207+
if arity > 5 ||
208+
(intrinsic == "" && !aotSupportsArity(codegenVarValue(vr), arity)) {
208209
return nil
209210
}
210211
key := aotExternalCallKey{vr: vr, arity: arity}
@@ -236,11 +237,13 @@ func aotExternalIntrinsic(vr *lang.Var, arity int) string {
236237
switch {
237238
case name == "assoc" && (arity == 3 || arity == 5):
238239
case name == "count" && arity == 1:
240+
case name == "dec" && arity == 1:
239241
case name == "cons" && arity == 2:
240242
case name == "conj" && arity == 2:
241243
case name == "empty?" && arity == 1:
242244
case name == "first" && arity == 1:
243245
case name == "get" && (arity == 2 || arity == 3):
246+
case name == "inc" && arity == 1:
244247
case name == "next" && arity == 1:
245248
case name == "nth" && (arity == 2 || arity == 3):
246249
case name == "peek" && arity == 1:
@@ -267,6 +270,8 @@ func (g *Generator) aotExternalIntrinsicCall(
267270
)
268271
case "count":
269272
return fmt.Sprintf("lang.Count(%s)", args[0])
273+
case "dec":
274+
return fmt.Sprintf("lang.Numbers.Dec(%s)", args[0])
270275
case "cons":
271276
return fmt.Sprintf("lang.NewCons(%s, %s)", args[0], args[1])
272277
case "conj":
@@ -280,6 +285,8 @@ func (g *Generator) aotExternalIntrinsicCall(
280285
return fmt.Sprintf("lang.Get(%s, %s)", args[0], args[1])
281286
}
282287
return fmt.Sprintf("lang.GetDefault(%s, %s, %s)", args[0], args[1], args[2])
288+
case "inc":
289+
return fmt.Sprintf("lang.Numbers.Inc(%s)", args[0])
283290
case "next":
284291
return fmt.Sprintf("lang.Next(%s)", args[0])
285292
case "nth":

0 commit comments

Comments
 (0)