Search code examples
c++mathmatrixeigen

Copying a MatrixXf causes Eigen math to fail. Release and Debug versions provide different results


I encountered an error when creating a copy of a matrixxf which I used to get the rowwise average. It seems to generate exceedingly large values in debug, and a value of zero in release. In both cases, the answer is wrong as it the average is 0.5. If I don't use an extra line where I create a copy of inputs, the error disappears and the output is as expected. Why does this occur?

I have created a minimal example in cpp. I compile it in vs2022 using cl.exe with cmake.

int main() {
    auto startTime = std::chrono::high_resolution_clock::now();
    #ifdef NDEBUG
        std::cout << "Release Build" << std::endl;
    #else
        std::cout << "Debug Build" << std::endl;
    #endif

    std::cout << "Eigen world version: " << EIGEN_WORLD_VERSION << "\n";
    std::cout << "Eigen major version: " << EIGEN_MAJOR_VERSION << "\n";
    std::cout << "Eigen minor version: " << EIGEN_MINOR_VERSION << "\n";
    std::cout << "cpp version: " << __cplusplus << "\n";
    
    Eigen::setNbThreads(1);
    std::cout << "Eigen threads: " << Eigen::nbThreads() << "\n";

    Eigen::MatrixXf inputs(2, 4);
    inputs <<
        1, 0, 1, 0,
        1, 1, 0, 0;

    Eigen::MatrixXf calcNextLine = inputs;
    calcNextLine = calcNextLine.rowwise().mean();
    Eigen::MatrixXf calcSameLine = inputs.rowwise().mean();

    std::cout << "\ninputs = \n" << inputs << "\n";
    std::cout << "\ncalcNextLine = \n" << calcNextLine << "\n";
    std::cout << "\ncalcSameLine = \n" << calcSameLine << "\n";

    auto currentTime = std::chrono::high_resolution_clock::now();
    auto deltaTime = std::chrono::duration_cast<std::chrono::nanoseconds>(currentTime - startTime).count();
    std::cout << "\nFinished in " << (deltaTime / 1.0e6) << " milliseconds.\n";

    return 0;
}

Console output in debug:

Debug Build
Eigen world version: 3
Eigen major version: 4
Eigen minor version: 0
cpp version: 199711
Eigen threads: 1

inputs =
1 0 1 0
1 1 0 0

calcNextLine =
-1.07901e+08
-1.07901e+08

calcSameLine =
0.5
0.5

Finished in 4.2896 milliseconds.

Console output in release:

Release Build
Eigen world version: 3
Eigen major version: 4
Eigen minor version: 0
cpp version: 199711
Eigen threads: 1

inputs =
1 0 1 0
1 1 0 0

calcNextLine =
0
0

calcSameLine =
0.5
0.5

Finished in 3.6746 milliseconds.

Solution

  • If I don't use an extra line where I create a copy of inputs, the error disappears and the output is as expected. Why does this occur?

    Here is what's happening in this particular line:

    calcNextLine = calcNextLine.rowwise().mean();
    
    1. Most Eigen operations create proxy objects so that their evaluation can be stringed together into a single evaluation without allocating temporary memory. So rowwise().mean() creates an object representing the partial reduction which references the calcNextLine matrix.
    2. You assign this expression object to calcNextLine itself, invoking its assignment operator
    3. The assignment operator sees that the assigned object has a different size (since you reduced the rows), so it re-allocates to the new size
    4. This releases the memory which the rowwise mean expression object references. This creates a dangling pointer (or maybe an out-of-bounds access using the original size for the new allocation, I would have to check the source for that one)
    5. When the assignment operator evaluates the mean into its newly allocated array, undefined behaviour is invoked. Anything could happen

    I'm pretty sure the Eigen documentation page on aliasing issues explains this stuff but the website is unavailable at the moment (again). In general, having an alias between the left and right side of an assignment in Eigen is only okay if:

    1. The right side is a matrix multiplication, except with left_side.noalias() = right_side, of course
    2. The right side is a simple element-wise operation without size changes and it's a direct alias, not overlapping in any other way. This is okay: a = a + b. This is not okay: a.topRows(N) = a.middleRows(1, N) + b

    Anyway, how do you fix this? Two options:

    1. Use eval() to create a new vector before assigning it to its input
    calcNextLine = calcNextLine.rowwise().mean().eval();
    
    1. Use a separate variable. Since the output of the rowwise mean is a vector, assigning it to a matrix type leads to inefficient code, anyway