Dot product of 2 vectors C++

The (first) problem

This is the function template for the inner product in <numeric>:

template <class InputIterator1, class InputIterator2, class T>
   T inner_product (InputIterator1 first1, InputIterator1 last1,
                    InputIterator2 first2, T init);

Notice that what defines the type T of the output is the init parameter. So, given your input:

std::inner_product(x.begin(), x.end(), y.begin(), 0);

init = 0, therefore the type T is int. So, when the algorithm runs, it will typecast the double values into ints which, ultimately, will return an undefined int value.

A "fix" and the second problem

To fix the problem, all you have to do is give a properly-typed init value(that is, give a double as the init parameter). Just 0.0 will do:

std::inner_product(x.begin(), x.end(), y.begin(), 0.0);

Now, when you compile and run the program with that fix, it will still output an incorrect result: 0

This is because when the inner_product function accumulates the values, it does so using standard double addition. Therefore, you are subject to standard double imprecision, which has a machine epsilon of 2^(-52) — 2.22E-16 or about an imprecision in the sixteenth decimal place — which implies, for the number 1E20, that (1E20 + x) = 1E20 for all x < 2^(-52)*1E20 ≈ 22204.46.

To illustrate the point, let's add 1E20 + 23000 in the python interpreter(reminder that python uses IEEE-754 floating point arithmetic, which is equal to the precision of double in a standard C++ compiler):

>>> 1e20 + 23000
1.0000000000000002e+20

So you see that anything less than twenty thousand was ignored/"absorbed" in the addition.

Since your other numbers are less than 22204.46, the 1e20 will just "absorb" them until it gets added to -1E20, which will then "cancel out" and return 0.

The (easy) fix

The easiest way to fix this second problem is to use long double instead of double. This more precise double-precision type has a machine epsilon of 2^(-63) — 1.08E-19 or about nineteeen decimal places — which means that, for your input 1E20, the imprecision will be equal to 2^(-63)*1E20, or about 10.84. Running the program, the output will be -4000, which is quite close to the expected answer. But that's probably not what your professor expects, since he specifically requests the output to be precisely -4000.4.

Note: obviously, you could go for another, more precise numeric type, but your professor probably expects you to use double, so I won't go into detail in that.

Edit: as @phuclv mentioned in the comments, some compilers don't implement long double as 80-bit floating-point values, but instead may have the same precision as a double(64-bit). So you might have to look for libraries that provide proper 80-bit precision long doubles or even 128-bit IEEE-754 quadruple-precision floating point types. Though that definitely would not be considered "easy".

The (mostly correct) fix

Well, you can't be infinitely precise, because the double type has epsilon = 2^(-52), but you can be smarter in the addition, without just adding big values to small ones(remember: the big values "absorb" small ones because of imprecision in the double floating-point arithmetic). Basically, you should calculate an array that has the pairwise multiplication of the values, then sort it(based on the absolute value) then add the values using std::accumulate:

#include <iostream>
#include <numeric>
#include <vector>
#include <functional>
//Mind the use of these two new STL libraries
#include <algorithm> //std::sort and std::transform
#include <cmath> //abs()



int main(){

    std::vector<double> x{1.0e20, -1.0e3, 0.1, 1.0e20};
    std::vector<double> y{1.0, 4.0, -4.0, -1.0};
    //The vector with the pairwise products
    std::vector<double> products(x.size());

    //Do element-wise multiplication
    //C code: products[i] += x[i] * y[i];
    std::transform(x.begin(), x.end(), y.begin(), products.begin(), std::multiplies<double>());

    //Sort the array based on absolute-value
    auto sort_abs = [] (double a, double b) { return abs(a) < abs(b); };
    std::sort(products.begin(), products.end(), sort_abs);

    //Add the values of the products(note the init=0.0)
    double result = std::accumulate(products.begin(), products.end(), 0.0);

    std::cout << result << std::endl;
    return 0;
}

With this new code, the result is as expected: -4000.4

