Optimization problem with [[gnu::pure]] function attribute and threads

972 Views Asked by At

I have a program which nearly immediately finishes with -O0 on gcc, but hangs forever with gcc and -O3. It also exits immediately if I remove the [[gnu::pure]] function attribute, even though the function does not modify global state. The program is in three files:

thread.hpp

#include <atomic>

extern ::std::atomic<bool> stopthread;

extern void threadloop();
[[gnu::pure]] extern int get_value_plus(int x);

thread.cpp

#include <thread>
#include <atomic>
#include "thread.hpp"

namespace {
::std::atomic<int> val;
}

::std::atomic<bool> stopthread;

void threadloop()
{
   while (!stopthread.load())
   {
      ++val;
   }
}

[[gnu::pure]] int get_value_plus(int x)
{
   return val.load() + x;
}

main.cpp

#include <thread>
#include "thread.hpp"

int main()
{
   stopthread.store(false);
   ::std::thread loop(threadloop);

   while ((get_value_plus(5) + get_value_plus(5)) % 2 == 0)
      ;
   stopthread.store(true);
   loop.join();
   return 0;
}

Is this a compiler bug? A lack of documentation for the proper caveats to using [[gnu::pure]]? A misreading of the documentation for [[gnu::pure]] such that I've coded a bug?

2

There are 2 best solutions below

4
On BEST ANSWER

As it turns out, I misread the documentation. From the online documentation about the pure attribute in gcc:

The pure attribute prohibits a function from modifying the state of the program that is observable by means other than inspecting the function’s return value. However, functions declared with the pure attribute can safely read any non-volatile objects, and modify the value of objects in a way that does not affect their return value or the observable state of the program.

and a different paragraph:

Some common examples of pure functions are strlen or memcmp. Interesting non-pure functions are functions with infinite loops or those depending on volatile memory or other system resource, that may change between consecutive calls (such as the standard C feof function in a multithreading environment).

These two paragraphs make it clear that I've been lying to the compiler, and the function I wrote does not qualify as being 'pure' because it depends on a variable that might change at any time.

The reason I asked this question is because the answers to this question: __attribute__((const)) vs __attribute__((pure)) in GNU C didn't address this problem at all (at the time I asked my question anyway). And a recent C++ Weekly episode had a comment asking about threads and pure functions. So it's clear there's some confusion out there.

So the criteria for a function that qualifies for this marker is that it must not modify global state, though it is allowed to read it. But, if it does read global state, it is not allowed to read any global state that could be considered 'volatile', and this is best understood as state that might change between two immediately successive calls to the function, i.e. if the state it's reading can change in a situation like this:

f();
f();
10
On

I have a program which nearly immediately finishes with -O0 on gcc, but hangs forever with gcc and -O3

Yes, because the program gets compiled down to an infinite loop when optimizations are enabled.

Is this a compiler bug? A lack of documentation for the proper caveats to using [[gnu::pure]]? A misreading of the documentation for [[gnu::pure]] such that I've coded a bug?

It isn't a compiler bug. get_value_plus is not a pure function:

[[gnu::pure]] int get_value_plus(int x)
{
    return val.load() + x;
}

since the return value can change at any time (for the same x), because val is expected to be modified by the other thread.

The compiler, however, thinking that get_value_plus will always return the same value, will perform CSE and therefore will assume this:

while ((get_value_plus(5) + get_value_plus(5)) % 2 == 0);

can be written as:

int x = get_value_plus(5);
while ((x + x) % 2 == 0);

Which, indeed, it is an infinite loop regardless of the value of x:

while (true);

Please see the GCC documentation on pure for more details.

In general, avoid using optimization hints unless they are well understood!

In this case, the misunderstanding is that pure functions are allowed to read global memory, but not if that memory is changing from call to call by someone else than the caller:

However, functions declared with the pure attribute can safely read any non-volatile objects, and modify the value of objects in a way that does not affect their return value or the observable state of the program.