2

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?

Omnifarious
  • 54,333
  • 19
  • 131
  • 194

2 Answers2

4

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.

Acorn
  • 24,970
  • 5
  • 40
  • 69
  • 1
    The definition of a 'gnu::const' function is that it must only depend on its arguments. The definition of a `gnu::pure` function only requires that it not modify global state. Am I misunderstanding the definition. If so, can you point it out using the documentation? – Omnifarious Feb 22 '19 at 04:51
  • @Omnifarious Yes, you are misunderstanding it. The documentation says that you cannot depend on volatile objects, i.e. memory that is changing. The whole point of a `pure` function is to perform CSE -- if you return different values every time, you cannot do CSE! – Acorn Feb 22 '19 at 05:00
  • @Acorn - The whole point of CSE is that it's magic the compiler does without you having to think about it. So I don't think talking about CSE is a useful way to explain what functions can be pure. But you are indeed correct, the documentation does mention `volatile`. I was confused because people hardly ever mention this aspect when talking about it, for example in this question: https://stackoverflow.com/questions/29117836/attribute-const-vs-attribute-pure-in-gnu-c/48051050 – Omnifarious Feb 22 '19 at 05:14
  • @Omnifarious Marking a function as `pure`/`const` is purely an optimization hint to the compiler precisely so that it can perform CSE. It is even explained that way in the GCC docs in the very first sentences of the `pure`/`const` GCC docs. Not only that, in my opinion, it is easier to understand that way. – Acorn Feb 22 '19 at 05:19
  • @Acorn - What is CSE? how can I tell if the compiler will apply it in any given situation? It's not at all obvious, the analysis the compiler does to figure out if it can do CSE can be very complex, and nobody can be expected to understand what that means. But, I edited your answer into being something that gives a clear criteria for determining whether a function can be pure without having to reference CSE. And, as a bonus, points out where I misread the documentation. – Omnifarious Feb 22 '19 at 05:30
  • @Omnifarious There is no "complex analysis" going on. If a function isn't marked as `pure`, CSE cannot be performed, period. The point is that if you do not understand how these optimization hints work, you shouldn't try to be using them, because the risk of introducing subtle bugs is high, as you have seen (and as your linked question also proves). The same applies to all optimizations hints out there. And no, they are not "*magic the compiler does*". – Acorn Feb 22 '19 at 05:45
  • As for the edits, please do not edit answers of other people specially if you are new to the topic: you have not given clear criteria -- you will confuse other people further. – Acorn Feb 22 '19 at 05:46
  • You asked a question and had it answered properly (even another user acknowledged so, although somehow the comment has disappeared). You then show you misunderstand the fine details of CSE and the topic in general. Yet you seem entitled to change the answer from an expert and, if your edit is not agreed upon, then you downvote a clearly correct answer just to post yours. Sorry, that won't fly, and I am reporting your toxic behavior. – Acorn Feb 22 '19 at 17:07
  • "In general, avoid using optimization hints unless they are well understood!" But sometimes using things and making mistakes you understand them. So you come here, research further, etc. You can't have a fear attitude when programming, especially in C++. He needed a solution for his problem, not a lecture on being a "responsible programmer". – chila May 05 '20 at 15:53
  • @chila You are reading way too much into a sentence that is simply telling newcomers to be wary. I have seen people use hints in production code that triggered long, painful debugging sessions because they thought they were harmless since the compiler didn't complain. In fact, if we should "fear" languages, I'd say C++ would be the prime one, considering the dozens of ways to shoot yourself in the foot that it gives us. It is, after all, a language for experts. – Acorn May 05 '20 at 17:35
-1

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();
Omnifarious
  • 54,333
  • 19
  • 131
  • 194
  • So you ended up explaining `pure` by mentioning CSE, just as I explained to you how it should be done -- yet you called my answer "wrong". :-) – Acorn Feb 22 '19 at 17:31
  • "*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.*" No, that is not the criteria. It is an oversimplification, even taking into account the "volatile" part. – Acorn Feb 22 '19 at 17:33
  • "*The circumstances under which a compiler can perform this optimization are many, varied, and often surprising*" Please do not attribute "magic" behavior (as you called it in comments) to a compiler. It does not help to talk about optimizers that way -- it does not provide knowledge. It is very clear when CSE cannot happen, and that is the whole point of `pure`/`const`. – Acorn Feb 22 '19 at 17:38
  • @Acorn - CSE is actually nearly irrelevant to this answer. I don't care what CSE is or isn't or how magical it is. What I care about is precisely what criteria, exactly, without reference to any kind of internal compiler decisions, are relevant for deciding whether or not it's valid for a function to be market with the `pure` attribute. I removed the reference to CSE because it's a distraction from the actual answer. – Omnifarious Feb 22 '19 at 23:11