Skip to content

Commit aba3746

Browse files
committed
Fuse guarded square and take pipelines
1 parent eaf816e commit aba3746

21 files changed

Lines changed: 678 additions & 175 deletions

benchmark/RESULTS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ benchmarks; allocations are reported by Go's benchmark harness.
77
| --- | ---: | ---: | ---: |
88
| AOT float kernel | 232 ms, ~17.0M allocs | 1.265 ms, 1 alloc | ~183× |
99
| AOT reduce pipeline | 95 ms, ~5.87M allocs | 1.905 ms, 48 allocs | ~50× |
10+
| Exact let-go map/filter, AOT hot path | 33.42 µs, 46.0 KiB, 1,208 allocs | 7.40 µs, 1.12 KiB, 49 allocs | ~4.5× |
1011
| Boxed common-int arithmetic | 18.1 ns, 19 B, 2 allocs | 1.98 ns, 0 B, 0 allocs | ~9.1× |
1112
| AOT Game of Life | 982.22 ms, ~31.1M allocs | 686.85 ms, ~1.57M allocs | ~1.43× |
1213
| Interpreter constant arithmetic | 113.27 ms | 101.99 ms | ~10% |
1314
| Interpreter constant branch | 304.61 ms, ~20.0M allocs | 59.45 ms, 6 allocs | ~5.1× |
15+
| Exact let-go map/filter, interpreter hot path | 40.09 µs, 49.9 KiB, 1,224 allocs | 7.64 µs, 1.12 KiB, 48 allocs | ~5.2× |
1416

1517
The AOT baselines were captured before primitive float specialization and
1618
integer pipeline fusion. The interpreter comparison uses
1719
`benchmark/interpreter/constant_arithmetic_test.go` immediately before and
18-
after literal `lang.Numbers` folding.
20+
after literal `lang.Numbers` folding. The exact let-go rows compare commit
21+
`eaf816e` with the guarded square/filter/take pipeline optimization using the
22+
fixtures committed in the corresponding benchmark directories.
1923

2024
The portable programs shared with let-go currently produce these median
2125
wall-clock ratios, where a value below 1.0 favors Glojure:

benchmark/aot/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ The fixtures deliberately cover optimization gaps:
2323
subexpression, through a direct function call.
2424
- `float-kernel.glj`: repeated floating-point arithmetic through a direct
2525
function call.
26+
- `letgo-map-filter.glj`: let-go's exact map/filter/take benchmark, including
27+
its anonymous integer-square callback.
2628
- `reduce-pipeline.glj`: reduction over lazy `map` and `filter` producers.
2729

2830
Run a baseline before changing the compiler and compare it with the same
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
(ns bench.letgo-map-filter)
2+
3+
(defn run []
4+
(reduce + 0
5+
(take 100
6+
(filter even?
7+
(map #(* % %)
8+
(range 10000))))))
9+
10+
(def expected 1313400)

