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 multiplication #95

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft

Conversation

dlesnoff
Copy link
Contributor

This is still a draft.
I get a strange error in one test of the tliterals.nim file. I have no clue why this test is wrong when I switch the karatsuba multiplication.

@dlesnoff dlesnoff marked this pull request as ready for review January 31, 2022 10:38
@dlesnoff
Copy link
Contributor Author

Lack tests for multiplication.

@dlesnoff dlesnoff marked this pull request as draft January 31, 2022 12:01
Copy link
Contributor

@konsumlamm konsumlamm left a comment

Choose a reason for hiding this comment

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

Note: I haven't looked at karatsubaMultiplication itself yet.

src/bigints.nim Outdated Show resolved Hide resolved
src/bigints.nim Outdated Show resolved Hide resolved
src/bigints.nim Outdated
Comment on lines 451 to 454
if bl <= karatsubaTreshold:
karatsubaMultiplication(a, c, b)
else:
unsignedMultiplication(a, c, b)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if bl <= karatsubaTreshold:
karatsubaMultiplication(a, c, b)
else:
unsignedMultiplication(a, c, b)
if bl > karatsubaTreshold:
karatsubaMultiplication(a, c, b)
else:
unsignedMultiplication(a, c, b)

src/bigints.nim Outdated
Comment on lines 456 to 459
if cl <= karatsubaTreshold:
karatsubaMultiplication(a, b, c)
else:
unsignedMultiplication(a, b, c)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if cl <= karatsubaTreshold:
karatsubaMultiplication(a, b, c)
else:
unsignedMultiplication(a, b, c)
if cl > karatsubaTreshold:
karatsubaMultiplication(a, b, c)
else:
unsignedMultiplication(a, b, c)

src/bigints.nim Outdated
Comment on lines 467 to 473
if bl == 1:
# base case : multiply the only limb with each limb of second term
scalarMultiplication(a, c, b.limbs[0])
return
if cl == 1:
scalarMultiplication(a, b, c.limbs[0])
return
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if bl == 1:
# base case : multiply the only limb with each limb of second term
scalarMultiplication(a, c, b.limbs[0])
return
if cl == 1:
scalarMultiplication(a, b, c.limbs[0])
return

This is already done in unsignedMultiplication afaict.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea was to reduce the number of operations if we know there is only one limb.
We do not have to do a whole for loop.
But you are right, we can do a scalarMultiplication with unsignedMultiplication.

src/bigints.nim Outdated Show resolved Hide resolved
src/bigints.nim Outdated
Comment on lines 474 to 485
if bl < karatsubaTreshold:
if cl <= bl:
unsignedMultiplication(a, b, c)
else:
unsignedMultiplication(a, c, b)
return
if cl < karatsubaTreshold:
if bl <= cl:
unsignedMultiplication(a, c, b)
else:
unsignedMultiplication(a, b, c)
return
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it makes more sense to move this logic into unsignedMultiplication. Then we avoid checking this twice when calling karatsubaMultiplication from multiplication and we can call unsignedMultiplication inside karatsubaMultiplication (instead of karatsubaMultiplication itself).

Copy link
Contributor Author

@dlesnoff dlesnoff Feb 11, 2022

Choose a reason for hiding this comment

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

I have fixed many bugs in the version you just reviewed, I am sorry I should have warned you, there will be many changes to this version.
I made karatsubaMultiplication and multiplication completely independent to test them independently and compare obtained results.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine, I didn't expect this to be the final version anyway. As I said, I haven't looked at karatsubaMultiplication in depth yet, since I want to better understand the algorithm first.

Comment on lines +490 to +493
low_b.limbs = b.limbs[0 .. k-1]
high_b.limbs = b.limbs[k .. ^1]
low_c.limbs = c.limbs[0 .. k-1]
high_c.limbs = c.limbs[k .. ^1]
Copy link
Contributor

Choose a reason for hiding this comment

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

These all create new seqs, which isn't very efficient. Perhaps we should use openArray[uint32] instead (then this can use toOpenArray for O(1) slicing) or make our own "sliceable seq" to avoid copies.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I did not thougt about this. I used seq because I expect to join the results into a seq after.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@konsumlamm openArray are only used for procs arguments, when we want either a seq or an array as argument.
Can't we use some pointers here ? Do we have to create a new structure ?
Internal structure is a seq, if we use another structure, we would have to make a copy anyway or change the internal structure.
We can not use arrays neither, since we do not know the size at compile time. (It depends on the variable k, which value is known at runtime).

Copy link
Contributor

Choose a reason for hiding this comment

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

An openArray is basically a pointer and a length, it doesn't create a copy. Anything that doesn't create a copy but just modifies indices/pointers should be good.

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 do not see how we can call multiplication then on those pointers, without making a multiplication directly on arrays.
We need to initialize BigInts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These all create new seqs, which isn't very efficient. Perhaps we should use openArray[uint32] instead (then this can use toOpenArray for O(1) slicing) or make our own "sliceable seq" to avoid copies.

The algorithm wants the value of the polynomial corresponding to each parts of the slice.
The best way so far that I conceive is to modify the BigInt's limbs field from:

type
  BigInt* = object
    limbs: seq[uint32]
    isNegative: bool

to

type
  BigInt* = object
    limbs: ref seq[uint32]
    isNegative: bool

Otherwise, we will have to reimplement addition, subtraction and base case multiplication for another container.

Copy link

Choose a reason for hiding this comment

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

seqs are openarrays, if you implement for openarrays, it's implemented for seq and arrays.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have to change the whole code.

Addition, subtraction and multiplication take bigints with a sequence parameter as input.

For the Karatsuba algorithm as well as some others, we need to manipulate these sequences through a pointer and get a pointer for parts of the sequence.

