No operator += for Atomic<float> (—>std::atomic<float>)?


#1

After having used Atomics quite a bit here and there, today I got an irritating compiler error about std::atomic not having any viable overloaded operator += (I was pretty sure having used this before)

Is it true that there is no such operator for float and if so, for what reason? Google did not help me finding any hint in this direction. If it should normally work, what could have gone wrong? Code for reproduction is as easy as follows

Atomic<float> a (10.5f);
a += 0.1f;

#2

http://en.cppreference.com/w/cpp/atomic/atomic/operator_arith2

member only of atomic<Integral>(C++11) and atomic<Floating>(C++20) template specializations


#3

Alright, thank you for the quick reply, I obviously overlooked this line. So I will wait until C++20 to get this feature… :smiley:

But for what reason is this currently unsupported? Can’t imagine why such an operation should not be possible…


#4

I remember @timur talking about floats and atomic, it seems to difficult, since comparison in float domain can be ambiguous, which seems to be a problem for that implementation (I can’t remember why though).

See in this talk: https://www.youtube.com/watch?v=2vmXy7znEzs

TL;DR at ~11 mins


#5

As Timur says in his talk then strictly speaking this has a number of pitfalls. (Which is probably why — TL;DR — it will have to wait until C++20.)

But if you’re sure that your floats will not throw exceptions, maybe keep them within ±INF and not be NaNs then you could roll your own.

Something like this:

static std::atomic<float>& operator+= (std::atomic<float>& atomicFloat, float increment)
{
    float oldValue;
    float newValue;
    
    do
    {
        oldValue = atomicFloat.load (std::memory_order_relaxed);
        newValue = oldValue + increment;
    } while (! atomicFloat.compare_exchange_weak (oldValue, newValue,
                                                  std::memory_order_release,
                                                  std::memory_order_relaxed));
    return atomicFloat;
}

Then some test code that fires up a bunch of threads which each increment the atomic value a number of times:

int main (int argc, char* argv[])
{
    constexpr int   numThreads = 16;
    constexpr int   numIters   = 1000;
    constexpr float increment  = 0.1f;
    
    float nonAtomicValue (0.0f);

    for (int i = 0; i < numThreads; ++i)
        for (int j = 0; j < numIters; ++j)
            nonAtomicValue += increment;
    
    std::cout << "single thread nonAtomicValue = " << nonAtomicValue << "\n";

    nonAtomicValue = 0.0f; // reset
    
    std::atomic<float> atomicValue (0.0f);

    std::vector<std::thread> threads;
    
    for (int i = 0; i < numThreads; ++i)
    {
        threads.emplace_back ([&]()
        {
            for (int j = 0; j < numIters; ++j)
            {
                atomicValue += increment;    // safe for most values atomicValue might hold
                nonAtomicValue += increment; // unsafe

                // deliberately throw the threads out of sync
                std::this_thread::sleep_for (std::chrono::nanoseconds (i * j + 1));
            }
        });
    }
    
    for (auto& thread : threads)
        thread.join();
    
    std::cout << "multithread nonAtomicValue   = " << nonAtomicValue << "\n";
    std::cout << "multithread atomicValue      = " << atomicValue.load (std::memory_order_relaxed) << "\n";
    
    return 0;
}

Example output:

single thread nonAtomicValue = 1599.76
multithread nonAtomicValue   = 1599.16
multithread atomicValue      = 1599.76

The “multithread nonAtomicValue” value will almost always be incorrect.

The issue Timur mentions about the problems with float precision and the compare_exchange_xxx() functions should be avoided here as the bit pattern of the oldValue float should be identical to the contents of the atomic when the comparisons performed by compare_exchange_weak(). Of course we’re in trouble if oldValue is a NaN, since comparing NaNs always returns false! I deliberately picked a value of 0.1 in the code above as that is not perfectly representable as a float.

A simple example of the kind of trouble you can get in with comparing equality using floats is:

int main (int argc, char* argv[])
{
    float a = 0.0f, b = 0.1f;
    
    a += 0.01f;
    a += 0.01f;
    a += 0.01f;
    a += 0.01f;
    a += 0.01f;
    a += 0.01f;
    a += 0.01f;
    a += 0.01f;
    a += 0.01f;
    a += 0.01f;

    std::cout << "a = " << a << "\n";
    std::cout << "b = " << b << "\n";

    std::cout << "a==b = " << (a == b ? 1 : 0) << "\n";

    return 0;
}

Where a and b are both printed as being “0.1” but also shown to be unequal!


#6

Great, such a detailed answer. I’ll work through it step by step!