1

All values (a, b, c) are ulongs and can get pretty large (they go up to ulong.Max sometimes as well)

How can I make (a * b) % c execute fast. I have done a whole bunch of research into modular multiplication, but have not been able to find anything useful. So what is a (preferably THE) fastest way to compute this? I know that I can use BigInteger but BigInteger is horribly slow, so I am not going to use it.

Another thing that could work for me is highbits and lowbits % c (because I can use Math.BigMul). But I have not been able to find anything that works for 64 bit for that (only up to 63 bits).

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Please [edit] your question to be more specific. "(a * b) % c" is pretty generic if a, b, and c can be of any numeric type. you talk about BigInteger, so perhaps you're limiting to integers? You talk about 64 bits, so apparently there's a need for it to work at that level? The more constraints you can put on a problem, the easier it will be for people to help you, as it will limit the field of possible solutions. – Heretic Monkey Oct 23 '21 at 03:50
  • Can you give some examples that illustrate the types and numbers you are working with? – John Alexiou Oct 23 '21 at 03:51
  • How frequently does the `a * b` overflow in the `ulong` range? – Theodor Zoulias Oct 23 '21 at 04:01
  • @TheodorZoulias 99.99% of the time (idk raw values, its just pretty much all of the time) considering that they start in the ulong range already – Rubiksmaster02 Oct 23 '21 at 04:02
  • Rubiksmaster02 in that case my first thought would be to do the math using decimals: `(ulong)(((decimal)a * (decimal)b) % (decimal)c)` – Theodor Zoulias Oct 23 '21 at 04:04
  • @TheodorZoulias that gives very inaccurate results. I did think to do that as well, but the numbers were always quite off by alot. – Rubiksmaster02 Oct 23 '21 at 04:05
  • Rubiksmaster02 could you give an example of a, b and c that produces wrong result, when using decimals for the math? – Theodor Zoulias Oct 23 '21 at 04:06
  • Just tested the code out right now, and its actually throwing an error (specifically: Value was either too large or too small for a Decimal) values being passed: a: 284760866952734, b: 297274212699233, c: 333555577577777 – Rubiksmaster02 Oct 23 '21 at 04:10
  • Yeah, you are right. Even the `Decimal` has not enough range. A `System.OverflowException` is thrown. – Theodor Zoulias Oct 23 '21 at 04:28
  • 1
    [Multiply two overflowing integers modulo a third](https://stackoverflow.com/q/14858476/995714), [Fastest way to calculate a 128-bit integer modulo a 64-bit integer](https://stackoverflow.com/q/2566010/995714) – phuclv Oct 23 '21 at 04:50
  • @phuclv please do not jump to conclusions or I will report you. the question you marked min a duplicate as literally states that `a,b <= c < INT_MAX` which mine does not classify under. SO it would be great if you would read closer next time and reopen my question. – Rubiksmaster02 Oct 23 '21 at 05:02
  • @Rubiksmaster02 the algorithm is the same. Did you even read the answers and the linked questions there? [This question](https://stackoverflow.com/q/17283192/995714) asks about 64-bit unsigned long and is **the same as yours** – phuclv Oct 23 '21 at 06:07
  • @phuclv the [linked as duplicate](https://stackoverflow.com/questions/14858476/multiply-two-overflowing-integers-modulo-a-third) is about the C language, and the [question linked](https://stackoverflow.com/questions/17283192/how-to-calculate-abc) in your previous comment is about the C++ language, which makes a case for neither one of them being duplicate of this question. Can we assume that someone who knows C#, can translate any C or C++ code to C#? These are different languages. – Theodor Zoulias Oct 23 '21 at 06:17
  • Does `(modulo - 1) * (modulo - 1)` overflow 64 bits? If not then you can simply do `((a % modulo) * (b % modulo)) % modulo` – Charlieface Oct 23 '21 at 20:24
  • @Charlieface yes – Rubiksmaster02 Oct 23 '21 at 20:26

2 Answers2

2

Here is a reasonably fast implementation that uses the Math.BigMul method (.NET 5 and later), and the decimal data type:

/// <summary> Returns (a * b) % modulo </summary>
public static ulong ModuloMultiplication(ulong a, ulong b, ulong modulo)
{
    ulong high = Math.BigMul(a, b, out var low);
    if (high == 0) return low % modulo;
    if (high <= 0xFFFFFFFF) return (ulong)(((decimal)a * b) % modulo);
    decimal remainder = high;
    remainder *= 0x100000000;
    remainder += low >> 32;
    remainder %= modulo;
    remainder *= 0x100000000;
    remainder += low & 0xFFFFFFFF;
    remainder %= modulo;
    Debug.Assert((BigInteger)remainder == ((BigInteger)a * b) % modulo); // Validation
    return (ulong)remainder;
}

This implementation performs the operation in two steps, in order to overcome the 96 bit limitation of the decimal type. It's almost 2 times faster than using the BigInteger type, and it doesn't allocate memory. In case the product of the two numbers fits in the decimal range, the result is obtained in one step.

For a similar but slightly slower implementation that doesn't depend on the Math.BigMul method (source code), you can look at the 4th revision of this answer.


Update: The (a * b) % c operation could be even faster if you could use a native UInt128 type. Unfortunately this type does not exist. If you are feeling adventurous you could use the Dirichlet .NET Number Theory Library by Rick Sladkey (MIT License), that includes this type (along with the Int128). You could just download the UInt128.cs code file, and include it in your project. Then you could do this:

public static ulong ModuloMultiplication(ulong a, ulong b, ulong modulo)
{
    return (new Dirichlet.Numerics.UInt128(a) * b) % modulo;
}

It's around 2.5 times faster than the BigInteger in my PC.

This code file has ~2.700 lines of code. if you are interested only in the (a * b) % c functionality, you could probably strip it down to 150 lines or so, if you think it's worth the effort.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • thank you for the answer but i stated in the question that i am not going to use BigInteger. If i was going to i would just do (ulong)(((BigInteger)a * b) % c) – Rubiksmaster02 Oct 23 '21 at 18:56
  • @Rubiksmaster02 the second implementation is not based on `BigInteger`s. Is it a requirement that `decimal`s cannot be used as well? – Theodor Zoulias Oct 24 '21 at 01:31
1

Answer found and adapted from https://stackoverflow.com/a/18680280/14133865

public static ulong ModMul(ulong a, ulong b, ulong modulus)
{
    ulong result = 0UL;

    if (b >= modulus)
    {
        if (modulus > 0x7FFFFFFFFFFFFFFF)
        {
            b -= modulus;
        }
        else
        {
            b %= modulus;
        }
    }

    while (a != 0UL)
    {
        if ((a & 1UL) != 0UL)
        {
            if (b >= modulus - result)
            {
                result -= modulus;
            }

            result += b;
        }

        a >>= 1;

        ulong temp = b;

        if (b >= modulus - b)
        {
            temp -= modulus;
        }

        b += temp;
    }

    return result;
}
  • Is this solution faster than the simple `(ulong)(((BigInteger)a * b) % c)`? – Theodor Zoulias Oct 24 '21 at 01:35
  • I probably should have actually tested that, lol. – Rubiksmaster02 Oct 24 '21 at 03:15
  • Tl;dr: it depends, sometimes its slower sometimes its faster, just deps on the values – Rubiksmaster02 Oct 24 '21 at 03:24
  • Rubiksmaster02 hmm, isn't this a problem? The question has clearly set the bar, by stating that the `BigInteger` is horribly slow. So I would assume that anything not perceptibly faster than the `BigInteger`-based solution would be unacceptable. Unless you dislike the `BigInteger` because it [allocates memory](https://github.com/dotnet/runtime/issues/29378 "Expose a BigInteger.Builder to help avoid so many copies/allocations"), and not because it's slow. – Theodor Zoulias Oct 24 '21 at 03:32
  • 1
    Yes, i got a little ahead of myself but you are definitely right. (i did lazy tests as well, ill make some more thorough ones right now) My original basis for slowness was modpow not just bigint in general (although the code is cleaner without bigint) – Rubiksmaster02 Oct 24 '21 at 03:38
  • I literally dont know what to think of it, a lot of the time when i test single values my solution wins, but when i test a collection of values, yours wins @TheodorZoulias – Rubiksmaster02 Oct 24 '21 at 03:51
  • Rubiksmaster02 in that case you could choose either implementation, and it wouldn't make much of a difference. Just throw a coin, or select the one that is more cute. – Theodor Zoulias Oct 24 '21 at 03:55
  • There had to be a BigInteger speed up update in .net 6 or something like that, because i remember it being extremely slower. – Rubiksmaster02 Oct 24 '21 at 03:56
  • Rubiksmaster02 it's slow indeed when you are dealing with very large numbers (thousands of digits), and then you have to print the number in decimal format. The `ToString` [is slow](https://stackoverflow.com/questions/56088387/efficiently-convert-large-biginteger-to-string-tostring-takes-too-long). – Theodor Zoulias Oct 24 '21 at 04:02