We also need to operate on those slices of the sequence, i.e. get the value associated with each part of the slice, and add, subtract, and multiply these slices.

That's why we need a seq-like container with the possibility of getting a reference to each value of the seq.

None of the openarray types enables this.

Choose a reason for hiding this comment

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

As @konsumlamm said, I think openArray and toOpenArray is good enough to implement karatsuba multiplication.
You can write recursive procedure like this using openArray and toOpenArray:

proc sum(x: openArray[int]): int =
  case x.len:
  of 0:
    0
  of 1:
    x[0]
  of 2:
    x[0] + x[1]
  else:
    let mid = x.len div 2
    sum(toOpenArray(x, 0, mid - 1)) + sum(toOpenArray(x, mid, x.high))

echo sum([1, 2, 3, 4, 5])

This is a part of karatsubaMultiplication in your current PR:

var
  low_b, high_b, low_c, high_c: BigInt
# Decompose `b` and `c` in two parts of (almost) equal length
low_b.limbs = b.limbs[0 .. k-1]
high_b.limbs = b.limbs[k .. ^1]
low_c.limbs = c.limbs[0 .. k-1]
high_c.limbs = c.limbs[k .. ^1]

# subtractive version of Karatsuba's algorithm to limit carry handling
var lowProduct, highProduct, add3, add4, add5, middleTerm: BigInt = zero

multiplication(lowProduct, low_b, low_c)
multiplication(highProduct, high_b, high_c)

add3 = low_b - high_b
add4 = high_c - low_c

Above code would be written using toOpenArray like:

# Decompose `b` and `c` in two parts of (almost) equal length
template low_b = toOpenArray(b.limbs, 0, k - 1)
template high_b = toOpenArray(b.limbs, k, b.limbs.high)
template low_c = toOpenArray(c.limbs, 0, k - 1)
template high_c = toOpenArray(c.limbs, k, c.limbs.high)

# subtractive version of Karatsuba's algorithm to limit carry handling
var lowProduct, highProduct, add3, add4, add5, middleTerm: BigInt = zero

multiplication(lowProduct, low_b, low_c)
multiplication(highProduct, high_b, high_c)

# This code requires subtraction proc that takes 2 `openArray`
add3 = low_b - high_b
add4 = high_c - low_c

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the time taken and the detailed changes.

# This code requires subtraction proc that takes 2 `openArray`

I just want to point out that scalarMultiplication and unsignedMultiplication will also have to take openArrays:

  if bl == 1:
    scalarMultiplication(a, c, b.limbs[0]) # b and c are openArrays
    a.isNegative = b.isNegative xor c.isNegative
    return 
 ...
  if bl < karatsubaThreshold:
    if cl <= bl:
      unsignedMultiplication(a, b, c) # b and c are openArrays here
    else:
      unsignedMultiplication(a, c, b) # same
    a.isNegative = b.isNegative xor c.isNegative
    return
   ...

I can make a multiplication(a: var Bigint, b, c: openArray[uint32]) = proc, and avoid to rewrite addition and shr for openArrays.

I read the unsignedMultiplication and scalarMultiplication proc algorithms again and they effectively don't need parameters to be BigInts.
The substract proc will be quite delicate to convert for OpenArray parameters though.

I will look into it.

src/bigints.nim Outdated
# limit carry handling in opposition to the additive version
var
lowProduct, highProduct, A3, A4, A5, middleTerm: BigInt = zero
karatsubaMultiplication(lowProduct, low_b, low_c)
Copy link
Contributor

Choose a reason for hiding this comment

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

low_b and low_c aren't necessarily normalized afaict, so that may cause problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This might be the problem I encounter at compilation time. I have not done any test when the operands does not have the same limbs sequence’s length.

src/bigints.nim Outdated Show resolved Hide resolved
dlesnoff and others added 8 commits February 11, 2022 21:09
Co-authored-by: konsumlamm <[email protected]>
Add recent changes in the library.
Karatsuba only works in running time. Strange errors when the tests are executed as static. I commented for the present. Tests should be moved from main to tbigints.nim
# Decompose `b` and `c` in two parts of (almost) equal length
low_b.limbs = b.limbs[0 .. k-1]
high_b.limbs = b.limbs[k .. ^1]
low_c.limbs = c.limbs[0 .. k-1]

Choose a reason for hiding this comment

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

When c.limbs.len is smaller than half of b.limbs.len, k is larger than c.limbs.len and low_c.limbs = c.limbs[0 .. k-1] cause IndexDefect.
It would be better to add tests that multiply a large value by a small value.

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 can simply change n to the min of bl and cl in this case.
I want to add these tests, but I am waiting for my other PRs to be merged. In these, I have added random generation and benchmarks. I am especially waiting for #112 . With initRandomBigInt, I will be able to generate bigints of a specific size for tests.

@dlesnoff
Copy link
Contributor Author

dlesnoff commented Oct 11, 2022 via email

@konsumlamm
Copy link
Contributor

Furthermore, I am really concern by the choice of uint32 vs uint64 (as suggested by @mratsim). This seems to be a big change for the library, that we need to tackle beforehand.

Changing that requires proper support for addition and multiplication of uint64 without overflow (and I don't think writing that in assembly is a good solution). I don't see what that has to do with Karatsuba multiplication though, it doesn't really change the algorithm.

@@ -420,6 +419,30 @@ func unsignedMultiplication(a: var BigInt, b, c: BigInt) {.inline.} =
inc pos
normalize(a)

func scalarMultiplication(a: var BigInt, b: BigInt, c: uint32) {.inline.} =
# always called with bl >= cl
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This comment needs to be removed, since cl == 1 in this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants