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
67 changes: 67 additions & 0 deletions benchmarks/decs/bench_from_decs_count.das
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
options gen2
options persistent_heap
options no_unused_block_arguments = false
options no_unused_function_arguments = false

require dastest/testing_boost
require daslib/decs_boost
require daslib/linq
require daslib/linq_boost

// Bare-count benchmark. Validates Slice 1 of Approach Z: _fold(from_decs_template(type<Foo>).count())
// should splice to the arch.size shortcut — sum of per-archetype arch.size in a single for_each_archetype walk.

let N = 100000

[decs_template(prefix = "bench_")]
struct BenchCountRow {
val : int
}

def fixture(n : int) {
restart()
create_entities(n) $(eid : EntityId; i : int; var cmp : ComponentMap) {
apply_decs_template(cmp, BenchCountRow(val = i))
}
}

// m1: hand-written for_each_archetype + arch.size — the ideal target.
[benchmark]
def from_decs_count_m1_hand(b : B?) {
fixture(N)
b |> run("m1_hand_arch_size/{N}", N) {
var erq : EcsRequest
erq.req |> push("bench_val")
var total = 0
for_each_archetype(erq) $(arch : Archetype) {
total += arch.size
}
if (total == 0) {
b->failNow()
}
}
}

// m2: from_decs_template.count() bare — eager bridge baseline (no splice).
[benchmark]
def from_decs_count_m2_eager(b : B?) {
fixture(N)
b |> run("m2_eager_bridge/{N}", N) {
let total = from_decs_template(type<BenchCountRow>).count()
if (total == 0) {
b->failNow()
}
}
}

// m3: _fold(from_decs_template.count()) — Slice 1 splice target.
[benchmark]
def from_decs_count_m3_fold(b : B?) {
fixture(N)
b |> run("m3_fold_splice/{N}", N) {
let total = _fold(from_decs_template(type<BenchCountRow>).count())
if (total == 0) {
b->failNow()
}
}
}
104 changes: 104 additions & 0 deletions benchmarks/decs/bench_from_decs_template_sum.das
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
options gen2
options persistent_heap
options no_unused_block_arguments = false
options no_unused_function_arguments = false

require dastest/testing_boost
require daslib/decs_boost
require daslib/linq
require daslib/linq_boost

// Pattern 3 motivation benchmark: from_decs* + linq chain vs hand-written query.
// Establishes the current (eager-bridge + to_sequence + lambda-per-element) cost
// so the new splice can be measured against a clear baseline + the hand-written
// query ideal.

let N = 100000
let THRESHOLD = 50

[decs_template(prefix = "bench_")]
struct BenchSumRow {
val : int
}

def fixture(n : int) {
restart()
create_entities(n) $(eid : EntityId; i : int; var cmp : ComponentMap) {
apply_decs_template(cmp, BenchSumRow(val = (i * 37) % 1000))
}
}

// m1: hand-written query — the splice's ideal target (no iterator, no lambda,
// per-element body inlined into the archetype walk).
[benchmark]
def from_decs_sum_m1_query_hand(b : B?) {
fixture(N)
b |> run("m1_query_hand/{N}", N) {
var total = 0
query() $(bench_val : int) {
if (bench_val > THRESHOLD) {
total += bench_val
}
}
if (total == 0) {
b->failNow()
}
}
}

// m2: from_decs_template eager bridge (current).
// from_decs_template(type<Foo>)._where(p)._select(s).sum() →
// eager bridge materializes array<tuple<val:int>>, wraps with to_sequence,
// downstream chain pays lambda-per-element on top of materialization.
[benchmark]
def from_decs_sum_m2_template_eager(b : B?) {
fixture(N)
b |> run("m2_from_decs_template_eager/{N}", N) {
let total = (
from_decs_template(type<BenchSumRow>)
._where(_.val > THRESHOLD)
._select(_.val)
.sum()
)
if (total == 0) {
b->failNow()
}
}
}

// m3: from_decs block-form eager bridge (same path as m2 but via the older,
// non-template macro). Same cost class.
[benchmark]
def from_decs_sum_m3_block_eager(b : B?) {
fixture(N)
b |> run("m3_from_decs_block_eager/{N}", N) {
let total = (
from_decs($(bench_val : int){})
._where(_.bench_val > THRESHOLD)
._select(_.bench_val)
.sum()
)
if (total == 0) {
b->failNow()
}
}
}

