patternswiftMinor
Swift FloatingPoint rounded to places
Viewed 0 times
swiftroundedfloatingpointplaces
Problem
I'm playing a bit with extensions for default protocols in Swift 3.0.
I would like to implement
Both of these structs refer to
I wrote small extension for
However I can't find the most easiest and fastest method.
The problem is that there is no pow for floating point type, so I need to do lots of conversions.
At 1m calls it loses about 0.04s for the Double extension, which doesn't do
I would like to implement
rounded(toPlaces places: Int) -> Self for Double, Float.Both of these structs refer to
FloatingType protocol. I wrote small extension for
FloatingType protocol:extension FloatingPoint {
typealias Exponent = Int // is this okay? how can I write where `Exponent: Int` ?
public func rounded(toPlaces places: Int) -> Self {
guard places >= 0 else { return self }
let divisor = Self(Int(pow(10.0, Double(places))))
//let divisor = Self(sign: .plus, exponent: places, significand: Self(Int(pow(5, Double(places)))))
return (self * divisor).rounded() / divisor
}
}However I can't find the most easiest and fastest method.
Self(Int(pow(5, Double(places)))) so much conversions... Ugh...The problem is that there is no pow for floating point type, so I need to do lots of conversions.
At 1m calls it loses about 0.04s for the Double extension, which doesn't do
Self(Int(...)). Is it okay for such method or there is another approach?Solution
First of all one should be aware that most decimal fractions cannot
be represented exactly by a binary floating point number. For example,
does not assign the number
which is representable in the IEEE 754 format for double precision
floating point numbers, which happens to be
If the purpose of rounding the number is to present a result to the
user up to a certain precision, then it is better to use a number
formatter instead, for example
or
according to the user's locale settings.
In other cases (like working with monetary values) it may be more
appropriate to work with integers only (or
Having said that, here are some remarks and possible improvements
of your method.
I have named different implementations
program.
I am not sure what the effect of
in the protocol extension is, but the proper way to restrict an extension
is a
Of course you don't need a restriction for your other variant:
The first one looks a bit obfuscated, and relies on the representation
of the numbers as a binary mantissa with an exponent.
I would use it only if it gives a clear performance advantage.
So let's benchmark it! I used this simple
test code which rounds 10,000,000 numbers in the range 0.0 .. 10.0
to 1 .. 8 decimal digits:
The results are added to prevent the compiler from removing function
calls whose result is unused, and to verify that all methods give
exactly the same results.
On a 3,5 GHz iMac, with the code compiled in Release mode, I got
the following timings:
rounded1: 2.66 sec
rounded2: 2.52 sec
so the "obfuscated" method is actually a tiny bit slower.
But we can improve the performance. As you said, there is a lot of type conversions in
This can be improved a bit if we restrict the extension method to
the
restriction, because all current floating point types (
a value can be initialized from a
That makes it a bit faster:
rounded1: 2.34 sec
The bottleneck seems to be the
will usually be a small integer, it might be worth to use
iterated multiplication instead:
Here
the type of
integers or instances of
This is more than 4x faster compared to
a single type conversion:
rounded4: 0.49 sec
be represented exactly by a binary floating point number. For example,
let y = Double.pi.rounded(toPlaces: 3)does not assign the number
3.142 to y but the closest numberwhich is representable in the IEEE 754 format for double precision
floating point numbers, which happens to be
3.141999999999999904076730672386474907398223876953125If the purpose of rounding the number is to present a result to the
user up to a certain precision, then it is better to use a number
formatter instead, for example
print(String(format: "%.3f", Double.pi)) // 3.142or
NumberFormatter which is very versatile and can do the conversionaccording to the user's locale settings.
In other cases (like working with monetary values) it may be more
appropriate to work with integers only (or
DecimalNumber) to avoid all rounding issues.Having said that, here are some remarks and possible improvements
of your method.
I have named different implementations
rounded1, rounded2, ... so that all can be tested in the sameprogram.
I am not sure what the effect of
typealias Exponent = Intin the protocol extension is, but the proper way to restrict an extension
is a
where clause:extension FloatingPoint where Exponent == Int {
public func rounded1(toPlaces places: Int) -> Self {
guard places >= 0 else { return self }
let divisor = Self(sign: .plus, exponent: places, significand: Self(Int(pow(5, Double(places)))))
return (self * divisor).rounded() / divisor
}
}Of course you don't need a restriction for your other variant:
extension FloatingPoint {
public func rounded2(toPlaces places: Int) -> Self {
guard places >= 0 else { return self }
let divisor = Self(Int(pow(10.0, Double(places))))
return (self * divisor).rounded() / divisor
}
}The first one looks a bit obfuscated, and relies on the representation
of the numbers as a binary mantissa with an exponent.
I would use it only if it gives a clear performance advantage.
So let's benchmark it! I used this simple
test code which rounds 10,000,000 numbers in the range 0.0 .. 10.0
to 1 .. 8 decimal digits:
let start = Date()
var sum = 0.0
for i in 1 ... 10_000_000 {
let x = Double(i)/1_000_000.0
for p in 1 ... 8 {
let y = x.rounded(toPlaces: p)
sum += y
}
}
let end = Date()
let time = end.timeIntervalSince(start)
print(sum, time)The results are added to prevent the compiler from removing function
calls whose result is unused, and to verify that all methods give
exactly the same results.
On a 3,5 GHz iMac, with the code compiled in Release mode, I got
the following timings:
rounded1: 2.66 sec
rounded2: 2.52 sec
so the "obfuscated" method is actually a tiny bit slower.
But we can improve the performance. As you said, there is a lot of type conversions in
let divisor = Self(Int(pow(10.0, Double(places))))This can be improved a bit if we restrict the extension method to
the
BinaryFloatingPoint protocol instead. This is not a severe restriction, because all current floating point types (
Float, Double,CGFloat) conform to BinaryFloatingPoint. The advantage is thata value can be initialized from a
Double:extension BinaryFloatingPoint {
public func rounded3(toPlaces places: Int) -> Self {
guard places >= 0 else { return self }
let divisor = Self(pow(10.0, Double(places)))
return (self * divisor).rounded() / divisor
}
}That makes it a bit faster:
rounded1: 2.34 sec
The bottleneck seems to be the
pow function. Since placeswill usually be a small integer, it might be worth to use
iterated multiplication instead:
extension BinaryFloatingPoint {
public func rounded4(toPlaces places: Int) -> Self {
guard places >= 0 else { return self }
let divisor = Self((0..<places).reduce(1.0) { (accum, _) in 10.0 * accum })
return (self * divisor).rounded() / divisor
}
}Here
Double values are multiplied and the result converted tothe type of
Self. This turned out to be faster than multiplyingintegers or instances of
Self. This is more than 4x faster compared to
rounded3 and needs onlya single type conversion:
rounded4: 0.49 sec
Code Snippets
let y = Double.pi.rounded(toPlaces: 3)3.141999999999999904076730672386474907398223876953125print(String(format: "%.3f", Double.pi)) // 3.142typealias Exponent = Intextension FloatingPoint where Exponent == Int {
public func rounded1(toPlaces places: Int) -> Self {
guard places >= 0 else { return self }
let divisor = Self(sign: .plus, exponent: places, significand: Self(Int(pow(5, Double(places)))))
return (self * divisor).rounded() / divisor
}
}Context
StackExchange Code Review Q#142748, answer score: 6
Revisions (0)
No revisions yet.