benchmark/aot/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ func writeBenchmark(temp string, fixtures []fixture) {
189189
source.WriteString("\tif got := reduce_pipelineRun.Invoke(); !lang.Equals(got, int64(250000000000)) {\n")
190190
source.WriteString("\t\tt.Fatalf(\"reduce pipeline ignored Var redefinition: got %v, want 250000000000\", got)\n")
191191
source.WriteString("\t}\n")
192+
source.WriteString("\tincVar.BindRoot(originalInc)\n")
193+
source.WriteString("\ttakeVar := lang.FindNamespace(lang.NewSymbol(\"clojure.core\")).FindInternedVar(lang.NewSymbol(\"take\"))\n")
194+
source.WriteString("\toriginalTake := takeVar.Get()\n")
195+
source.WriteString("\ttakeVar.BindRoot(lang.FnFunc2(func(any, any) any { return lang.NewVector(int64(7)) }))\n")
196+
source.WriteString("\tdefer takeVar.BindRoot(originalTake)\n")
197+
source.WriteString("\tif got := letgo_map_filterRun.Invoke(); !lang.Equals(got, int64(7)) {\n")
198+
source.WriteString("\t\tt.Fatalf(\"let-go map/filter pipeline ignored take redefinition: got %v, want 7\", got)\n")
199+
source.WriteString("\t}\n")
192200
source.WriteString("}\n\n")
193201
for _, fixture := range fixtures {
194202
benchmarkName := exportedName(strings.TrimPrefix(fixture.nsName, "bench."))

benchmark/interpreter/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ startup.
1515
`constant-branch` measures whether a literal predicate inside a numeric loop
1616
prevents the interpreter's existing typed-loop compiler from recognizing the
1717
surrounding loop.
18+
19+
`let-go-map-filter` and `let-go-tak` reproduce the two official let-go
20+
interpreter workloads that are closest to Glojure. Keeping them in-process
21+
removes executable startup noise while profiling and validating changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package interpreterbench
2+
3+
import (
4+
"testing"
5+
6+
"github.com/glojurelang/glojure/pkg/lang"
7+
"github.com/glojurelang/glojure/pkg/runtime"
8+
)
9+
10+
const (
11+
letGoMapFilterExpression = `
12+
(reduce + 0
13+
(take 100
14+
(filter even?
15+
(map #(* % %) (range 10000)))))`
16+
letGoMapFilterSource = `
17+
(defn benchmark-let-go-map-filter-run []
18+
` + letGoMapFilterExpression + `)`
19+
)
20+
21+
func BenchmarkLetGoMapFilter(b *testing.B) {
22+
runtime.ReadEval(letGoMapFilterSource)
23+
run := benchmarkFn(b, "benchmark-let-go-map-filter-run")
24+
if got := run.Invoke(); got != int64(1_313_400) {
25+
b.Fatalf("run = %v, want 1313400", got)
26+
}
27+
28+
b.ReportAllocs()
29+
b.ResetTimer()
30+
for i := 0; i < b.N; i++ {
31+
benchmarkResult = run.Invoke()
32+
}
33+
}
34+
35+
func BenchmarkLoadLetGoMapFilter(b *testing.B) {
36+
b.ReportAllocs()
37+
for i := 0; i < b.N; i++ {
38+
benchmarkResult = runtime.ReadEval(letGoMapFilterExpression)
39+
}
40+
}
41+
42+
func BenchmarkLetGoTak(b *testing.B) {
43+
runtime.ReadEval(`
44+
(defn benchmark-let-go-tak [x y z]
45+
(if (< y x)
46+
(benchmark-let-go-tak
47+
(benchmark-let-go-tak (dec x) y z)
48+
(benchmark-let-go-tak (dec y) z x)
49+
(benchmark-let-go-tak (dec z) x y))
50+
z))
51+
52+
(defn benchmark-let-go-tak-run []
53+
(benchmark-let-go-tak 30 22 12))`)
54+
run := benchmarkFn(b, "benchmark-let-go-tak-run")
55+
if got := run.Invoke(); got != int64(13) {
56+
b.Fatalf("run = %v, want 13", got)
57+
}
58+
59+
b.ReportAllocs()
60+
b.ResetTimer()
61+
for i := 0; i < b.N; i++ {
62+
benchmarkResult = run.Invoke()
63+
}
64+
}
65+
66+
func benchmarkFn(tb testing.TB, name string) lang.IFn {
67+
tb.Helper()
68+
return lang.GlobalEnv.CurrentNamespace().
69+
FindInternedVar(lang.NewSymbol(name)).
70+
Get().(lang.IFn)
71+
}

pkg/lang/interfaces.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ type (
5959
ReduceInt64(acc, value int64) int64
6060
}
6161

62+
// Int64ReductionStepper is an optional unboxed reduction path that can
63+
// terminate early without manufacturing a boxed Reduced value.
64+
Int64ReductionStepper interface {
65+
ReduceInt64Step(acc, value int64) (result int64, reduced bool)
66+
}
67+
68+
// Int64StepReducible is an optional unboxed reduction path for sources
69+
// that can stop when an Int64ReductionStepper signals completion.
70+
Int64StepReducible interface {
71+
ReduceInt64Steps(reducer Int64ReductionStepper, initial int64) int64
72+
}
73+
6274
IKVReduce interface {
6375
KVReduce(f IFn, init any) any
6476
}

pkg/lang/longrange.go

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ type (
2020
LongChunk struct {
2121
start, step int64
2222
count int
23-
nthFn FnFunc1
2423
}
2524
)
2625

2726
var (
28-
_ ISeq = (*LongRange)(nil)
29-
_ Sequential = (*LongRange)(nil)
30-
_ IReduce = (*LongRange)(nil)
31-
_ IReduceInit = (*LongRange)(nil)
32-
_ ASeq = (*LongRange)(nil)
33-
_ IDrop = (*LongRange)(nil)
34-
_ IChunkedSeq = (*LongRange)(nil)
35-
_ Counted = (*LongRange)(nil)
27+
_ ISeq = (*LongRange)(nil)
28+
_ Sequential = (*LongRange)(nil)
29+
_ IReduce = (*LongRange)(nil)
30+
_ IReduceInit = (*LongRange)(nil)
31+
_ Int64StepReducible = (*LongRange)(nil)
32+
_ ASeq = (*LongRange)(nil)
33+
_ IDrop = (*LongRange)(nil)
34+
_ IChunkedSeq = (*LongRange)(nil)
35+
_ Counted = (*LongRange)(nil)
3636

3737
_ IChunk = (*LongChunk)(nil)
3838
_ IReduceInit = (*LongChunk)(nil)
@@ -236,6 +236,25 @@ func (r *LongRange) ReduceInit(f IFn, init any) any {
236236
return ret
237237
}
238238

239+
func (r *LongRange) ReduceInt64Steps(
240+
reducer Int64ReductionStepper,
241+
initial int64,
242+
) int64 {
243+
result := initial
244+
value := r.start
245+
for index := 0; index < r.count; index++ {
246+
var reduced bool
247+
result, reduced = reducer.ReduceInt64Step(result, value)
248+
if reduced {
249+
return result
250+
}
251+
if index+1 < r.count {
252+
value += r.step
253+
}
254+
}
255+
return result
256+
}
257+
239258
func (r *LongRange) Drop(n int) Sequential {
240259
if n < 0 {
241260
return r
@@ -251,13 +270,11 @@ func (r *LongRange) Drop(n int) Sequential {
251270
// LongChunk
252271

253272
func NewLongChunk(start, step int64, count int) *LongChunk {
254-
chunk := &LongChunk{
273+
return &LongChunk{
255274
start: start,
256275
step: step,
257276
count: count,
258277
}
259-
chunk.nthFn = func(i any) any { return chunk.Nth(MustAsInt(i)) }
260-
return chunk
261278
}
262279

263280
func (c *LongChunk) First() any {
@@ -282,7 +299,9 @@ func (c *LongChunk) Count() int {
282299
func (c *LongChunk) fieldOrMethod(name string) (interface{}, bool) {
283300
switch name {
284301
case "nth", "Nth":
285-
return c.nthFn, true
302+
return FnFunc1(func(i any) any {
303+
return c.Nth(MustAsInt(i))
304+
}), true
286305
case "nthDefault", "NthDefault":
287306
return FnFunc2(func(i, notFound any) any {
288307
return c.NthDefault(MustAsInt(i), notFound)

pkg/lang/longrange_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@ package lang
22

33
import "testing"
44

5+
type stoppingInt64Reducer struct {
6+
FnFunc2
7+
calls int
8+
stopAfter int
9+
}
10+
11+
func (r *stoppingInt64Reducer) ReduceInt64Step(
12+
acc, value int64,
13+
) (int64, bool) {
14+
r.calls++
15+
return acc + value, r.calls == r.stopAfter
16+
}
17+
518
func TestLongRangeChunksAreBounded(t *testing.T) {
619
r := NewLongRange(0, 100, 1).(*LongRange)
720

@@ -48,3 +61,15 @@ func TestDescendingLongRangeChunksAreBounded(t *testing.T) {
4861
t.Fatalf("first value after first chunk = %v, want 68", got)
4962
}
5063
}
64+
65+
func TestLongRangeInt64StepsCanStopDescendingReduction(t *testing.T) {
66+
r := NewLongRange(10, 0, -2).(*LongRange)
67+
reducer := &stoppingInt64Reducer{stopAfter: 3}
68+
got := r.ReduceInt64Steps(reducer, int64(0))
69+
if got != int64(24) {
70+
t.Fatalf("ReduceInt64Steps = %v, want 24", got)
71+
}
72+
if reducer.calls != 3 {
73+
t.Fatalf("reducer calls = %d, want 3", reducer.calls)
74+
}
75+
}

pkg/lang/slicechunk.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import "errors"
44

55
type (
66
SliceChunk struct {
7-
slc []interface{}
8-
nthFn FnFunc1
7+
slc []interface{}
98
}
109
)
1110

@@ -14,11 +13,7 @@ var (
1413
)
1514

1615
func NewSliceChunk(slc []interface{}) *SliceChunk {
17-
chunk := &SliceChunk{
18-
slc: slc,
19-
}
20-
chunk.nthFn = func(i any) any { return chunk.Nth(MustAsInt(i)) }
21-
return chunk
16+
return &SliceChunk{slc: slc}
2217
}
2318

2419
func (sc *SliceChunk) Count() int {
@@ -48,7 +43,9 @@ func (sc *SliceChunk) NthDefault(i int, def interface{}) interface{} {
4843
func (sc *SliceChunk) fieldOrMethod(name string) (interface{}, bool) {
4944
switch name {
5045
case "nth", "Nth":
51-
return sc.nthFn, true
46+
return FnFunc1(func(i any) any {
47+
return sc.Nth(MustAsInt(i))
48+
}), true
5249
case "nthDefault", "NthDefault":
5350
return FnFunc2(func(i, notFound any) any {
5451
return sc.NthDefault(MustAsInt(i), notFound)

0 commit comments

Comments
 (0)