Improve the accuracy of fastCbrt()
Using brute-force numerical methods, we can find a better magic
constant for fastCbrt(). This new constant improves the max
error across various ranges:
- 8.3446500E-7 -> 5.9604645E-7 in the range -1f..1f
- 6.6757200E-6 -> 4.7683716E-6 in the range -256f..256f
- 1.0681152E-4 -> 3.8146973E-5 in the range -65_536f..65_536f
- 2.1362305E-4 -> 1.5258789E-4 in the range -16_777_216..16_777_216f
Test: MathHelpersTest.kt
RelNote: N/A
Change-Id: I1149d7c77fe1e770eb20a2169ea655fdc02cf72e
diff --git a/compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/MathHelpersTest.kt b/compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/MathHelpersTest.kt
index ab429d6..cfa2ead 100644
--- a/compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/MathHelpersTest.kt
+++ b/compose/ui/ui-util/src/androidUnitTest/kotlin/androidx/compose/ui/util/MathHelpersTest.kt
@@ -28,8 +28,8 @@
class MathHelpersTest {
// `f = 16777216f` is the first value where `f + 1 == f` due to float imprecision, so that's
// where testing floating point errors becomes interesting
- val testStart = 16777216L
- val testEnd = testStart + 1000
+ private val testStart = 16777216L
+ private val testEnd = testStart + 1000
@Test
fun testLerpLargeFloats() {
@@ -118,14 +118,14 @@
@Test
fun testZeroFastCbrt() {
- val zeroError = 8.35E-7f
+ val zeroError = 5.97E-7f
assertTrue(fastCbrt(0.0f) <= zeroError)
assertTrue(fastCbrt(-0.0f) >= -zeroError)
}
@Test
fun testFastCbrtError() {
- val maxError = 1.76E-6f
+ val maxError = 1.193E-6f
for (i in 0..65_536) {
val v = i / 8_192.0f // v is in the range 0f..8f
val error = abs(fastCbrt(v) - cbrt(v))
diff --git a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
index b96decc..3b08ee1 100644
--- a/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
+++ b/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
@@ -123,10 +123,10 @@
* - Zero, returns a value close to 0 (~8.3e-14) with the same sign
*
* The maximum error compared to [kotlin.math.cbrt] is:
- * - 8.3446500E-7 in the range -1f..1f
- * - 6.6757200E-6 in the range -256f..256f
- * - 1.0681152E-4 in the range -65_536f..65_536f
- * - 2.1362305E-4 in the range -16_777_216..16_777_216f
+ * - 5.9604645E-7 in the range -1f..1f
+ * - 4.7683716E-6 in the range -256f..256f
+ * - 3.8146973E-5 in the range -65_536f..65_536f
+ * - 1.5258789E-4 in the range -16_777_216..16_777_216f
*/
fun fastCbrt(x: Float): Float {
// Our fast cube root approximation is implemented using the binary
@@ -219,20 +219,18 @@
// Rhapson method. One round proved not precise enough for our needs, and
// more rounds don't improve the results significantly given our use cases.
//
- // Note: the constant 0x2a555555 we computed earlier is only a standalone
+ // Note: the constant 0x2a555555 we computed above is only a standalone
// approximation that doesn't account for the subsequent Newton-Rhapson
// refinements. The approximation can be improved for Newton-Rhapson by
- // debiasing it. To debias 0x2a555555 we follow the solution used by the
- // now famous "Quake 3 inverse square root". Instead of using 0x5f400000
- // as the "magic constant" (following our derivation above, you can compute
- // the constant for an inverse square root using (1 - -1/2) * I(1)), the
- // Quake 3 solution uses 0x5f3759df. To improve our constant we simply
- // apply the same ratio:
- //
- // magic = 0x2a555555 * 0x5f3759df / 0x5f400000
- // = 0x2a517d40
+ // debiasing it. To debias 0x2a555555, we just use a brute-force method to
+ // minimize the error between cbrt() and this function. Doing so gives us
+ // a new constant, 0x2a510554L, which greatly improves the maximum error:
+ // - 6.2584877E-6 -> 5.9604645E-7 in the range -1f..1f
+ // - 5.0067900E-5 -> 4.7683716E-6 in the range -256f..256f
+ // - 4.0054320E-4 -> 3.8146973E-5 in the range -65_536f..65_536f
+ // - 1.6021729E-3 -> 1.5258789E-4 in the range -16_777_216..16_777_216f
val v = x.toRawBits().toLong() and 0x1ffffffffL
- var estimate = floatFromBits((0x2a517d40L + v / 3).toInt())
+ var estimate = floatFromBits((0x2a510554L + v / 3).toInt())
// 2 rounds of the Newton-Rhapson method to improve accuracy
estimate -= (estimate - x / (estimate * estimate)) * (1.0f / 3.0f)