Skip to content

Commit 200515f

Browse files
committed
Finish removing the BigInts from * for FD{Int128}!
Finally implements the fast-multiplication optimization from #45, but this time for 128-bit FixedDecimals! :) This is a follow-up to #93, which introduces an Int256 type for widemul. However, the fldmod still required 2 BigInt allocations. Now, this PR uses a custom implementation of the LLVM div-by-const optimization for (U)Int256, which briefly widens to Int512 (😅) to perform the fldmod by the constant 10^f coefficient. This brings 128-bit FD multiply to the same performance as 64-bit. :)
1 parent a245651 commit 200515f

File tree

3 files changed

+73
-14
lines changed

3 files changed

+73
-14
lines changed

src/FixedPointDecimals.jl

+11-13
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ export checked_abs, checked_add, checked_cld, checked_div, checked_fld,
3636

3737
using Base: decompose, BitInteger
3838

39-
import BitIntegers # For 128-bit _widemul / _widen
39+
using BitIntegers: BitIntegers, UInt256, Int256
4040
import Parsers
4141

42+
include("fldmod-by-const.jl")
43+
4244
# floats that support fma and are roughly IEEE-like
4345
const FMAFloat = Union{Float16, Float32, Float64, BigFloat}
4446

@@ -129,8 +131,10 @@ _widemul(x::Unsigned,y::Signed) = signed(_widen(x)) * _widen(y)
129131

130132
# Custom widen implementation to avoid the cost of widening to BigInt.
131133
# FD{Int128} operations should widen to 256 bits internally, rather than to a BigInt.
132-
_widen(::Type{Int128}) = BitIntegers.Int256
133-
_widen(::Type{UInt128}) = BitIntegers.UInt256
134+
_widen(::Type{Int128}) = Int256
135+
_widen(::Type{UInt128}) = UInt256
136+
_widen(::Type{Int256}) = BitIntegers.Int512
137+
_widen(::Type{UInt256}) = BitIntegers.UInt512
134138
_widen(t::Type) = widen(t)
135139
_widen(x::T) where {T} = (_widen(T))(x)
136140

@@ -196,18 +200,12 @@ function _round_to_nearest(quotient::T,
196200
end
197201
_round_to_nearest(q, r, d, m=RoundNearest) = _round_to_nearest(promote(q, r, d)..., m)
198202

199-
# In many of our calls to fldmod, `y` is a constant (the coefficient, 10^f). However, since
200-
# `fldmod` is sometimes not being inlined, that constant information is not available to the
201-
# optimizer. We need an inlined version of fldmod so that the compiler can replace expensive
202-
# divide-by-power-of-ten instructions with the cheaper multiply-by-inverse-coefficient.
203-
@inline fldmodinline(x,y) = (fld(x,y), mod(x,y))
204-
205203
# multiplication rounds to nearest even representation
206204
# TODO: can we use floating point to speed this up? after we build a
207205
# correctness test suite.
208206
function Base.:*(x::FD{T, f}, y::FD{T, f}) where {T, f}
209207
powt = coefficient(FD{T, f})
210-
quotient, remainder = fldmodinline(_widemul(x.i, y.i), powt)
208+
quotient, remainder = fldmod_by_const(_widemul(x.i, y.i), Val(powt))
211209
reinterpret(FD{T, f}, _round_to_nearest(quotient, remainder, powt))
212210
end
213211

@@ -234,12 +232,12 @@ function Base.round(x::FD{T, f},
234232
RoundingMode{:NearestTiesUp},
235233
RoundingMode{:NearestTiesAway}}=RoundNearest) where {T, f}
236234
powt = coefficient(FD{T, f})
237-
quotient, remainder = fldmodinline(x.i, powt)
235+
quotient, remainder = fldmod_by_const(x.i, Val(powt))
238236
FD{T, f}(_round_to_nearest(quotient, remainder, powt, m))
239237
end
240238
function Base.ceil(x::FD{T, f}) where {T, f}
241239
powt = coefficient(FD{T, f})
242-
quotient, remainder = fldmodinline(x.i, powt)
240+
quotient, remainder = fldmod_by_const(x.i, Val(powt))
243241
if remainder > 0
244242
FD{T, f}(quotient + one(quotient))
245243
else
@@ -435,7 +433,7 @@ function Base.checked_sub(x::T, y::T) where {T<:FD}
435433
end
436434
function Base.checked_mul(x::FD{T,f}, y::FD{T,f}) where {T<:Integer,f}
437435
powt = coefficient(FD{T, f})
438-
quotient, remainder = fldmodinline(_widemul(x.i, y.i), powt)
436+
quotient, remainder = fldmod_by_const(_widemul(x.i, y.i), Val(powt))
439437
v = _round_to_nearest(quotient, remainder, powt)
440438
typemin(T) <= v <= typemax(T) || Base.Checked.throw_overflowerr_binaryop(:*, x, y)
441439
return reinterpret(FD{T, f}, T(v))

