From 81ddf6708495ca50f12bd209125088aeb2695259 Mon Sep 17 00:00:00 2001 From: Eric Barkie Date: Tue, 20 Oct 2026 15:32:01 -0400 Subject: [PATCH] Keep float remainders and float keys of whole numbers in the loop Code written before Lua had integers takes bits with / and %, and in Lua 5.4 that arithmetic is in floats: a bit buffer is a float, bitbuf % p a float remainder, and bitbuf % size + 1 a float key. The loop did neither. A float remainder went to the driver and math.Mod, which takes the quotient's bits one at a time, so a remainder of a 2^40 bit buffer cost tens of iterations of frexp and ldexp; a float key went to the driver to be normalised. A DEFLATE decoder written that way ran 3 times slower than in C Lua, at 141 ms a MiB to 45. A remainder of whole numbers below 2^53 is exact as integers: the loop computes it so, with the sign a zero keeps from the dividend, as fmod gives it; floatMod does the same for the driver, and leaves math.Mod for the rest. A float key with a whole value takes the array and hash paths of GETTABLE and SETTABLE as the integer it normalises to. The decoder goes to 54 ms a MiB; the suite, which has neither, is unchanged. Two integers with a divisor below 1 stay the driver's, as their result is an integer. The loop grows by what the merged store cases freed. The gate's baseline moves by reloads on the exit paths of GETTABLE and of JMP's tick: the back edge itself touches no stack slot. --- arith.go | 9 +++++ bench/asmgate/baseline.txt | 4 +-- checked_gen.go | 37 ++++++++++++++++++++ exec_test.go | 69 ++++++++++++++++++++++++++++++++++++++ fast.go | 50 +++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 2 deletions(-) diff --git a/arith.go b/arith.go index fc444cd..3e16e0d 100644 --- a/arith.go +++ b/arith.go @@ -113,6 +113,15 @@ func floatMod(x, y float64) float64 { if y == 0 || math.IsInf(x, 0) || x != x || y != y { return (x * y) / (x * y) } + // whole numbers below 2^53, as code written before Lua had integers takes bits: exact + // as integers, where math.Mod loops over the bits of the quotient + if a, b := int64(x), int64(y); float64(a) == x && float64(b) == y && x < 0x1p53 && x > -0x1p53 && y < 0x1p53 && y > -0x1p53 { + m := intMod(a, b) + if m == 0 { + return math.Copysign(0, x) // as fmod: a zero remainder keeps the dividend's sign + } + return float64(m) + } m := math.Mod(x, y) if (m > 0 && y < 0) || (m < 0 && y != m && y > 0) { m += y diff --git a/bench/asmgate/baseline.txt b/bench/asmgate/baseline.txt index 0d4a421..6003ce6 100644 --- a/bench/asmgate/baseline.txt +++ b/bench/asmgate/baseline.txt @@ -1,8 +1,8 @@ # Stack accesses per hot opcode in (*executor).fast, outside write-barrier slow paths. # Opcodes not listed must have none. Written by go run ./bench/asmgate -update. amd64-v1 head 1 -amd64-v1 opGetField 7 -amd64-v1 opGetTable,opGetIndex 2 +amd64-v1 opGetField 6 +amd64-v1 opGetTable,opGetIndex 3 amd64-v1 opJmp 3 amd64-v1 opSelf 1 amd64-v3 head 2 diff --git a/checked_gen.go b/checked_gen.go index 03b54e0..21b1580 100644 --- a/checked_gen.go +++ b/checked_gen.go @@ -410,6 +410,14 @@ dispatch: idx = uint64(uint8(i>>24)) - 1 } else if key := ®s[uint8(i>>24)]; key.p == tInt() { idx = key.n - 1 + } else if key.p == tFloat() { + // a float with a whole value, as arithmetic in code written for Lua 5.1 makes + // indexes: the integer key it normalises to + var whole bool + if idx, whole = wholeIndex(key.n); !whole { + e = exSlowOp + break dispatch + } } else if key.n>>56 == kindString && !isScalar(key.p) && key.n&lenMask <= 16 && obj.n == kindTable<<56 && !isScalar(obj.p) { // a short string key: hash it and probe inline, as hashString and keyEq would t, n := (*Table)(obj.p), key.n&lenMask @@ -507,6 +515,12 @@ dispatch: idx = uint64(uint8(i>>16)) - 1 } else if key := ®s[uint8(i>>16)]; key.p == tInt() { idx = key.n - 1 + } else if key.p == tFloat() { + var whole bool + if idx, whole = wholeIndex(key.n); !whole { + e = exSlowOp + break dispatch + } } else { e = exSlowOp break dispatch @@ -582,6 +596,29 @@ dispatch: } else { setInt(®s[uint8(i>>8)], uint64(q)) } + } else if (uint8(i) == opMod || uint8(i) == opModK) && (x.p == tFloat() || y.p == tFloat()) && isNum(x) && isNum(y) { + // a float remainder of whole numbers below 2^53, as code written before Lua had + // integers takes bits: exact as integers, where math.Mod loops over the quotient's + // bits. Two integers with a divisor below 1 are the driver's: the result is an integer + fx, fy := toFloat(x), toFloat(y) + a, b := int64(fx), int64(fy) + if float64(a) != fx || float64(b) != fy || b == 0 || fx >= 0x1p53 || fx <= -0x1p53 || fy >= 0x1p53 || fy <= -0x1p53 { + // not whole, too large, NaN, infinite, or a zero divisor. Spelt out: wholeInt + // here spills the loop + e = exSlowOp + break dispatch + } + m := a % b + if m != 0 && (m^b) < 0 { // round towards minus infinity + m += b + } + r := float64(m) + if m == 0 { + // as fmod: a zero remainder keeps the dividend's sign, which is bit 63 of an + // integer and of a float alike + r = math.Float64frombits(x.n & (1 << 63)) + } + setFloat(®s[uint8(i>>8)], r) } else { e = exSlowOp break dispatch diff --git a/exec_test.go b/exec_test.go index a460a6f..ebf59bf 100644 --- a/exec_test.go +++ b/exec_test.go @@ -1020,3 +1020,72 @@ func TestWrapFailureKeepsNothing(t *testing.T) { } release(&rt.pool, ex) } + +// The remainder of floats with whole values, as code written for Lua 5.1 takes bits, is +// computed as integers, inline and in the driver: the same as fmod gives, sign of a zero +// included; values that are not whole, or too large, still go the long way. +func TestFloatModWhole(t *testing.T) { + src := ` + local function same(a, b) return a == b and (a ~= 0 or 1 / a == 1 / b) end + local K = 3.0 + local cases = { + { 7.0, 3.0, 1.0 }, { -7.0, 3.0, 2.0 }, { 7.0, -3.0, -2.0 }, { -7.0, -3.0, -1.0 }, + { 6.0, 3.0, 0.0 }, { -6.0, 3.0, -0.0 }, { 6.0, -3.0, 0.0 }, { -0.0, 3.0, -0.0 }, { 0.0, -3.0, 0.0 }, + { 7, 3.0, 1.0 }, { 7.0, 3, 1.0 }, { -6, 3.0, -0.0 }, { -5, -1.0, -0.0 }, { 6, -3.0, 0.0 }, { -7, 3.0, 2.0 }, + { 5.5, 2.0, 1.5 }, { 7.0, 2.5, 2.0 }, { 2^53, 3.0, 2.0 }, { 2^60, 7.0, 1.0 }, { 16777216.0 * 65536, 256.0, 0.0 }, + { 1e300, 7.0, 1.0 }, -- the last three go the long way, with the values C Lua gives + } + for i, c in ipairs(cases) do + local x, y, want = c[1], c[2], c[3] + local got = x % y + assert(same(got, want), i .. ": " .. tostring(x) .. " % " .. tostring(y) .. " = " .. tostring(got)) + assert(math.type(got) == "float", i) + end + local x = 7.0 + assert(same(x % 3.0, 1.0) and same(-x % 3.0, 2.0) and same((x - 13) % 3.0, -0.0)) -- the constant form, MODK + assert(same(K % 3, 0.0) and same(-K % 3, -0.0)) -- a float register with an integer constant + local nan = 7.0 % 0.0 + assert(nan ~= nan) + local bitbuf, room = 0, 1 -- a bit buffer as inflate keeps one, in floats + for i = 1, 20 do bitbuf, room = bitbuf + (i % 256) * room, room * 256 end + local v = 0 + for _ = 1, 50 do local p = 2 ^ 5; local r = bitbuf % p; v = v * 2 + r; bitbuf = (bitbuf - r) / p end + return v` + for _, checked := range []bool{true, false} { + res, err := runInEnv(t, MustNew(withLoop(checked)), src) + if err != nil || len(res) != 1 { + t.Fatalf("checked=%v: %v %v", checked, res, err) + } + if f, _ := res[0].Float(); f != 5181448545894400 { // as C Lua computes it + t.Errorf("checked=%v: bit buffer gave %v", checked, res[0]) + } + } +} + +// A float key with a whole value finds the integer key it normalises to, inline, in the +// array part and in the hash part; one without, or past the integers, goes the long way. +func TestFloatKeysInline(t *testing.T) { + src := ` + local t = { 10, 20, 30, [0] = 5, [-1] = -5, [100] = 1000, [0.5] = "half", [2^53] = "big", [2^63] = "huge" } + assert(t[1.0] == 10 and t[3.0] == 30 and t[0.0] == 5 and t[-0.0] == 5 and t[-1.0] == -5 and t[100.0] == 1000) + assert(t[0.5] == "half" and t[2^53] == "big" and t[2^63] == "huge" and t[4.0] == nil and t[1e300] == nil) + local nan = 0 / 0 + assert(t[nan] == nil) + t[2.0] = 22 + t[0.0] = 55 + t[100.0] = 1001 + t[4.0] = 40 -- an append, as t[4] is + assert(t[2] == 22 and t[0] == 55 and t[100] == 1001 and t[4] == 40 and #t == 4) + local w = {} + for k = 1, 10 do w[k + 0.0] = k * k end + local s = 0 + for k = 1, 10 do s = s + w[k] end + assert(s == 385 and #w == 10) + return "ok"` + for _, checked := range []bool{true, false} { + res, err := runInEnv(t, MustNew(withLoop(checked)), src) + if err != nil || len(res) != 1 || res[0].String() != "ok" { + t.Errorf("checked=%v: %v %v", checked, res, err) + } + } +} diff --git a/fast.go b/fast.go index 6a9e719..b236c55 100644 --- a/fast.go +++ b/fast.go @@ -44,6 +44,19 @@ func strEqBytes(x, y *Value, n uintptr) bool { return true } +// wholeIndex returns the array index of a float key, from its bits, when its value is whole +// and an integer: the key it normalises to, less one, as normKey gives it. The range is +// tested on the float, as arm64 saturates int64(f) where amd64 gives the minimum. Small +// enough to inline into fast; split in two, it spills the loop. +func wholeIndex(bits uint64) (uint64, bool) { + f := math.Float64frombits(bits) + n := int64(f) + if float64(n) != f || f >= 0x1p63 || f < -0x1p63 { + return 0, false + } + return uint64(n) - 1, true +} + // isNum reports whether v is an integer or a float. Small enough to inline into fast. func isNum(v *Value) bool { return v.p == tInt() || v.p == tFloat() } @@ -508,6 +521,14 @@ dispatch: idx = uint64(uint8(i>>24)) - 1 } else if key := ®s[uint8(i>>24)]; key.p == tInt() { idx = key.n - 1 + } else if key.p == tFloat() { + // a float with a whole value, as arithmetic in code written for Lua 5.1 makes + // indexes: the integer key it normalises to + var whole bool + if idx, whole = wholeIndex(key.n); !whole { + e = exSlowOp + break dispatch + } } else if key.n>>56 == kindString && !isScalar(key.p) && key.n&lenMask <= 16 && obj.n == kindTable<<56 && !isScalar(obj.p) { // a short string key: hash it and probe inline, as hashString and keyEq would t, n := (*Table)(obj.p), key.n&lenMask @@ -605,6 +626,12 @@ dispatch: idx = uint64(uint8(i>>16)) - 1 } else if key := ®s[uint8(i>>16)]; key.p == tInt() { idx = key.n - 1 + } else if key.p == tFloat() { + var whole bool + if idx, whole = wholeIndex(key.n); !whole { + e = exSlowOp + break dispatch + } } else { e = exSlowOp break dispatch @@ -680,6 +707,29 @@ dispatch: } else { setInt(®s[uint8(i>>8)], uint64(q)) } + } else if (uint8(i) == opMod || uint8(i) == opModK) && (x.p == tFloat() || y.p == tFloat()) && isNum(x) && isNum(y) { + // a float remainder of whole numbers below 2^53, as code written before Lua had + // integers takes bits: exact as integers, where math.Mod loops over the quotient's + // bits. Two integers with a divisor below 1 are the driver's: the result is an integer + fx, fy := toFloat(x), toFloat(y) + a, b := int64(fx), int64(fy) + if float64(a) != fx || float64(b) != fy || b == 0 || fx >= 0x1p53 || fx <= -0x1p53 || fy >= 0x1p53 || fy <= -0x1p53 { + // not whole, too large, NaN, infinite, or a zero divisor. Spelt out: wholeInt + // here spills the loop + e = exSlowOp + break dispatch + } + m := a % b + if m != 0 && (m^b) < 0 { // round towards minus infinity + m += b + } + r := float64(m) + if m == 0 { + // as fmod: a zero remainder keeps the dividend's sign, which is bit 63 of an + // integer and of a float alike + r = math.Float64frombits(x.n & (1 << 63)) + } + setFloat(®s[uint8(i>>8)], r) } else { e = exSlowOp break dispatch