Search code examples
cperformanceoptimizationtime-complexitymultiplication

correct O(n log n) time complexity for multiplying two integers in C on modern x86 CPUs?


Consider the following C-Code on modern day Intel or AMD x86_64 hardware, where the datatype int has 32 bits:

// calc x times y for any two integers values of x and y (where the result can be stored by the int datatype)
int fastMult(int x, int y) {
    /*
     * Assuming the following operators take O(n) or O(1) time:
     * ==, <, >, &&, ||, &, |, >>, -, +, ?:
     */

    // x*0 == 0*y == 0
    if (y == 0 || x == 0) return 0;

    // (-x)(-y) == xn and (-x)y == x(-y)
    if (x < 0) return fastMult(-x, -y);

    int isNegative = y < 0; // x cannot be negative here

    // y*x is faster than x*y for bigger absolute y than x
    if (isNegative && x < -y || x < y) return fastMult(y, x);
    if (isNegative) y = -y; // handle y in a simpler way

    int res = fastMult(x, y >> 1); // called at max lb(y) times aka sizeof(y) times
    res = res + res; // one addition
    if (y & 1) res = x + res; // possible second addition

    // if y was negative, then the answer is negative
    return isNegative ? -res : res;
}

If we disregard the recursive step, the slowest operation in this code, besides the branches by the ifs, would be the + operation. This operation can still be executed within a single clock cycle by the CPU. So even if adding two 32 bit integer takes basically O(1) time in our hardware, we should still call it O(n) for bigint operations where a bigint is an int with n-bits.

Having said this, the base case for this recursive implementation of multiplying two numbers stops at y == 0. The function calls itself (besides the very first times when it might switch x and y and change some signs) at max 32 times, since it calls itself with the parameter of y as y >> 1. This operation shifts the bits by one to the left, until all the bits are 0, which, for an 32 bit int, can be at max y >> 32.

Does this mean it is an O(n log n) algorithm for multiplying two integers together (since it would also work for bigints with the same time complexity).

Why O(n log n)? We are doing multiple O(1) and O(n) calculations when calling this function once. Since we call this function up to O(log n) times, we multiply O(log n) with O(n) and come to O(n log n). At least thats my understanding of this.

I am unsure about that, since all the usual methods of multiplying two integers take O(n*n) steps and there are only very few and complex algorithms which are faster than that, see: https://www.wikiwand.com/en/Multiplication_algorithm

Simpler version of this code for unsigned integers:

// x * y for unsigned integers of x and y
int fastMult(int x, int y) {
    if (y == 0) return 0;

    int res = fastMult(x, y >> 1);
    res <<= 1; // O(1) or O(n), doesnt matter since O(n) is below this line and 2 times O(n) is still O(n)
    if (y & 1) res += x; // O(n)

    return res;
}

Solution

  • You're problem seems to be a confusion between n and x and y, which causes you to mistakenly think you're doing something log(n) times, when you are actually doing it n times. So to be clear

    • n is the (maximum) number of digits/bits in the numbers you are multiplying. This is the normal value of interest for talking about the complexity of arithmetic operation

    • x and y are the values you are mulitplying. So each of them has up to n digits.

    So when you recurse with a shift y >> 1 you're halving y, not n. This only reduces y by one bit (reducing n by one), so you end up recursing O(n) times, not O(log(n))