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
4 changes: 3 additions & 1 deletion docs/adr/0007-symbolic-bounds.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
27 changes: 15 additions & 12 deletions lib/defk/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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} =
Expand Down Expand Up @@ -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} ->
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))

[
Expand Down
18 changes: 14 additions & 4 deletions lib/defk/copies.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
29 changes: 29 additions & 0 deletions test/copies_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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