// m4: from_decs_template + _fold(...) (post-Phase 2/3 splice may or may not
// fire here depending on planner-cascade coverage). Reveals whether the existing
// linq_fold splice handles the from_decs* eager bridge at all on master baseline.
[benchmark]
def from_decs_sum_m4_template_fold(b : B?) {
fixture(N)
b |> run("m4_from_decs_template_fold/{N}", N) {
let total = _fold(
from_decs_template(type<BenchSumRow>)
._where(_.val > THRESHOLD)
._select(_.val)
.sum()
)
if (total == 0) {
b->failNow()
}
}
}
86 changes: 86 additions & 0 deletions daslib/decs_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -703,3 +703,89 @@ class FromDecsMacro : AstCallMacro {
)
}
}

[call_macro(name="from_decs_template")]
class FromDecsTemplateMacro : AstCallMacro {
//! This macro converts a DECS query over a ``[decs_template]`` struct into an ``iterator<tuple<...>>``.
//! The field list (names and types) is derived from the struct definition; component names are
//! the struct's prefix (per ``[decs_template(prefix=...)]``) joined with each field name. For example::
//!
//! [decs_template(prefix="particle_")]
//! struct Particle { pos, vel : float3 }
//! let it = from_decs_template(type<Particle>)
//! for (item in it) {
//! print("pos {item.pos}, vel {item.vel}\n")
//! }
//!
//! This is the eager bridge form — equivalent to writing
//! ``from_decs($(particle_pos : float3; particle_vel : float3){})`` by hand and projecting back into
//! field names. Tuple element types are by-value (the query's archetype refs do not outlive the
//! invoke block).
def override visit(prog : ProgramPtr; mod : Module?; var expr : ExprCallMacro?) : ExpressionPtr {
macro_verify(length(expr.arguments) == 1, prog, expr.at, "expecting from_decs_template(type<Foo>)")
macro_verify(expr.arguments[0] is ExprTypeDecl, prog, expr.at, "expecting from_decs_template(type<Foo>), got {expr.arguments[0].__rtti}")
let typeExpr = expr.arguments[0] as ExprTypeDecl
let td = typeExpr.typeexpr
macro_verify(td != null && td.baseType == Type.tStructure, prog, expr.at, "from_decs_template expects type<Struct>, got non-struct type")
let st = td.structType
macro_verify(st != null, prog, expr.at, "from_decs_template: struct type is not resolved")
// Resolve [decs_template] annotation + prefix
var prefix = ""
var hasTemplate = false
for (ann in st.annotations) {
if (ann.annotation.name == "decs_template") {
hasTemplate = true
let ppref = ann.arguments |> decs_prefix
prefix = (ppref is yes) ? (ppref as yes) : "{st.name}_"
break
}
}
macro_verify(hasTemplate, prog, expr.at, "from_decs_template: struct {st.name} is not annotated with [decs_template]")
macro_verify(!empty(st.fields), prog, expr.at, "from_decs_template: struct {st.name} has no fields")
// Tuple type (field names as record names, by-value types stripped of ref/const)
var tupleType = new TypeDecl(baseType = Type.tTuple, at = expr.at)
tupleType.argNames.resize(length(st.fields))
for (fld, idx in st.fields, count()) {
var ft = clone_type(fld._type)
ft.flags.ref = false
ft.flags.constant = false
tupleType.argTypes |> emplace_new(ft)
tupleType.argNames[idx] := fld.name
}
// Tuple constructor: pull each prefixed component into a record-named field
var mkTuple = new ExprMakeTuple(at = expr.at)
mkTuple.recordNames.resize(length(st.fields))
for (fld, idx in st.fields, count()) {
mkTuple.values |> push(new ExprVar(at = fld.at, name := "{prefix}{fld.name}"))
mkTuple.recordNames[idx] := fld.name
}
// Query block: one arg per field, name = prefixed component, type = const non-ref
// fld.init (if set) clones in so the query macro picks get_default_ro
var qblock = qmacro(${
res |> push($e(mkTuple))
})
var qba = qblock as ExprMakeBlock
var qbb = qba._block as ExprBlock
qbb.blockFlags.isClosure = true
for (fld in st.fields) {
var argType = clone_type(fld._type)
argType.flags.ref = false
argType.flags.constant = true
qbb.arguments |> emplace_new <| new Variable(at = fld.at,
name := "{prefix}{fld.name}",
_type = argType,
init = clone_expression(fld.init)
)
}
// Call query macro and wrap in invoke
var callquery = make_call(expr.at, "query")
(callquery as ExprCallMacro).arguments |> push(clone_expression(qblock))
return qmacro(
invoke(${
var res : array<$t(tupleType)>
$e(callquery)
return res.to_sequence()
})
)
}
}
Loading
Loading