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 int
s 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 double
s 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, like0.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.