Swift currently has some significant performance shortcomings when dealing with conversions between integers and floating point types in generic contexts. As an example, I've been looking at optimising the following method:
@inlinable
func floatToUnormGenericA<I: BinaryInteger & FixedWidthInteger & UnsignedInteger>(_ c: Float, type: I.Type) -> I {
if c.isNaN {
return 0
}
let c = min(1.0, max(c, 0.0))
let scale = Float(I.max)
let rescaled = c * scale
return I(rescaled.rounded(.toNearestOrAwayFromZero))
}
public func convertA(_ x: Float) -> UInt8 {
return floatToUnormGenericA(x, type: UInt8.self)
}
The behaviour I want is for the generated assembly to be equivalent to the following, where the type I
is made concrete:
@inlinable
func floatToUnormConcrete(_ c: Float, type: UInt8.Type) -> UInt8 {
if c.isNaN {
return 0
}
let c = min(1.0, max(c, 0.0))
let scale = Float(UInt8.max)
let rescaled = c * scale
return UInt8(rescaled.rounded(.toNearestOrAwayFromZero))
}
public func convertConcrete(_ x: Float) -> UInt8 {
return floatToUnormConcrete(x, type: UInt8.self)
}
Even with all optimisations enabled and full inlining, Swift is not able to (and not allowed to) specialise convertA
to be equivalent to convertB
due to the fact that in the generic context, methods such as I(_: Float)
are interpreted as calls to FixedWidthInteger.init<T: BinaryFloatingPoint>(_ source: T)
rather than the concrete equivalents (e.g. UInt8.init(_ source: Float)
). What this means is that currently, to get good performance, we're required to manually specialise like this:
@inlinable
func floatToUnormGenericB<I: BinaryInteger & FixedWidthInteger & UnsignedInteger>(_ c: Float, type: I.Type) -> I {
if c.isNaN {
return 0
}
let c = min(1.0, max(c, 0.0))
let scale: Float
if I.self == UInt8.self {
scale = Float(UInt8.max)
} else if I.self == UInt16.self {
scale = Float(UInt16.max)
} else {
scale = Float(I.max)
}
let rescaled = c * scale
let rounded = rescaled.rounded(.toNearestOrAwayFromZero)
if I.self == UInt8.self {
return UInt8(rounded) as! I
} else if I.self == UInt16.self {
return UInt16(rounded) as! I
} else {
return I(rounded)
}
}
When fully specialised, this produces equivalent assembly to floatToUnormConcrete
, which is what we want. However, no Swift author should ever have to write this.
Semantically, what we want is the ability for the method UInt8.init(_ source: Float)
to be marked as an equivalent more-specific overload of FixedWidthInteger.init<T: BinaryFloatingPoint>(_ source: T)
; however, to my knowledge, there's currently no way to express this in Swift. I'm not proposing any specific syntax here; however, I'm proposing that there be a supported way to mark methods as equivalent such that the compiler is allowed to replace one with another when specialising in a generic context.
Beyond the example I've outlined here, this is actually a very important issue in other contexts. SwiftUI on macOS's performance profile is dominated by calls to BinaryFloatingPoint._convert
when converting between CGFloat
s and Float
/Double
s; I obviously don't have access to SwiftUI's source, but I'm reasonably sure that it's caused by the same problem:
The assembly for all three of the snippets above can be viewed here on Godbolt.