Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Karatsuba added, 2 bugs are fixed #328

Merged
merged 15 commits into from
May 14, 2021
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@ import space.kscience.kmath.operations.BigIntField
import space.kscience.kmath.operations.JBigIntegerField
import space.kscience.kmath.operations.invoke

private fun BigInt.pow(power: Int): BigInt = modPow(BigIntField.number(power), BigInt.ONE)


@State(Scope.Benchmark)
internal class BigIntBenchmark {

val kmNumber = BigIntField.number(Int.MAX_VALUE)
val jvmNumber = JBigIntegerField.number(Int.MAX_VALUE)
val largeKmNumber = BigIntField { number(11).pow(100_000) }
val largeKmNumber = BigIntField { number(11).pow(100_000UL) }
val largeJvmNumber = JBigIntegerField { number(11).pow(100_000) }
val bigExponent = 50_000

Expand Down Expand Up @@ -59,7 +57,7 @@ internal class BigIntBenchmark {

@Benchmark
fun kmPower(blackhole: Blackhole) = BigIntField {
blackhole.consume(kmNumber.pow(bigExponent))
blackhole.consume(kmNumber.pow(bigExponent.toULong()))
}

@Benchmark
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,22 @@ public interface Ring<T> : Group<T>, RingOperations<T> {
* neutral operation for multiplication
*/
public val one: T

public fun T.pow(exponent: ULong): T = when {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any real use-cases for Long power?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean negative exponent usage? If so, no. It is impossible to implement.

Or do you mean big range? If so, yes for some non-bigint rings. As we don't know what the rings are, we cannot say anything about the usage of exponents of UInt.MAX_VALUE.toULong().inc()..ULog.MAX_VALUE

this == zero && exponent > 0UL -> zero
this == one -> this
this == -one -> powWithoutOptimization(exponent % 2UL)
else -> powWithoutOptimization(exponent)
}

private fun T.powWithoutOptimization(exponent: ULong): T = when (exponent) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation details should not be present In generic algebra. Probably better to use extensions instead. Like Ring<T>.pow(T, exponent). This change needs additional discussion.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely agree with @altavir.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it should be placed there (as an extension). Thank you! I'll fix it.
Furthermore, it may be better to create a function in BigInt that would make it possible to use power for bigints without BigIntField { }. What do you think, @altavir @CommanderTvis? After inventing multiple receivers that should be replaced though.

0UL -> one
1UL -> this
else -> {
val pre = powWithoutOptimization(exponent shr 1).let { it * it }
if (exponent and 1UL == 0UL) pre else pre * this
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ public class BigInt internal constructor(
else -> sign * compareMagnitudes(magnitude, other.magnitude)
}

public override fun equals(other: Any?): Boolean =
if (other is BigInt) compareTo(other) == 0 else error("Can't compare KBigInteger to a different type")
public override fun equals(other: Any?): Boolean = other is BigInt && compareTo(other) == 0

public override fun hashCode(): Int = magnitude.hashCode() + sign

Expand Down Expand Up @@ -87,20 +86,23 @@ public class BigInt internal constructor(
public operator fun times(b: BigInt): BigInt = when {
this.sign == 0.toByte() -> ZERO
b.sign == 0.toByte() -> ZERO
// TODO: Karatsuba
b.magnitude.size == 1 -> this * b.magnitude[0] * b.sign.toInt()
this.magnitude.size == 1 -> b * this.magnitude[0] * this.sign.toInt()
else -> BigInt((this.sign * b.sign).toByte(), multiplyMagnitudes(this.magnitude, b.magnitude))
}

public operator fun times(other: UInt): BigInt = when {
sign == 0.toByte() -> ZERO
other == 0U -> ZERO
other == 1U -> this
else -> BigInt(sign, multiplyMagnitudeByUInt(magnitude, other))
}

public operator fun times(other: Int): BigInt = if (other > 0)
this * kotlin.math.abs(other).toUInt()
else
-this * kotlin.math.abs(other).toUInt()
public operator fun times(other: Int): BigInt = when {
other > 0 -> this * kotlin.math.abs(other).toUInt()
other != Int.MIN_VALUE -> -this * kotlin.math.abs(other).toUInt()
else -> times(other.toBigInt())
}

public operator fun div(other: UInt): BigInt = BigInt(this.sign, divideMagnitudeByUInt(this.magnitude, other))

Expand Down Expand Up @@ -238,6 +240,7 @@ public class BigInt internal constructor(
public const val BASE_SIZE: Int = 32
public val ZERO: BigInt = BigInt(0, uintArrayOf())
public val ONE: BigInt = BigInt(1, uintArrayOf(1u))
private const val KARATSUBA_THRESHOLD = 80

private val hexMapping: HashMap<UInt, String> = hashMapOf(
0U to "0", 1U to "1", 2U to "2", 3U to "3",
Expand Down Expand Up @@ -318,7 +321,16 @@ public class BigInt internal constructor(
return stripLeadingZeros(result)
}

private fun multiplyMagnitudes(mag1: Magnitude, mag2: Magnitude): Magnitude {
internal fun multiplyMagnitudes(mag1: Magnitude, mag2: Magnitude): Magnitude = when {
mag1.size + mag2.size < KARATSUBA_THRESHOLD || mag1.isEmpty() || mag2.isEmpty() -> naiveMultiplyMagnitudes(
mag1,
mag2
)
// TODO implement Fourier
else -> karatsubaMultiplyMagnitudes(mag1, mag2)
}

internal fun naiveMultiplyMagnitudes(mag1: Magnitude, mag2: Magnitude): Magnitude {
val resultLength = mag1.size + mag2.size
val result = Magnitude(resultLength)

Expand All @@ -337,6 +349,21 @@ public class BigInt internal constructor(
return stripLeadingZeros(result)
}

internal fun karatsubaMultiplyMagnitudes(mag1: Magnitude, mag2: Magnitude): Magnitude {
//https://en.wikipedia.org/wiki/Karatsuba_algorithm
val halfSize = min(mag1.size, mag2.size) / 2
val x0 = mag1.sliceArray(0 until halfSize).toBigInt(1)
val x1 = mag1.sliceArray(halfSize until mag1.size).toBigInt(1)
val y0 = mag2.sliceArray(0 until halfSize).toBigInt(1)
val y1 = mag2.sliceArray(halfSize until mag2.size).toBigInt(1)

val z0 = x0 * y0
val z2 = x1 * y1
val z1 = (x0 + x1) * (y1 + y0) - z0 - z2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be more optimal to use the other formula for z1, namely $z1 = (x0-x1)*(y1-y0)+z2+z0$ ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it.


return (z2.shl(2 * halfSize * BASE_SIZE) + z1.shl(halfSize * BASE_SIZE) + z0).magnitude
}

private fun divideMagnitudeByUInt(mag: Magnitude, x: UInt): Magnitude {
val resultLength = mag.size
val result = Magnitude(resultLength)
Expand Down Expand Up @@ -427,7 +454,7 @@ private val hexChToInt: MutableMap<Char, Int> = hashMapOf(
public fun String.parseBigInteger(): BigInt? {
val sign: Int
val sPositive: String

//TODO substring = O(n). Can be replaced by some drop that is O(1)
when {
this[0] == '+' -> {
sign = +1
Expand All @@ -447,8 +474,10 @@ public fun String.parseBigInteger(): BigInt? {
var digitValue = BigInt.ONE
val sPositiveUpper = sPositive.uppercase()

if (sPositiveUpper.startsWith("0X")) { // hex representation
if (sPositiveUpper.startsWith("0X")) {
// hex representation
val sHex = sPositiveUpper.substring(2)
// TODO optimize O(n2) -> O(n)

for (ch in sHex.reversed()) {
if (ch == '_') continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package space.kscience.kmath.operations

import space.kscience.kmath.testutils.RingVerifier
import kotlin.math.pow
import kotlin.test.Test
import kotlin.test.assertEquals

Expand All @@ -21,6 +22,19 @@ internal class BigIntAlgebraTest {
assertEquals(res, 1_000_000.toBigInt())
}

@Test
fun testKBigIntegerRingPow() {
BigIntField {
for (num in -5..5)
for (exponent in 0UL..10UL)
assertEquals(
num.toDouble().pow(exponent.toInt()).toLong().toBigInt(),
num.toBigInt().pow(exponent),
"$num ^ $exponent"
)
}
}

@Test
fun testKBigIntegerRingSum_100_000_000__100_000_000() {
BigIntField {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

package space.kscience.kmath.operations

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlin.test.*

@kotlin.ExperimentalUnsignedTypes
class BigIntOperationsTest {
Expand Down Expand Up @@ -150,6 +151,18 @@ class BigIntOperationsTest {
assertEquals(prod, res)
}

@Test
fun testKaratsuba() {
val x = uintArrayOf(12U, 345U)
val y = uintArrayOf(6U, 789U)
assertContentEquals(BigInt.naiveMultiplyMagnitudes(x, y), BigInt.karatsubaMultiplyMagnitudes(x, y))
repeat(1000) {
val x1 = UIntArray(Random.nextInt(100, 1000)) { Random.nextUInt() }
val y1 = UIntArray(Random.nextInt(100, 1000)) { Random.nextUInt() }
assertContentEquals(BigInt.naiveMultiplyMagnitudes(x1, y1), BigInt.karatsubaMultiplyMagnitudes(x1, y1))
}
}

@Test
fun test_shr_20() {
val x = 20.toBigInt()
Expand Down Expand Up @@ -383,4 +396,12 @@ class BigIntOperationsTest {

return assertEquals(res, x % mod)
}

@Test
fun testNotEqualsOtherTypeInstanceButButNotFails() = assertFalse(0.toBigInt().equals(""))

@Test
fun testIntAbsOverflow() {
assertEquals((-Int.MAX_VALUE.toLong().toBigInt() - 1.toBigInt()) * 2, 2.toBigInt() * Int.MIN_VALUE)
}
}