test/fldmod-by-const_tests.jl

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
@testset "calculate_inverse_coeff signed" begin
3+
using FixedPointDecimals: calculate_inverse_coeff
4+
5+
# The correct magic number here comes from investigating the following native code
6+
# produced on an m2 aarch64 macbook pro:
7+
# @code_native ((x)->fld(x,100))(2)
8+
# ...
9+
# mov x8, #55051
10+
# movk x8, #28835, lsl #16
11+
# movk x8, #2621, lsl #32
12+
# movk x8, #41943, lsl #48
13+
# Where:
14+
# julia> 55051 | 28835 << 16 | 2621 << 32 | 41943 << 48
15+
# -6640827866535438581
16+
@test calculate_inverse_coeff(Int64, 100) == (-6640827866535438581, 6)
17+
18+
# Same for the tests below:
19+
20+
# (LLVM's magic number is shifted one bit less, then they shift by 2, instead of 3,
21+
# but the result is the same.)
22+
@test calculate_inverse_coeff(Int64, 10) == (7378697629483820647 << 1, 3)
23+
24+
@test calculate_inverse_coeff(Int64, 1) == (1, 0)
25+
end
26+
27+
@testset "calculate_inverse_coeff signed 4" begin
28+
using FixedPointDecimals: calculate_inverse_coeff
29+
30+
# Same here, our magic number is shifted 2 bits more than LLVM's
31+
@test calculate_inverse_coeff(UInt64, 100) == (0xa3d70a3d70a3d70b, 6)
32+
33+
@test calculate_inverse_coeff(UInt64, 1) == (UInt64(0x1), 0)
34+
end
35+
36+
@testset "div_by_const" begin
37+
vals = [2432, 100, 0x1, Int32(10000), typemax(Int64), typemax(Int16), 8, Int64(2)^32]
38+
for a_base in vals
39+
# Only test negative numbers on `a`, since div_by_const requires b > 0.
40+
@testset for (a, b, f) in Iterators.product((a_base, -a_base), vals, (unsigned, signed))
41+
a, b = promote(f(a), f(b))
42+
@test FixedPointDecimals.div_by_const(a, Val(b)) == a ÷ b
43+
end
44+
end
45+
end
46+
47+
@testset "fldmod_by_const" begin
48+
vals = [2432, 100, 0x1, Int32(10000), typemax(Int64), typemax(Int16), 8, Int64(2)^32]
49+
for a_base in vals
50+
# Only test negative numbers on `a`, since fldmod_by_const requires b > 0.
51+
@testset for (a, b, f) in Iterators.product((a_base, -a_base), vals, (unsigned, signed))
52+
a, b = promote(f(a), f(b))
53+
@test FixedPointDecimals.fldmod_by_const(a, Val(b)) == fldmod(a, b)
54+
end
55+
end
56+
end

test/runtests.jl

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@ include(joinpath(pkg_path, "test", "utils.jl"))
1010

1111
@testset "FixedPointDecimals" begin
1212
include("FixedDecimal.jl")
13-
end # global testset
13+
end
14+
15+
@testset "FixedPointDecimals" begin
16+
include("fldmod-by-const_tests.jl")
17+
end
18+

0 commit comments

Comments
 (0)