1
class Foo{
public:
    void fetch(void)
    {
        int temp=-1;
        someSlowFunction(&temp);
        bar=temp;
    }
    int getBar(void)
    {
        return bar;
    }
    void someSlowFunction(int *ptr)
    {
        usleep(10000);
        *ptr=0;
    }
private:
    int bar;
};

I'm new to atomic operations so I may get some concepts wrong.

Considering above code, assuming loading and storing int type are atomic[Note 1], then getBar() could only get the bar before or after a fetch().

However, if a compiler is smart enough, it could optimize away temp and change it to:

    void Foo::fetch(void)
    {
        bar=-1;
        someSlowFunction(&bar);
    }

Then in this case getBar() could get -1 or other intermediate state inside someSlowFunction() under certain timing conditions.

Is this risk possible? Does the standard prevent such optimizations?


Note 1: http://preshing.com/20130618/atomic-vs-non-atomic-operations/

The language standards have nothing to say about atomicity in this case. Maybe integer assignment is atomic, maybe it isn’t. Since non-atomic operations don’t make any guarantees, plain integer assignment in C is non-atomic by definition.

In practice, we usually know more about our target platforms than that. For example, it’s common knowledge that on all modern x86, x64, Itanium, SPARC, ARM and PowerPC processors, plain 32-bit integer assignment is atomic as long as the target variable is naturally aligned. You can verify it by consulting your processor manual and/or compiler documentation. In the games industry, I can tell you that a lot of 32-bit integer assignments rely on this particular guarantee.

I'm targeting ARM Cortex-A8 here, so I consider this a safe assumption.

user3528438
  • 2,737
  • 2
  • 23
  • 42
  • If you need sychornization then use a [`std::mutex`](http://en.cppreference.com/w/cpp/thread/mutex) or [`std::atomic`](http://en.cppreference.com/w/cpp/atomic/atomic) – NathanOliver Sep 30 '15 at 14:57
  • _"assuming loading and storing int type are atomic"_ When is that true? – Lightness Races in Orbit Sep 30 '15 at 14:57
  • _"then `getBar()` could only get the bar before or after a `fetch()`"_ Not true. – Lightness Races in Orbit Sep 30 '15 at 14:58
  • _"if a compiler is smart enough, it could optimize away temp and change it to:"_ Also not true. – Lightness Races in Orbit Sep 30 '15 at 14:58
  • This code is as thread safe as it gets - there are no threads there, no synchronization, no nothing. To get a real answer, I suggest you elaborate on threaded usage here. – SergeyA Sep 30 '15 at 15:01
  • @LightnessRacesinOrbit : Why do you say that the compiler won't optimise away temp? Is it because it is being called by reference in the next function, or something else in general? – therainmaker Sep 30 '15 at 15:05
  • @LightnessRacesinOrbit, I do not see why compiler could not perform specified optimization. It's result would be indistinguishable from original sequence, and as such, allowed. – SergeyA Sep 30 '15 at 15:05
  • @SergeyA My thread usage is simple: `fetch()` and `getBar()` are called from different threads. `getBar()` doesn't have to get the latest version of `bar`, because in this use case it just need to get a "sufficiently up-to-date" one. I just don't want it to get garbage values. – user3528438 Sep 30 '15 at 15:09
  • @SergeyA: No, not really. There are aliasing concerns. You're passing a _pointer_. Your compiler can't just swap that out for a different pointer whenever it wants. – Lightness Races in Orbit Sep 30 '15 at 15:17
  • (hmm though I just spotted that `someSlowFunction` is in the same class and defined right there; meh) – Lightness Races in Orbit Sep 30 '15 at 15:18
  • @LightnessRacesinOrbit, yeah, exactly. – SergeyA Sep 30 '15 at 15:20

4 Answers4

3

Compiler optimization can not break thread safety!

You might however experience issues with optimizations in code that appeared to be thread safe but really only worked because of pure luck.

If you access data from multiple threads, you must either

  • Protect the appropriate sections using std::mutex or the like.
  • or, use std::atomic.

If not, the compiler might do optimizations that is next to impossible to expect.

I recommend watching CppCon 2014: Herb Sutter "Lock-Free Programming (or, Juggling Razor Blades), Part I" and Part II

anorm
  • 2,255
  • 1
  • 19
  • 38
0

After answering question in comments, it makes more sense. Let's analyze thread-safety here given that fetch() and getBar() are called from different threads. Several points will need to be considered:

  • 'Dirty reads', or garabage reading due to interrupted write. While a general possibility, does not happen on 3 chip families I am familiar with for aligned ints. Let's discard this possibility for now, and just assume read values are alwats clean.
  • 'Improper reads', or an option of reading something from bar which was never written there. Would it be possible? Optimizing away temp on the compiler part is, in my opinion, possible, but I am no expert in this matter. Let's assume it does not happen. The caveat would still be there - you might NEVER see the new value of bar. Not in a reasonable time, simply never.
SergeyA
  • 61,605
  • 5
  • 78
  • 137
0

The compiler can apply any transformation that results in the same observable behavior. Assignments to local non-volatile variables are not part of the observable behavior. The compiler may just decide to eliminate temp completely and just use bar directly. It may also decide that bar will always end up with the value zero, and set at the beginning of the function (at least in your simplified example).

However, as you can read in James' answer on a related question the situation is more complex because modern hardware also optimizes the executed code. This means that the CPU re-orders instructions, and neither the programmer or the compiler has influence on that without using special instructions. You need either a std::atomic, you memory fences explicitly (I wouldn't recommend it because it is quite tricky), or use a mutex which also acts as a memory fence.

Jens
  • 9,058
  • 2
  • 26
  • 43
-1

It probably wouldn't optimize that way because of the function call in the middle, but you can define temp as volatile, this will tell the compiler not to perform these kinds of optimizations.

Depending on the platform, you can certainly have cases where multibyte quantities are in an inconsistent state. It doesn't even need to be thread related. For example, a device experiencing low voltage during a power brown-out can leave memory in an inconsistent state. If you have pointers getting corrupted, then it's usually bad news.

One way I approached this on a system without mutexes was to ensure every piece of data could be verified. For example, for every datum T, there would be a validation checksum C and a backup U.

A set operation would be as follows:

U = T
T = new value
C = checksum(T)

And a get operation would be as follows:

is checksum(T) == C
    yes: return T
     no: return U

This guarantees that the whatever is returned is in a consistent state. I would apply this algorithm to the entire OS, so for example, entire files could be restored.

If you want to ensure atomicity without getting into complex mutexes and whatever, try to use the smallest types possible. For example, does bar need to be an int or will unsigned char or bool suffice?

Yimin Rong
  • 1,890
  • 4
  • 31
  • 48
  • 1
    How does using the smallest type help with thread safety? Also volatile does not guarantee any thread safety as well. – NathanOliver Sep 30 '15 at 15:10
  • @NathanOliver - Any data type wider than the architecture's data bus width is susceptible to having a write operation interrupted such that a read operation will be getting a value in an inconsistent state. The use of `volatile` is to address any compiler optimizations. – Yimin Rong Sep 30 '15 at 15:15
  • @YiminRong Delcaring temp as `volatile`does not influence the behavior of `bar`. Volatile will make sure that the assignments to temp, i.e. setting it first to 0 and the to -1, will be in the order in the source code because it is part of the observable behavior, but assignments to bar are subject to optimization. The compiler will probably inline `someSlowFunction`, and then easily see that bar is set to 0 all the time. No, it may move the assignment somewhere else, because it is better for pipelining or whatever reasons apply. Short summary: `volatile` is not for thread-safety. – Jens Sep 30 '15 at 15:36