Skip to content

Commit

Permalink
Improve: arbitrary integer multiplication (#84).
Browse files Browse the repository at this point in the history
All multiplication related benchmarks are like 3x faster after this patch.
  • Loading branch information
oscbyspro committed Sep 7, 2024
1 parent 1f97a2e commit d43e8d0
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 56 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,8 +678,8 @@ It uses a fast double-and-add algorithm:
###### MacBook Pro, 13-inch, M1, 2020, -O, code coverage disabled.

```swift
try! Fibonacci<UXL>( 1_000_000) // 0.04s
try! Fibonacci<UXL>(10_000_000) // 1.65s
try! Fibonacci<UXL>( 1_000_000) // 0.02s
try! Fibonacci<UXL>(10_000_000) // 0.50s
```

But you can also step through it manually:
Expand Down
4 changes: 2 additions & 2 deletions Sources/CoreKit/BinaryInteger+Addition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ extension BinaryInteger {

/// The next value in arithmetic progression.
@inlinable public consuming func incremented(_ condition: Bool = true) -> Fallible<Self> {
self.plus(Self(Bit(condition))) // size >= 8
self.plus(Self(Bit(condition)))
}

/// The next value in arithmetic progression.
@inlinable public consuming func decremented(_ condition: Bool = true) -> Fallible<Self> {
self.minus(Self(Bit(condition))) // size >= 8
self.minus(Self(Bit(condition)))
}
}

Expand Down
52 changes: 32 additions & 20 deletions Sources/CoreKit/Models/DataInt+Addition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,21 +328,27 @@ extension MutableDataInt.Body {

/// - Requires: `self.count >= elements.count + 1`
@inlinable public consuming func incrementSubSequence(
by elements: borrowing Immutable,
times multiplier: consuming Element,
plus increment: Element = .zero
by elements: consuming Immutable,
times multiplier: borrowing Element,
plus increment: consuming Element = .zero
) -> Fallible<Self> {

var bit: Bool
var increment = increment // consume: compiler bug...
var overflow: (Bool, Bool)

for index in elements.indices {
let product = elements[unchecked: index].multiplication(multiplier, plus: increment)
(self[unchecked: index], bit) = (self[unchecked: index]).plus(product.low).components()
(increment) = product.high.plus(Element(Bit(bit))).unchecked()
while !elements.isEmpty {
let (low,high) = elements[unchecked: ((()))].multiplication(multiplier).components()
(self[unchecked: ()], overflow.0) = self[unchecked: ()].plus((((low)))).components()
(self[unchecked: ()], overflow.1) = self[unchecked: ()].plus(increment).components()

increment = consume high
increment = increment.incremented(overflow.0).unchecked()
increment = increment.incremented(overflow.1).unchecked()

self = (consume self )[unchecked: 1...]
elements = (consume elements)[unchecked: 1...]
}

return (consume self)[unchecked: elements.count...].incrementSubSequence(by: increment)
return (consume self).incrementSubSequence(by: increment)
}

/// - Requires: `self.count >= elements.count + 1`
Expand All @@ -358,20 +364,26 @@ extension MutableDataInt.Body {

/// - Requires: `self.count >= elements.count + 1`
@inlinable public consuming func decrementSubSequence(
by elements: borrowing Immutable,
times multiplier: consuming Element,
plus decrement: Element = .zero
by elements: consuming Immutable,
times multiplier: borrowing Element,
plus decrement: consuming Element = .zero
) -> Fallible<Self> {

var bit: Bool
var decrement = decrement // consume: compiler bug...
var overflow: (Bool, Bool)

for index in elements.indices {
let product = elements[unchecked: index].multiplication(multiplier, plus: decrement)
(self[unchecked: index], bit) = (self[unchecked: index]).minus(product.low).components()
(decrement) = product.high.plus(Element(Bit(bit))).unchecked()
while !elements.isEmpty {
let (low,high) = elements[unchecked:(((())))].multiplication(multiplier).components()
(self[unchecked: ()], overflow.0) = self[unchecked: ()].minus((((low)))).components()
(self[unchecked: ()], overflow.1) = self[unchecked: ()].minus(decrement).components()

decrement = consume high
decrement = decrement.incremented(overflow.0).unchecked()
decrement = decrement.incremented(overflow.1).unchecked()

self = (consume self )[unchecked: 1...]
elements = (consume elements)[unchecked: 1...]
}

return (consume self)[unchecked: elements.count...].decrementSubSequence(by: decrement)
return (consume self).decrementSubSequence(by: decrement)
}
}
45 changes: 25 additions & 20 deletions Sources/CoreKit/Models/DataInt+Multiplication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ extension MutableDataInt.Body {
/// [algorithm]: https://en.wikipedia.org/wiki/multiplication_algorithm
///
@inlinable public consuming func initialize(to lhs: Immutable, times rhs: Immutable) {
if 16 > Swift.min(lhs.count, rhs.count) {
self.initializeByLongAlgorithm(to: lhs, times: rhs, plus: .zero)
if 32 > Swift.min(lhs.count, rhs.count) {
self.initializeByLongAlgorithm(to: lhs, times: rhs)
} else {
self.initializeByKaratsubaAlgorithm(to: lhs, times: rhs)
}
Expand All @@ -71,8 +71,8 @@ extension MutableDataInt.Body {
/// - Parameter self: A buffer of size `2 * elements`.
///
@inlinable public consuming func initialize(toSquareProductOf elements: Immutable) {
if 16 > elements.count {
self.initializeByLongAlgorithm(toSquareProductOf: elements, plus: .zero)
if 32 > elements.count {
self.initializeByLongAlgorithm(toSquareProductOf: elements)
} else {
self.initializeByKaratsubaAlgorithm(toSquareProductOf: elements)
}
Expand All @@ -98,40 +98,45 @@ extension MutableDataInt.Body {
/// [algorithm]: https://en.wikipedia.org/wiki/multiplication_algorithm
///
@inline(never) @inlinable public consuming func initializeByLongAlgorithm(
to lhs: Immutable, times rhs: Immutable, plus increment: Element = .zero
to lhs: consuming Immutable, times rhs: consuming Immutable, plus increment: consuming Element = .zero
) {
//=--------------------------------------=
Swift.assert(self.count == lhs.count+rhs.count, String.indexOutOfBounds())
Swift.assert(self.count >= 1 || increment == 0, String.indexOutOfBounds())

Swift.assert(self.count == lhs.count + rhs.count, String.indexOutOfBounds())
Swift.assert(self.count >= 1 || increment.isZero, String.indexOutOfBounds())
//=--------------------------------------=
var pointer: UnsafeMutablePointer<Element> = self.start
//=--------------------------------------=
// pointee: initialization 1
//=--------------------------------------=
var carry: Element = increment
let first: Element = rhs.count > .zero ? rhs[unchecked: ()] : Element()
let first: Element = rhs.first ?? Element()

for index in lhs.indices {
let product = lhs[unchecked: index].multiplication(first, plus: carry)
carry = product.high
let product = lhs[unchecked: index].multiplication(first, plus: increment)
increment = product.high
pointer.initialize(to: product.low)
pointer = pointer.successor()
}

if !rhs.isEmpty {
pointer.initialize(to: carry)
pointer.initialize(to: increment)
pointer = pointer.successor()
rhs = (consume rhs )[unchecked: 1...]
self = (consume self)[unchecked: 1...]
}

Swift.assert(IX(self.start.distance(to: pointer)) == lhs.count)
//=--------------------------------------=
// pointee: initialization 2
//=--------------------------------------=
for index in rhs.indices.dropFirst() {
while !rhs.isEmpty {
pointer.initialize(to: .zero)
pointer = pointer.successor()
(copy self)[unchecked: index...].incrementSubSequence(by: lhs, times: rhs[unchecked: index], plus: .zero).unchecked()
(copy self).incrementSubSequence(by: copy lhs, times: rhs[unchecked: ()]).unchecked()
rhs = (consume rhs )[unchecked: 1...]
self = (consume self)[unchecked: 1...]
}
Swift.assert(IX(self.start.distance(to: pointer)) == self.count)

Swift.assert(IX(self.start.distance(to: pointer)) == lhs.count)
}

/// Initializes `self` to the square [long][algorithm] product of `elements` plus `increment`.
Expand All @@ -145,9 +150,9 @@ extension MutableDataInt.Body {
@inline(never) @inlinable public consuming func initializeByLongAlgorithm(
toSquareProductOf elements: Immutable, plus increment: Element = .zero
) {
//=--------------------------------------=
Swift.assert(self.count == 2 * elements.count, String.indexOutOfBounds())
Swift.assert(self.count >= 1 || increment == 0, String.indexOutOfBounds())

Swift.assert(self.count == 2 * (elements.count), String.indexOutOfBounds())
Swift.assert(self.count >= 1 || increment.isZero, String.indexOutOfBounds())
//=--------------------------------------=
// pointee: initialization
//=--------------------------------------=
Expand Down
24 changes: 15 additions & 9 deletions Tests/Benchmarks/BinaryInteger+Factorial.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,34 @@ final class BinaryIntegerBenchmarksOnFactorial: XCTestCase {

/// ###### 2024-09-06 (MacBook Pro, 13-inch, M1, 2020):
///
/// - `0.12 seconds`
/// 0.12 seconds
/// 0.04 seconds after (#84)
///
func test1e5AsUXL() {
let value: UXL = blackHoleIdentity(100_000)
XCTAssertEqual(value.factorial().value.entropy(), Count(1_516_706))
let index: UXL = blackHoleIdentity(100_000)
let element: UXL = index.factorial().unwrap()
XCTAssertEqual(element.entropy(), Count(1_516_706))
}

/// ###### 2024-09-06 (MacBook Pro, 13-inch, M1, 2020):
///
/// - `0.40 seconds`
/// 0.40 seconds
/// 0.12 seconds after (#84)
///
func test2e5AsUXL() {
let value: UXL = blackHoleIdentity(200_000)
XCTAssertEqual(value.factorial().value.entropy(), Count(3_233_401))
let index: UXL = blackHoleIdentity(200_000)
let element: UXL = index.factorial().unwrap()
XCTAssertEqual(element.entropy(), Count(3_233_401))
}

/// ###### 2024-09-06 (MacBook Pro, 13-inch, M1, 2020):
///
/// - `1.32 seconds`
/// 1.32 seconds
/// 0.37 seconds after (#84)
///
func test4e5AsUXL() {
let value: UXL = blackHoleIdentity(400_000)
XCTAssertEqual(value.factorial().value.entropy(), Count(6_866_790))
let index: UXL = blackHoleIdentity(400_000)
let element: UXL = index.factorial().unwrap()
XCTAssertEqual(element.entropy(), Count(6_866_790))
}
}
16 changes: 13 additions & 3 deletions Tests/Benchmarks/Fibonacci.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,25 @@ final class FibonacciBenchmarks: XCTestCase {
//=------------------------------------------------------------------------=

func testFibonacciUXL1e5() throws {
blackHole(try! Fibonacci<UXL>(blackHoleIdentity(100_000)))
blackHole(try Fibonacci<UXL>(blackHoleIdentity(100_000)))
}

/// ###### 2024-09-07 (MacBook Pro, 13-inch, M1, 2020):
///
/// 0.04 seconds
/// 0.02 seconds after (#84)
///
func testFibonacciUXL1e6() throws {
blackHole(try! Fibonacci<UXL>(blackHoleIdentity(1_000_000)))
blackHole(try Fibonacci<UXL>(blackHoleIdentity(1_000_000)))
}

/// ###### 2024-09-07 (MacBook Pro, 13-inch, M1, 2020):
///
/// 1.65 seconds
/// 0.50 seconds after (#84)
///
func testFibonacciUXL1e7() throws {
blackHole(try! Fibonacci<UXL>(blackHoleIdentity(10_000_000)))
blackHole(try Fibonacci<UXL>(blackHoleIdentity(10_000_000)))
}

/// ###### 2024-08-08 (MacBook Pro, 13-inch, M1, 2020):
Expand Down

0 comments on commit d43e8d0

Please sign in to comment.