SuffixConversion.jl makes it easier to write type-generic code supporting different floating point types.
The principle interface is the @suffix <typename>
macro which defines a special variable of the same name prefixed with an underscore (_
). When pre-multiplied by a number, it will convert the number to the specified type:
julia> using SuffixConversion
julia> @suffix Float64
SuffixConversion.SuffixConverter{Float64}()
julia> @suffix Float32
SuffixConversion.SuffixConverter{Float32}()
julia> 0.2_Float32
0.2f0
This takes advantage of Julia's implicit multiplication of numerical literal coefficients.
One of the benefits of Julia is that you can write generic code: a single method definition will work efficiently for multiple datatypes, by generating specialized code for each type signature. One difficulty is working with numeric literals: by default, a literal with a decimal point (e.g. 7.3
) is treated as a Float64
, which may cause unexpected promotion:
julia> addhalf_naive(x) = x + 0.5
addhalf_naive (generic function with 1 method)
julia> addhalf_naive(Float32(1)) # returns a Float64
1.5
The intended way to use this package is to use @suffix
inside your function definition to define the corresponding suffix variable. Typically the type will be either determined by a parameter in the type signature:
function addhalf(x::FT) where {FT}
@suffix FT
return x + 0.5_FT
end
or it can also be computed as part of the expression:
function addhalf(x)
@suffix FT = typeof(x)
return x + 0.5_FT
end
Another common cause of unexpected type promotion are integer values: while arithmetic operations which combine floating point and integer values will be converted to the floating point type:
julia> 2 * 1.2f0 # returns a Float32
2.4f0
some intermediate integer-only operations such as division (/
) or square root (sqrt
) can be converted to a Float64
, which may result in an unexpectd conversion:
julia> 1/2 * 1.2f0 # returns a Float64
0.6000000238418579
This can be addressed by appending the suffix to the integer literals
function mulhalf(x::FT) where {FT}
@suffix FT
return 1_FT/2_FT * x
end
The macro
@suffix FT
expands to
_FT = SuffixConversion.SuffixConverter{FT}()
and the SuffixConverter
type defines methods for pre-multiplication by a Number
Base.:*(x::Number, ::SuffixConverter{FT}) where {FT} = convert(FT, x)
which performs the actual conversion.
It also relies on the fact that implicit multiplication has higher precedence than regular multiplication, so
x * 0.2_FT
will parse as
x * (0.2 * _FT)
For most floating point types (other than BigFloat
, see below) this should generally work with no runtime overhead, as the Julia compiler is able to determine that the conversion is pure (i.e. has no side effects), and so constant fold the conversion at compile time.
For example, the addhalf
function defined above is able to convert this to a single Float32
multiply:
julia> @code_llvm addhalf(1f0)
; @ REPL[2]:1 within `addhalf`
define float @julia_addhalf_115(float %0) #0 {
top:
; @ REPL[2]:3 within `addhalf`
; ┌ @ float.jl:408 within `+`
%1 = fadd float %0, 5.000000e-01
; └
ret float %1
}
BigFloat
s are handled specially by first converting to a decimal string representation, then converting back. This allows things like
julia> @suffix BigFloat
SuffixConversion.SuffixConverter{BigFloat}()
julia> 0.2_BigFloat
0.2000000000000000000000000000000000000000000000000000000000000000000000000000004
whereas regular conversion will give the Float64
value in BigFloat
precision
julia> BigFloat(0.2)
0.200000000000000011102230246251565404236316680908203125
Note that the literal still goes through the Julia parser, which first converts literals to Float64
, so this may not work as intended if there are more than 15 significant figures:
julia> 0.1000000000000000000007_BigFloat
0.1000000000000000000000000000000000000000000000000000000000000000000000000000002
julia> big"0.1000000000000000000007"
0.1000000000000000000007000000000000000000000000000000000000000000000000000000003
The typical alternative is to manually convert everything to literals
addhalf(x::FT) where {FT} = x + FT(0.5)
This is mostly equivalent to our approach (other than the BigFloat
handling), however it does require more parentheses, which can get confusing with larger expressions.
Another package which tries to address this problem is ChangePrecision.jl: it defines a macro @changeprecision
which performs conversion syntactically. It includes the disclaimer "This package is for quick experiments, not production code", and appears to be intended for use at the top-level, rather than inside function definitions.