Tough it obviously has it's limitations. For example, if the input was the vectors v1 = {100.0, 1E20} and v2 = {10.0, 1.0}, which should return 100000000000000001000 as a result, will obviously just return 1E20.


There are a logical error and some numeric issues in the posted snippet.

  • std::inner_product Initializes the accumulator with the initial value passed, so it uses the same type for it a and for the returned value. The posted code uses an integer, 0, while a floating point value, like 0.0 should be used.
  • The values in the vectors have an extremely wide range of magnitudes. A floating point type like double has a finite precision, it can't represent every possible real number without rounding errors. Also (and because of that) floating point math operations are not associative and sensitive to the order in which they are performed.

To picture it, you can run the following snippet.

#include <numeric>
#include <algorithm>
#include <array>
#include <fmt/core.h> // fmt::print

int main()
{
    using vec4d = std::array<double, 4>;
    
    vec4d x{1.0e20, 1.0e20, -1.0e3, 0.1};
    vec4d y{1.0, -1.0, 4.0, -4.0};
    
    vec4d z;
    std::transform( std::begin(x), std::end(x), std::begin(y), std::begin(z)
                  , std::multiplies<double>{} );
    std::sort(std::begin(z), std::end(z));

    fmt::print("{0:>{1}}\n", "sum", 44);
    fmt::print("{0:->{1}}", '\n', 48);
    do {
        for (auto i : z) {
            fmt::print("{0:8}", i);
        }
        auto sum{ std::accumulate(std::begin(z), std::end(z), 0.0) };
        fmt::print("{0:{1}.{2}f}\n", sum, 14, 1);
    } while ( std::next_permutation(std::begin(z), std::end(z)) );
}

Here is its output:

                                         sum
-----------------------------------------------
  -1e+20   -4000    -0.4   1e+20           0.0
  -1e+20   -4000   1e+20    -0.4          -0.4
  -1e+20    -0.4   -4000   1e+20           0.0
  -1e+20    -0.4   1e+20   -4000       -4000.0
  -1e+20   1e+20   -4000    -0.4       -4000.4
  -1e+20   1e+20    -0.4   -4000       -4000.4
   -4000  -1e+20    -0.4   1e+20           0.0
   -4000  -1e+20   1e+20    -0.4          -0.4
   -4000    -0.4  -1e+20   1e+20           0.0
   -4000    -0.4   1e+20  -1e+20           0.0
   -4000   1e+20  -1e+20    -0.4          -0.4
   -4000   1e+20    -0.4  -1e+20           0.0
    -0.4  -1e+20   -4000   1e+20           0.0
    -0.4  -1e+20   1e+20   -4000       -4000.0
    -0.4   -4000  -1e+20   1e+20           0.0
    -0.4   -4000   1e+20  -1e+20           0.0
    -0.4   1e+20  -1e+20   -4000       -4000.0
    -0.4   1e+20   -4000  -1e+20           0.0
   1e+20  -1e+20   -4000    -0.4       -4000.4
   1e+20  -1e+20    -0.4   -4000       -4000.4
   1e+20   -4000  -1e+20    -0.4          -0.4
   1e+20   -4000    -0.4  -1e+20           0.0
   1e+20    -0.4  -1e+20   -4000       -4000.0
   1e+20    -0.4   -4000  -1e+20           0.0

Note that the "correct" answer, -4000.4, only occurs when the bigger terms (1e+20 and -1e+20) cancel out in the first summation. This is an artifact due to the particular numbers chosen as inputs, where the two biggest ones are equal in terms of magnitude and also have opposite sign. In general subctracting two numbers that are almost the some leads to catastrophic cancellation and loss of significance.

The next best result, -4000.0, happens when the smaller value in terms of magnitude, 0.4, is "near" the biggest ones and it's canceled out.

Various techiniques can be adopted to reduce the amount of growing numerical errors when summing many terms, like pairwise summation or compensated summation (see e.g. Kahan summation).

Here, I tested Neumaier summation with the same samples.