SuffixConversion.jl

SuffixConversion
Author simonbyrne
Popularity
9 Stars
Updated Last
6 Months Ago
Started In
July 2023

SuffixConversion.jl

Build Status

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.

Why?

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

How does it work?

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)

What are the performance impacts?

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
}

What about BigFloats?

BigFloats 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

Alternatives

Manual conversion

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.

ChangePrecision.jl

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.