Rounding
Rounding generally replaces a value $x$ with an approximation $\hat{x}$, which is from a smaller set of representable values (e.g. with fewer decimal or binary places of accuracy). Binary rounding removes the information in the $n$ last bits by setting them to 0 (or 1). Several rounding modes exist, and BitInformation.jl implements them efficiently with bitwise operations, in-place or by creating a copy of the original array.
BitInformation.jl extends Base.bitstring
with a split option to better visualise sign, exponent and mantissa bits for floats.
julia> bitstring(1.1f0)
"00111111100011001100110011001101"
julia> bitstring(1.1f0,:split)
"0 01111111 00011001100110011001101"
Round to nearest
With binary round to nearest a full-precision number is replaced by the nearest representable float with fewer mantissa bits by rounding the trailing bits to zero. BitInformation.jl implements this by extending Julia's round
to round(::Array{T},n::Integer)
where T
either Float32
or Float64
and n
the number of significant bits retained after rounding. Negative n
are possible too, which will round even the exponent bits.
Rounding
julia> # bitwise representation (split in sign, exp, sig bits) of some random numbers
julia> bitstring.(A,:split)
5-element Array{String,1}:
"0 01111101 01001000111110101001000"
"0 01111110 01010000000101001110110"
"0 01111110 01011101110110001000110"
"0 01111101 00010101010111011100000"
"0 01111001 11110000000000000000101"
to n=3
significant bits via round(A,3)
yields
julia> bitstring.(round(A,3),:split)
5-element Array{String,1}:
"0 01111101 01000000000000000000000"
"0 01111110 01100000000000000000000" # round up, flipping the third significant bit
"0 01111110 01100000000000000000000" # same here
"0 01111101 00100000000000000000000" # and here
"0 01111010 00000000000000000000000" # note how the carry bits correctly carries into the exponent
This rounding function is IEEE compatible as it also implements tie-to-even, meaning that 01
which is exactly halway between 0
and 1
is round to 0
which is the even number (a bit sequence ending in a 0
is even). Similarly, 11
is round up to 100
and not down to 10
. Rounding to 1 signficant bit means that only 1,1.5,2,3,4,6...
are representable.
julia> A = Float32[1.25,1.5,1.75]
julia> bitstring.(A,:split)
3-element Vector{String}:
"0 01111111 01000000000000000000000"
"0 01111111 10000000000000000000000"
"0 01111111 11000000000000000000000"
julia> bitstring.(round(A,1),:split)
3-element Vector{String}:
"0 01111111 00000000000000000000000" # 1.25 is tie between 1.0 and 1.5, round down to even
"0 01111111 10000000000000000000000" # 1.5 is representable, no rounding
"0 10000000 00000000000000000000000" # 1.75 is tie between 1.5 and 2.0, round up to even
Bit shave
In contrast to round to nearest, shave
will always round to zero by shaving the trailing significant bits off (i.e. set them to zero). This rounding mode therefore introduces a bias towards 0 and the rounding error can be twice as large as for round to nearest.
julia> bitstring.(shave(A,3),:split)
5-element Array{String,1}:
"0 01111101 01000000000000000000000" # identical to round here
"0 01111110 01000000000000000000000" # round down here, whereas `round` would round up
"0 01111110 01000000000000000000000"
"0 01111101 00000000000000000000000"
"0 01111001 11100000000000000000000" # no carry bit for `shave`
Bit set
Similar to shave
, set_one
will always set the trailing significant bits to 1
. This rounding mode therefore introduces a bias away from 0 and the rounding error can be twice as large as for round to nearest.
julia> bitstring.(set_one(A,3),:split)
5-element Array{String,1}:
"0 01111101 01011111111111111111111" # all trailing bits are always 1
"0 01111110 01011111111111111111111"
"0 01111110 01011111111111111111111"
"0 01111101 00011111111111111111111"
"0 01111001 11111111111111111111111"
Bit groom
Combining shave
and set_one
, by alternating both removes the bias from both. This method is called grooming and is implemented via the groom
function
julia> bitstring.(groom(A,3),:split)
5-element Array{String,1}:
"0 01111101 01000000000000000000000" # shave
"0 01111110 01011111111111111111111" # set to one
"0 01111110 01000000000000000000000" # shave
"0 01111101 00011111111111111111111" # etc.
"0 01111001 11100000000000000000000"
Bit halfshave
Another way to remove the bias from shave
is to replace the trailing significant bits with 100...
which is equivalent to round to nearest, but uses representable values that are always halfway between. This also removes the bias of shave
or set_one
and yields on average a rounding error that is as large as from round to nearest
julia> bitstring.(halfshave(A,3),:split)
5-element Array{String,1}:
"0 01111101 01010000000000000000000" # set all discarded bits to 1000...
"0 01111110 01010000000000000000000"
"0 01111110 01010000000000000000000"
"0 01111101 00010000000000000000000"
"0 01111001 11110000000000000000000"