diff --git a/docs/adr/0007-symbolic-bounds.md b/docs/adr/0007-symbolic-bounds.md index d64b7e45..49a73726 100644 --- a/docs/adr/0007-symbolic-bounds.md +++ b/docs/adr/0007-symbolic-bounds.md @@ -87,7 +87,9 @@ Replace the fact model with one representation: The compiler is at about +29% of its time before step 1, past the ceiling; the next step cuts first. The copy analysis's one pass per arm (`defk-o08.3`, 6.39) took 9% off: +17.6% of the work before step 1, still - past the ceiling. + past the ceiling. Each statement's reads walked once (`defk-o08.4`, 6.40) + took 8.6% more off: +7.4% of the work before step 1 (-1.0% without + `SatEx.Kernels`), under the ceiling again. - **Raising guards become the way to state facts about arguments** in a kernel, which the language docs would describe. - **Out of scope:** degree 3 and nested products, facts across loop diff --git a/docs/performance.md b/docs/performance.md index f9e449d1..1d2f4d10 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -2577,6 +2577,30 @@ else: 0))`; it now says `if first < 0 and n > 0, do: raise(ArgumentError, - **Source:** `defk-o08.3`; `test/copies_test.exs`. - **Status:** measured, provisional. +### 6.40 Each statement's reads walked once: another -9% of the compiler's work + +- **Numbers:** entering a statement computed its reads three times: in + `Copies.live_after/1` (`stmt_reads/1`), in the compiler's `hoist/3` and + in `Copies.enter_stmt/4` (`reads/1`, the same set: `read_counts/1` skips a + match's left side as `stmt_reads/1` does). `Copies.block_liveness/1` keeps + each statement's reads beside its live set, and `enter/3` hands them on. + Over the 108 modules of `mix dump_c`, two rounds each, interleaved with + main: 125.9 -> 115.0 M reductions (-8.6%, the same without + `SatEx.Kernels`), 987 -> 934 ms (-5.4%); `HashChain.Kernels` and its + Elixir-mode twin -2.2 M reductions each, `SatEx.Kernels` -3.6 M. Against + the compiler before step 1 (6.38's baseline, by reductions): +7.4% + (-1.0% without `SatEx.Kernels`), from +17.6%: under ADR-0007's ceiling. +- **Same C:** `mix dump_c` byte-identical for all 108 modules; a property + test checks `reads/1 == stmt_reads/1` and that `block_liveness/1` agrees + with `live_after/1` on generated blocks. +- **Not done:** a statement nested d blocks deep is still walked once per + enclosing statement (its reads are part of theirs); building a + statement's reads from its blocks' would need the walk to follow the + lowering's order. +- **Conditions:** M-series Mac on battery, no quiet check; `cc` not run. +- **Source:** `defk-o08.4`; `test/copies_test.exs`. +- **Status:** measured, provisional. + --- ## 7. When a hand-written or wrapper NIF is still the answer @@ -2884,4 +2908,4 @@ example or close a workload ticket, add its entries and its line here. `defk-sve`, `defk-sve.1` ([`state-copy-cost.md`](https://github.com/DanielLuning/defk/blob/main/docs/research/state-copy-cost.md)), `defk-ulk`, `defk-ulk.1` ([`simd.md`](https://github.com/DanielLuning/defk/blob/main/docs/research/simd.md)), `defk-33b`, `defk-ulk.4`, `defk-52v.15` ([`cdcl_sat.exs`](https://github.com/DanielLuning/defk/blob/main/examples/cdcl_sat.exs)), `defk-tif`, `defk-gqk`, `defk-6ag`, `defk-7dm` ([`bench/port_roundtrip.exs`](https://github.com/DanielLuning/defk/blob/main/bench/port_roundtrip.exs)), `defk-1ub`, - `defk-o08.2`, `defk-o08.3`. + `defk-o08.2`, `defk-o08.3`, `defk-o08.4`. diff --git a/lib/defk/compiler.ex b/lib/defk/compiler.ex index b5f3a66d..53a646f5 100644 --- a/lib/defk/compiler.ex +++ b/lib/defk/compiler.ex @@ -369,7 +369,8 @@ defmodule Defk.Compiler do case Code.string_to_quoted(src) do # the statements are lowered in the block of the statement env is at {:ok, ast} -> - probe(fn -> stmt_list(exprs(ast), Copies.live_after(exprs(ast)), env) end) != :error + probe(fn -> stmt_list(exprs(ast), Copies.block_liveness(exprs(ast)), env) end) != + :error _ -> false @@ -906,7 +907,7 @@ defmodule Defk.Compiler do defp lower_block_env(ast, env) do es = exprs(ast) {init, [last]} = Enum.split(es, -1) - lives = Copies.live_after(es) + lives = Copies.block_liveness(es) {s1, env} = stmt_list(init, lives, nested(env, ast)) {s2, v, env} = @@ -957,10 +958,11 @@ defmodule Defk.Compiler do # A block evaluated only for effect, nested like lower_block's. defp lower_stmts(ast, env) do es = exprs(ast) - stmt_list(es, Copies.live_after(es), nested(env, ast)) + stmt_list(es, Copies.block_liveness(es), nested(env, ast)) end - # statements `es` of one block; `lives` has the names read after each + # statements `es` of one block; `lives` has, for each, the names read after + # it and the names it reads (Copies.block_liveness/1) defp stmt_list(es, lives, env) do Enum.zip(AllocZeroing.mark(es), lives) |> Enum.reduce({[], env}, fn {e, live}, {acc, env} -> @@ -1349,12 +1351,13 @@ defmodule Defk.Compiler do defp array_ids({:arr, _}), do: %{id: make_ref(), buf: make_ref(), fin: false, depth: 1} defp array_ids(_t), do: %{} - # Before statement `e` of a block, `live` the names read after it: copy - # what its nested writes need (hoist/3), then note what is live. - defp enter(e, live, env) do + # Before statement `e` of a block, `live` the names read after it and + # `reads` those it reads: copy what its nested writes need (hoist/4), then + # note what is live. + defp enter(e, {live, reads}, env) do names = Copies.live_names(e, live) - {sh, env} = hoist(e, names, env) - {sh, %{env | scope: Copies.enter_stmt(env.scope, e, names, env.vars)}} + {sh, env} = hoist(e, names, reads, env) + {sh, %{env | scope: Copies.enter_stmt(env.scope, e, names, reads, env.vars)}} end # A variable bound outside the branches and loop bodies of `e` and written @@ -1363,8 +1366,8 @@ defmodule Defk.Compiler do # only binding of an owned buffer, so its writes are in place, and a loop # copies once. An input array written in a branch is copied even when the # branch is not taken. - defp hoist(e, names, env) do - seen = MapSet.union(names, Copies.reads(e)) + defp hoist(e, names, reads, env) do + seen = MapSet.union(names, reads) Enum.reduce(Copies.nested_writes(e), {[], env}, fn n, {acc, env} -> case env.vars do @@ -5099,7 +5102,7 @@ defmodule Defk.Compiler do defp lower_tail(ast, env, leaf) do es = exprs(ast) {init, [last]} = Enum.split(es, -1) - lives = Copies.live_after(es) + lives = Copies.block_liveness(es) {s, env} = stmt_list(init, lives, nested(env, ast)) [ diff --git a/lib/defk/copies.ex b/lib/defk/copies.ex index 8f157f06..8778d1fe 100644 --- a/lib/defk/copies.ex +++ b/lib/defk/copies.ex @@ -177,10 +177,17 @@ defmodule Defk.Copies do def stmt_reads(e), do: reads(e) # for each statement of a block, the names read after it in the block - def live_after(es) do + def live_after(es), do: es |> block_liveness() |> Enum.map(&elem(&1, 0)) + + # for each statement of a block, {the names read after it in the block, + # the names it reads}: what entering it needs (enter_stmt/5), from one + # walk of it. stmt_reads/1 is reads/1 of the statement: read_counts/1 + # skips a match's left side as stmt_reads/1 does. + def block_liveness(es) do {lives, _} = List.foldr(es, {[], MapSet.new()}, fn e, {acc, live} -> - {[live | acc], live |> MapSet.difference(binds(e)) |> MapSet.union(stmt_reads(e))} + reads = stmt_reads(e) + {[{live, reads} | acc], live |> MapSet.difference(binds(e)) |> MapSet.union(reads)} end) lives @@ -289,9 +296,12 @@ defmodule Defk.Copies do # with `vars` the bindings after the copies hoisted before `e`: what is # live after `e`, and for the blocks nested in it (which also see what `e` # reads, as a later iteration or a later arm does). - def enter_stmt(%Scope{} = s, e, names, vars) do + # `reads` is reads(e), when the caller has it (block_liveness/1). + def enter_stmt(%Scope{} = s, e, names, vars), do: enter_stmt(s, e, names, reads(e), vars) + + def enter_stmt(%Scope{} = s, e, names, reads, vars) do live = MapSet.union(pairs(vars, names), s.out) - nest = MapSet.union(live, pairs(vars, reads(e))) + nest = MapSet.union(live, pairs(vars, reads)) %{s | stmt: e, live: live, live_names: names, nest: nest} end diff --git a/test/copies_test.exs b/test/copies_test.exs index 1a32eb51..6e95fbca 100644 --- a/test/copies_test.exs +++ b/test/copies_test.exs @@ -407,4 +407,33 @@ defmodule Defk.CopiesTest do assert Copies.arm_reads(twice, node) == nil end end + + describe "block_liveness/1, each statement's reads walked once" do + property "a statement's reads are its stmt_reads, and the lives are live_after's" do + check all(es <- StreamData.list_of(stmt_gen(), min_length: 1, max_length: 5), max_runs: 300) do + for e <- es, do: assert(Copies.reads(e) == Copies.stmt_reads(e), Macro.to_string(e)) + + liveness = Copies.block_liveness(es) + assert Enum.map(liveness, &elem(&1, 0)) == Copies.live_after(es) + assert Enum.map(liveness, &elem(&1, 1)) == Enum.map(es, &Copies.reads/1) + end + end + + test "an array write reads its target, a match does not read its left side" do + [{_, write}, {_, match}] = + Copies.block_liveness( + quote( + do: + ( + a[i] = v + x = f(y) + ) + ) + |> elem(2) + ) + + assert write == MapSet.new([:a, :i, :v]) + assert match == MapSet.new([:y]) + end + end end