tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

float-layout-investigation.md (8466B)


Binary64 layout investigation — first screen

Status: investigation only. No public number encoding, arithmetic API, compiler, reducer, or published fixture has changed. These workers are bit-container experiments, not a floating-point implementation.

Contract and questions

Target binary64 semantics with pure Tree Calculus arithmetic. Keep internal working representation separate from public values and exact IEEE bit exchange. Preserve all 64 input bits in interchange, including signed zero and NaN payloads; NaN propagation during arithmetic is a separate policy still to specify.

First question: how much does accessing/unpacking existing binary64 data cost? Later questions: which significand layout and normalization policy minimize the cost of correctly rounded arithmetic, and whether public and working layouts should differ.

Reducer inspection

src/Runtime/Tree.hs uses lazy Haskell constructors and a small apply function. There are no native numeric primitives. A constructor can hold unevaluated branches. Traversing a bit spine costs reductions; a bounded natural used as a word does not acquire machine-word arithmetic. Sharing is inherited from host thunks, not a general memo table for equivalent applications.

Do not use bench/ApplyStats.hs's strict threaded evaluator as the timing oracle: its strict arguments/intermediates change demand relative to the reference reducer. Do not use CLI --repeat for independent work: src/Main.hs binds the applied term outside the repetition loop, allowing previous forcing to be reused.

--profile-full-result uses forceTreeShared to force the complete finite output while avoiding repeated visits to physically shared nodes. Profiling samples perform collections; therefore deltas include sampling/GC overhead. Allocation is cumulative allocated bytes, not live memory.

Candidates and method

bench/float-layouts.tri compares:

Sign is a Boolean tree in the fields representation. Exponent and fraction are raw IEEE fields, not yet a normalized arithmetic significand/exponent tuple.

Each candidate extracts signs, extracts exponents, and unpacks to the same fields tree. Inputs come from Python integer bit operations. The corpus includes positive/negative zeros, minimum/maximum subnormals, minimum normals, adjacent values around one, maximum finite values, infinities, signaling/quiet NaN bit patterns, and deterministic random words. Complete output trees are checked against independently constructed expectations on every invocation.

The generic variants intentionally remain as algorithm controls. The Fast variants use fixed-width bit splicing rather than natural multiplication by powers of two and numeric-counter extraction. They are candidates, not a claim of optimal code. Fixed splices canonicalize zero fields and zero-extend short natural encodings.

Runs use fresh processes, one Haskell capability, a 2 GiB heap limit, bounded process/whole-run time, randomized job order, and five repetitions. The first repetition is excluded from summaries; it is not a warmed evaluation thunk. Compilation/import are outside measurement. Timing runs from invocation start through complete output forcing, before rendering. Input construction not already forced by loading can contribute to this interval. The harness also checks sign projection with divergent unrelated fields.

The constant-sign consumer is a profiling/traversal control, not a value to subtract from every observation. Short timings are noisy, including cases where a simpler control is measured slower than a projection.

Initial results

Built the current Haskell host with nix build .# --no-link --print-out-paths. Exact runtime, compiler, source, harness and worker-bundle hashes, samples, and min/max/median values are retained in bench/float-layout-results.json.

Medians below are per batch, not per scalar operation. MB are decimal allocated megabytes, not peak residency. Full unpack uses the fixed-splice variants for packed/words/bytes.

Workload 128 values ms / MB 512 values ms / MB
Fields sign 1.00 / 1.02 2.16 / 3.95
Packed sign 3.42 / 12.37 10.21 / 49.39
Words sign 2.38 / 6.75 6.55 / 26.87
Bytes sign 2.04 / 3.66 4.65 / 14.49
Fields unpack 4.47 / 6.30 15.69 / 28.09
Packed unpack 22.93 / 125.10 90.87 / 501.68
Words unpack 25.58 / 111.87 80.78 / 449.09
Bytes unpack 23.49 / 100.13 80.58 / 402.09

At 128 values, generic byte unpack took 646.78 ms and allocated 4751.82 MB, versus 23.49 ms / 100.13 MB with direct bit splicing. This is primarily an algorithm effect, not evidence that bytes themselves are unusable.

All 190 timed invocations across both runs matched their complete expectations; the four additional divergent-field demand checks passed. That is 60,800 per-value workload checks, not 60,800 distinct input words or IEEE arithmetic tests. Python syntax compilation and whitespace checks also passed.

What these results establish

What they do not establish

Next investigation stage

The first significand results are now available in the shift/rounding/normalization investigation. They cover LSB/MSB direction, width-cache costs, and rounded chains; blocked layouts and complete arithmetic pipelines remain open.

  1. Compare LSB-first significands, MSB-first significands, and blocked variants on equivalent operations, including the cost of maintaining their invariants.
  2. Compare a normalized 53-bit significand against odd-significand storage; compare cached bit lengths with recomputation. Include subnormals, whose leading-bit behavior differs from normal numbers.
  3. Implement and independently check right-shift-with-sticky, normalization, round-to-nearest/ties-to-even, aligned addition/subtraction, and 53-by-53-bit products. Test cancellation, carry growth, ties, and exponent gaps beyond the significand width. No intermediate shift should scale blindly with an arbitrary exponent gap.
  4. Measure full output, chained consumers, sign/exponent-only demand, allocation, and retained-memory behavior separately. Include packing/unpacking amortized across chains rather than charging it to every operation by assumption.
  5. Add correctly rounded division and exact bit-level differential tests before selecting a public encoding. Float arithmetic needs a software reference with explicit rounding semantics, not merely the host's decimal formatting.

Do not freeze the public representation from this first screen.

Reproduce

From the repository root (use fresh output directories):

HOST=$(nix build .# --no-link --print-out-paths)/bin/tricu
python3 test/float_layouts.py "$HOST" \
  --directory /tmp/float-layouts-128 --size 128 --repeats 5
python3 test/float_layouts.py "$HOST" \
  --directory /tmp/float-layouts-512 --size 512 --repeats 5

The harness explicitly selects the repository's shipped stable compiler, stages isolated sources, and retains source snapshots, compiled bundle, input/output trees, command logs, samples and a report in the output directory. A failed run leaves complete: false; failure is not silently excluded from successful measurements. Python assertions follow the existing probe harness convention; run without python -O.