2

I'm trying to perform Modular Exponentiation for large values (upto 64-bits) and I wrote this function for it:

uint64_t modularExp(uint64_t num, uint64_t exp, uint64_t mod) 
{
    string expBits = bitset<64>(exp).to_string();   
    expBits = expBits.substr(expBits.find("1")+1);
    
    string operations = "";
    
    uint64_t result = num;
    for (int i = 0; i < expBits.length(); ++i)
    {
        result = (uint64_t)pow(result, 2) % mod;
        if (expBits[i] == '1')
            result = (result * num) % mod;
    }   
            
    return result;  
}

This works good with small numbers (8 digits or less) but for large numbers, even though they're in the 64 bit range, the result comes out wrong.

Additionally, when the value of mod exceeds 4294967296 (Max 32 bit value), the result just comes out as zero. I suspect the pow function perhaps has a role to play in this issue but I can't figure it out for sure.

Any advice would be greatly appreciated.

  • 2
    the square of a large number is likely to exceed the maximum that can be stored in 64 bits. (If you square a number it'll get about twice as long. Hence squaring a 34 bit number will need 67 bits to store the result) – Ronald Aug 19 '20 at 14:50
  • Is there a workaround for this? I'm not aware of any Java BigInteger equivalent for C++ that allows for above 64-bit airthmetic. – Maurice Kasomwung Aug 19 '20 at 15:01

1 Answers1

2

First of all, some general advice:

  • It's better not to use strings when working with integers, as operations with strings are much slower and might become a bottleneck for performance. It's also less clear what is actually being done when strings are involved.
  • You shouldn't use std::pow with integers, because it operates on floating-point numbers and loses precision.

For the main question, as a workaround, you can use this O(log^2(n)) solution, which should work for arguments up to 63 bits (since it only ever uses addition and multiplication by 2). Note how all that string magic is unnecessary if you just iterate over the bits in small-to-large order:

#include <cstdint>

uint64_t modular_mul(uint64_t a, uint64_t b, uint64_t mod) {
    uint64_t result = 0;
    for (uint64_t current_term = a; b; b >>= 1) {
        if (b & 1) {
            result = (result + current_term) % mod;
        }
        current_term = 2 * current_term % mod;
    }
    return result;
}

uint64_t modular_pow(uint64_t base, uint64_t exp, uint64_t mod) {
    uint64_t result = 1;
    for (uint64_t current_factor = base; exp; exp >>= 1) {
        if (exp & 1) {
            result = modular_mul(result, current_factor, mod);
        }
        current_factor = modular_mul(current_factor, current_factor, mod);
    }
    return result;
}

Also, in gcc a (non-standard) __uint128_t is available for some targets. (which can be used to replace modular_mul with normal multiplication)

shananton
  • 508
  • 2
  • 12