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:
- fields:
Fork sign (Fork biasedExponent rawFraction); - packed: one canonical natural holding the 64-bit IEEE pattern;
- words:
Fork low32 high32, both canonical naturals; - bytes: eight canonical octets in a little-endian proper list.
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
- Direct fields avoid substantial extraction work on this reducer.
- Splitting packed storage changes access costs; all compact layouts should not be treated as equivalent.
- Fixed-width tree operations materially outperform generic natural arithmetic for these particular bit-manipulation tasks.
What they do not establish
- An overall winning float representation. Field unpack is effectively identity, and the output format is deliberately fields: this screen measures the cost of reaching that working representation, not an unbiased arithmetic competition.
- Relative arithmetic speed, packed storage footprint, retained heap size, or round-trip/formatting costs.
- Correct rounding, arithmetic NaN policy, or binary64 conformance.
- Small timing differences between the optimized packed/word/byte variants are stable enough to choose among them.
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.
- Compare LSB-first significands, MSB-first significands, and blocked variants on equivalent operations, including the cost of maintaining their invariants.
- 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.
- 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.
- 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.
- 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.