1

As we know, the compiler or the CPU may reorder the execution as they want, only if they follow the as-if rule. For example, if we have such a piece of code:

C = A + B;
D = E + F;

The compiler or the CPU may execute D = E + F before C = A + B. I can understand that.

Today, my colleague tried to build a log library with C++. His idea is to use constructor & destructor to make some logs with the help of some overloading stream functions, such as operator<<().

Basically, he offered such a kind of class:

class Log
{
public:
    Log() { streamObj << "start: " << getCurrentTime() << endl; }
    ~Log() { streamObj << "end: " << getCurrentTime() << endl; }
};

Now, I'm the user of the log library. My colleague told me that I could use the library as below:

void func()
{
    Log log;
    // do something1
    // do something2
    // do something3
    return;
}

So when the first line is executed, the constructor is invoked so I can get a "start" in the log. And when the function returns, the destructor of log will be invoked so I will get an end in the log. With the help of the object log, we can clearly find the start of the function and the end of the function.

That sounds clear and great.

However, as I mentioned at the beginning of the post, the machine may do some reordering as it wants. So I'm now wondering if it is possible that the constructor of log is invoked later than we think and/or the destructor of the log is invoked sooner than we think so that the log can't work as we expected. I mean, the code of func did look as above but when it was compiled or was executed, the real order became:

// do something1
Log log;
// do something2
// call the destructor of `log`
// do something3
return

BTW, the stream in the class Log is directed into somewhere else, such as a file, or a shared memory or a TCP socket.

So am I reasonable? Or this kind of reordering will never happen? If it may happen, is there some technique to forbid this kind of reordering or some technique to offer an useable log library which can tell us the start and the end of any function?

In fact, I've heard some new technique in C++11, such as std::atomic and std::atomic_thread_fence. As my understanding, if this kind of reordering is possible, what I need might be... a fence?

class Log
{
public:
    Log() {
        streamObj << "start: " << getCurrentTime() << endl;
        // build a fence here!
    }
    ~Log() { streamObj << "end: " << getCurrentTime() << endl; }
};

I really don't know if it is possible...

About side effects/observable behavior

As my understanding, this is a side effect/observable behavior:

A = B + C

Why? Because the value of A is changed.

Now, what if I code like this:

void func()
{
    Log log;
    A = B + C;
}

So the order could be:

  • constructor of log
  • A = B + C
  • destructor of log

However, if the order becomes:

  • A = B + C
  • constructor of log
  • destructor of log

I think that would be fine. Why? Because the value of A, which is about a side effect/observable behavior, is NOT modified. No matter which order we take, its value would be always B + C.

Am I right? If I'm right, I think it means that the Log won't work as expected.

UPDATE

A = B + C has a side effect, which is that the value of A is changed, but it isn't an observable behavior.

Yves
  • 11,597
  • 17
  • 83
  • 180
  • 2
    The object destruction doesn't happen until the scope is left ([source](https://stackoverflow.com/questions/2087600/is-a-c-destructor-guaranteed-not-to-be-called-until-the-end-of-the-block)). That can't be changed by optimization. Construction is a little more difficult. – NathanOliver Mar 25 '21 at 13:36
  • 4
    The compiler is not allowed to reorder operations that have visible side effects. Writing output is a visible side effect. If `// do something3` does not have visible side effects, it could be postponed until after the destructor. But a well-formed program could not detect this change, because the side effects are not visible. – Pete Becker Mar 25 '21 at 13:42
  • @NathanOliver: What rule in the C++ standard precludes moving operations from after a destructor call and in code in a different scope to before the destructor call if they have no observable behavior and do not change the observable behavior of the destructor? – Eric Postpischil Mar 25 '21 at 13:47
  • @PeteBecker `Writing output is a visible side effect`: well, what if I write output into a shared memory, instead of a real file, is this still a visible side effect? – Yves Mar 25 '21 at 13:47
  • @largest_prime_is_463035818 In fact I've googled a lot about the as-if rule and about the observable behavior. But I still can't figure out what they are... If `Log log;` is rearranged after the `// do something1`, could we say that "an observable behavior has been changed because a string is written into a shared memory or a file 0.003 seconds late"? – Yves Mar 25 '21 at 13:53
  • sorry removed my comment, as it was just reiterating what has been said before. You are worried about the correctness of the timestamp, right? – 463035818_is_not_an_ai Mar 25 '21 at 13:54
  • I had similar doubts some time ago which led me to this question: https://stackoverflow.com/questions/46572817/timing-vs-the-as-if-rule – 463035818_is_not_an_ai Mar 25 '21 at 13:55
  • @Yves: Are you asking in theory or in practice? (in theory, writing `"elapsed time = 42ms"` seems even allowed) – Jarod42 Mar 25 '21 at 14:05
  • About the _"as-if"_ rule https://en.cppreference.com/w/cpp/language/as_if – Richard Critten Mar 25 '21 at 14:10
  • 3
    [Here's an example](https://gcc.godbolt.org/z/3bv5rv6oE) of the compiler moving an assignment past the destructor of the `Log` object. As for what constitutes observable behavior, the [definition](https://timsong-cpp.github.io/cppwp/intro.abstract#def:behavior,observable) covers file output and interactive devices. "Shared memory" is not part of the C++ language, so the standard naturally cannot impose requirements on something outside its scope. The implementation is of course welcome to document how it treats shared memory. – Raymond Chen Mar 25 '21 at 14:11
  • 2
    That said, you could declare your shared memory as [`volatile`](https://timsong-cpp.github.io/cppwp/dcl.type.cv#5): Volatile access and library functions [are considered side effects](https://timsong-cpp.github.io/cppwp/intro.execution#7), and side effects can be sequenced. (In practice, shared objects are going to be volatile because they can be changed by the person you're sharing them with, without the compiler's knowledge.) – Raymond Chen Mar 25 '21 at 14:22
  • 1
    if `// do somethingX` has no side effects, so your function "becomes" just `{ Log log; }`, so print "current" time twice. Reordering is just a possible way of optimization. – Jarod42 Mar 25 '21 at 14:32
  • @largest_prime_is_463035818 I added something more in my post, please check it if you want. – Yves Mar 26 '21 at 02:01
  • @RaymondChen I added something more in my post, please check it if you want. – Yves Mar 26 '21 at 02:02
  • @Jarod42 I added something more in my post about side effects, please check it if you want. – Yves Mar 26 '21 at 02:03
  • changing the value of `A` alone is not observable. The whole point about observable beahvior is that you can actually observe it. If you write the result of `A` to the log in `Log` constructor and in the destructor, then the compiler has no way to rearrange the three lines. Or maybe it has a way, but you cannot tell the difference because the observable behavior (in this case what is written to the stream) must be as if they are not rearranged. – 463035818_is_not_an_ai Mar 26 '21 at 08:09
  • Not sure how aggressive are compiler are about moving code. (especially that extern functions (those where definition is in another TU or even lib)) are generally treated as black box (so with possible side-effect). LTO (link time optimization) might be applied afterward, but it has to reason at lower level, which might make that kind of change even harder (and idea is to make code faster. I don't see why `A=B+C; Log log;` would be faster) – Jarod42 Mar 26 '21 at 08:22
  • If the calculation of "B+C" and the modification of "A" has no effect upon the output of "Log", then reordering it has no "observable" effect and is therefore permitted by the standard. – Raymond Chen Mar 26 '21 at 13:51
  • @RaymondChen Alright, I think I got it. Thanks. – Yves Mar 27 '21 at 01:37

0 Answers0