Skip to content

Native U64 and I64: the 64-bit word as a machine number, with its laws - #931

Open
phenomenon0 wants to merge 1 commit into
bendlang:mainfrom
phenomenon0:u64-i64-native
Open

phenomenon0 wants to merge 1 commit into
bendlang:mainfrom
phenomenon0:u64-i64-native

Conversation

@phenomenon0

@phenomenon0 phenomenon0 commented Sep 21, 2026

Copy link
Copy Markdown

After the strings review asked for smaller steps, here is one: the 64-bit word as a machine number, with its laws.

What changed

  • base.bend: U64 (the Word(64n) slot F64 uses, with the u64_ op family) and its signed twin I64 (two's complement; only four ops read the sign — the ordered compare, neg, the arithmetic shift, is_neg). Both carry add_comm, filled by the same width-generic Word.add_comm proof U32 cites — the signed filler is the unsigned one, repacked.
  • comp.ts: WORDS rows for the two types; the u64_/i64_ templates; the match-scrutinee word detection generalized to W64 words (Nat keeps its own path); word_to_u64/u64_to_word added to the JS runtime — the 32-bit pair's 64-bit siblings.
  • tests/base/u64_ops.bend (15 checks) and tests/base/i64_ops.bend (20 checks): four lanes identical — check, interpret, JS, C — including structural-construction cross-checks (a conversion that packed only 32 bits answers 0 for hi32 and fails checks 13 and 15).

Why

A Word(64n) used to be a 64-cell structure. It stays exactly as it is — this is additive: U64/I64 are new typed views over the same Word(64n) slot, and datatypes outside WORDS compile byte-identically (the bench keeps a structural control, S64/T64, precisely so that old path stays exercised). Measured, per 10M operations on the C lane:

  • U64 increment: 2,085 ms → 0.62 ms — the empty-main floor; a word operation costs what an empty program costs.
  • I64 (signed test + increment): 14,420 ms → 4.88 ms; JS 157×, interpret 121×.
  • Zero I64 nodes remain in the emitted C; the structural 64-cell node is gone from both lanes.

Against native C

The hand-written C baseline for the same loops (clang -O3, volatile-fenced counter so nothing folds away): 2.49 ms per 10M increments. The native Bend op measures 0.62 ms against a 0.60 ms empty program — one machine +, the same instruction C would emit. The floor is a fold, and the fold is real: 1,000,000,000 increments run in 0.57 ms and print the right answer (1000000000). The signed bench is the stricter measurement — its loop does not fold (4.88 ms against a 0.70 ms floor is measured work) — and net of the floor it is 3,450× the old lowering on C, 165× on JS, 147× on interpret.

Size

comp.ts +82/−8; base.bend +270 (mostly the two documented blocks); cap re-measured for this tree: comp.ts 65,000 → 66,047 ttok. Gate: PASS 46/46.

The signed half is small by construction — two's complement makes add/sub/mul/the bitwise ops bit-identical to the unsigned ones, so they share lowering; only what reads the sign is new.

Measurement reports (longer, with the lane methodology and the bench programs — bench/nbench*.bend, bench/nbench.c — kept out of this PR to stay lean) live on our fork: github.com/phenomenon0/benddocs/omen/lanes/native-u64.md, native-i64.md.

Happy to split, reshape, or cut anything that fights the budget.

- base.bend: the U64 block (Word(64n) slot, u64_ family) and the signed
  twin I64 (two's complement; four ops read the sign), both with add_comm
  citing the width-generic Word.add_comm
- comp.ts: WORDS rows, u64_/i64_ templates, W64 match-scrutinee detection,
  word_to_u64/u64_to_word in the JS runtime
- tests/base: u64_ops (15 checks) + i64_ops (20 checks), four lanes identical
- measured: 2,085 -> 0.62 ms and 14,420 -> 4.88 ms per 10M on C
- cap measured for this tree: comp.ts 65000 -> 66047 ttok
@victormeloasm

Copy link
Copy Markdown
Contributor

This would make the entire language and libs to be rewritten. Mine Math would need to adapt. I will do it if it was merged.

@phenomenon0

Copy link
Copy Markdown
Author

Thanks for looking at this, Víctor — one thing that makes it easier: it's additive, not a break. Word(64n) stays exactly as it is; U64/I64 are new typed views over it, and nothing outside those two types changes codegen (we keep a structural control in the bench precisely so the old path stays exercised). So no library is forced to adapt — but for something like bend-hashes, moving the hot words onto U64 should be mostly upside: the ops become one machine instruction. If you'd like, I'm happy to help try a piece of it on a fork before this lands, so the adaptation is a bench, not a leap.

@phenomenon0

Copy link
Copy Markdown
Author

One more data point while this sits in review: both words lower to the device program. A 2²⁰-leaf fork tree over U64.inc/U64.add prints 1048576 on Metal (M3 Ultra) and CUDA (RTX 3090), identical to --gpu off; the signed twin's tree — negation, I64.is_lt, adds — prints the same sign and sum on both. Happy to add these as device smoke tests here or in a follow-up (~30 lines each).

@victormeloasm

Copy link
Copy Markdown
Contributor

Ok, I will adapt it in less than 2 hours if it was merged and my math too.

@victormeloasm

Copy link
Copy Markdown
Contributor

My math library is the basic one, it will not use GPU. It was a port of math.h from the C23 lib it runs even in a i386.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants