ionspin / kotlin-multiplatform-bignum

A Kotlin multiplatform library for arbitrary precision arithmetics
Apache License 2.0
339 stars 40 forks source link

BigDecimal: Replace ==ZERO by isZero, ~40% time reduction on main methods #301

Open glureau opened 4 weeks ago

glureau commented 4 weeks ago

Replacing == ZERO by isZero() on BigDecimal class.

Motivation

equals method calls BD.compare(BD) which requires to bring the significant to same exponent before comparing (if either exponent or precision are not equals). This calculation go through getRidOfRadix and numberOfDecimalDigits which is in total quite expansive.

Metrics

As I've seen in #299 and #300, optimization gains are relatively similar no matter the platform. To save some time I'll only use JVM (the quickest) to benchmark all public methods directly involving the '== ZERO'. (Don't hesitate to ask me for more benchmarks if you think it's risky.)

Benchmark (JVM)

Best of 3 runs, 1000000 iterations each, M3 Pro with 36GB RAM, duration in ms

Method BEFORE AFTER % time reduction
subtract 395 219 44%
add 287 164 43%
pow 269 153 43%
hashCode 202 66 67%
add(ZERO) 79 20 75%
toStringExpanded 167 110 34%

Benchmark code

    @Test
    fun perfAddZero() {
        val bd = BigDecimal.parseString("123")
        val duration = measureTime {
            repeat(1_000_000) { bd + BigDecimal.ZERO }
        }
        println("add(ZERO): $duration")
    }

    @Test
    fun perfAdd() {
        val a = BigDecimal.parseString("123")
        val b = BigDecimal.parseString("34.56")
        val duration = measureTime {
            repeat(1_000_000) { a + b }
        }
        println("add: $duration")
    }

    @Test
    fun perfSubtract() {
        val a = BigDecimal.parseString("123")
        val b = BigDecimal.parseString("34.56")
        val duration = measureTime {
            repeat(1_000_000) { a - b }
        }
        println("subtract: $duration")
    }

    @Test
    fun perfHashCode() {
        val bd = BigDecimal.parseString("123.45")
        val duration = measureTime {
            // hashCode uses removeTrailingZeroes
            repeat(1_000_000) { bd.hashCode() }
        }
        println("hashCode: $duration")
    }

     @Test
    fun perfPow() {
        val bd = BigDecimal.parseString("123.45")
        val duration = measureTime {
            repeat(1_000_000) { bd.pow(3) }
        }
        println("pow: $duration")
    }

     @Test
    fun perfToStringExpanded() {
        val bd = BigDecimal.parseString("123.45")
        val duration = measureTime {
            repeat(1_000_000) { bd.toStringExpanded() }
        }
        println("toStringExpanded: $duration")
    }

Raw data

Before

Run 1 subtract: 395.261291ms add: 288.861958ms pow: 282.718375ms hashCode: 212.823917ms add(ZERO): 93.253209ms toStringExpanded: 167.068250ms

Run 2 subtract: 397.731ms add: 312.411208ms pow: 269.341042ms hashCode: 206.681333ms add(ZERO): 78.573125ms toStringExpanded: 167.801541ms

Run 3 subtract: 394.628875ms add: 286.920209ms pow: 279.920042ms hashCode: 202.090084ms add(ZERO): 79.048292ms toStringExpanded: 176.490833ms

After

Run 1 subtract: 225.815875ms add: 163.852750ms pow: 155.257792ms hashCode: 67.060416ms add(ZERO): 20.868541ms toStringExpanded: 117.800792ms

Run 2 subtract: 229.606292ms add: 196.132167ms pow: 153.424542ms hashCode: 66.612959ms add(ZERO): 19.638667ms toStringExpanded: 109.715250ms

Run 3 subtract: 219.301333ms add: 187.846375ms pow: 157.756ms hashCode: 66.152625ms add(ZERO): 20.008958ms toStringExpanded: 111.473625ms