2

The program below has different behaviors with different option levels. When I compile it with -O3, it will never terminate. when I compile it with -O0, it will always terminate very soon.

#include <stdio.h>
#include <pthread.h>

void *f(void *v) {
    int *i = (int *)v;
    *i = 0;
    printf("set to 0!\n");
    return NULL;
}

int main() {
    const int c = 1;
    int i = 0;
    pthread_t thread;
    void *ptr = (void *)&c;
    while (c) {
        i++;
        if (i == 1000) {
            pthread_create(&thread, NULL, &f, ptr);
        }
    }
    printf("done\n");
}

This is the result of running it with different optimization flags.

username@hostname:/src$  gcc -O0 main.c -o main
username@hostname:/src$  ./main 
done
set to 0!
set to 0!
username@hostname:/src$  gcc -O3 main.c -o main
username@hostname:/src$  ./main 
set to 0!
set to 0!
set to 0!
set to 0!
set to 0!
set to 0!
^C
username@hostname:/src$ 

The answer given by the professor's slide is like this:

  • Will it always terminate?

  • Depends of gcc options

  • With –O3 (all optimisations): no

Why?

  • The variable c is likely to stay local in a register, hence it will not be shared.

Solution « volatile »


Thank you for your replies. I now realize that volatile is a keyword in C. The description of the volatile keyword:

A volatile specifier is a hint to a compiler that an object may change its values in ways not specified by the language so that aggressive optimizations must be avoided.

According to my understanding, there is a shared register that stores the c value when we use -O3 flag. So the main thread and sub-thread will share it. In this case, if a sub-thread modifies c to 0, the main thread will get 0 when it wants to read c to compare in the while(c) statement. Then, the loop stops.

There is no register storing c that can be shared by the main thread and sub-threads when we use -O0 flag. Though the c is modified by a sub-thread, this change may not be written to memory and just be stored in a register, or it is written to memory while the main thread just uses the old value which is read and saved in a register. As a result, the loop is infinite.

If I declared the c value with const: const volatile int c = 1;, the program will terminate finally even if we compiled it with -O3. I guess all threads will read c from the main memory and write back to the main memory if they change the c value.


I know, according to the specifications or rules about C language, we are not allowed to modify a value that is declared by the const keyword. But I don't understand what is un behavior.

I wrote a test program:

#include "stdio.h"

int main() {
    const int c = 1;
    int *i = &c;
    *i = 2;
    printf("c is : %d\n", c);
}

output

username@hostname:/src$ gcc test.c -o test
test.c: In function ‘main’:
test.c:9:14: warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]
    9 |     int *i = &c;
      |              ^
username@hostname:/src$ ./test
c is : 2
username@hostname:/src$ 

The result is 2 which means a variable declared with the const can be modified but this behavior is not suggested, right?


I also tried changing the judgment condition. If it is changed to while (1){ from while(c){, the loop will be an infinite one no matter using -O0 or -O3


This program is not a good one as it violates the specifications or rules of C language. Actually it comes from the lecture about software security.

Can I just understand like this? All threads share the same register storing c when we compile the program with -O0.

While the value c is in un-shared registers, so main thread is not informed when sub-threads modify value c when we use -O3. Or, while(c){ is replaced by while(1){ when we use -O3 so the loop is infinite.

I know this question can be solved easily if I check the generated assembly code. But I am not good at it.

  • 7
    The variable `c` is marked as ***constant***. You're not allowed to try to modify it. Any attempt to modify it, for example through a pointer, leads to *undefined behavior*. So the professors is right, but for the wrong reasons. – Some programmer dude Feb 04 '23 at 16:24
  • 2
    It's quite possible that `main` will no longer be reading `c` (neither from memory nor from a register) when compiling with optimizations. Have you tried to inspect the assembly generated for `main` at various optimization levels using a tool like godbolt.org? If you have assembly outputs but have trouble reading them, [edit] them into your question and you may get answers that explain what's going on more closely. – nanofarad Feb 04 '23 at 16:37
  • 2
    Using `volatile` is not a good solution! – Jonathan Leffler Feb 04 '23 at 16:48
  • Note: Some Programmer Dude said, "...undefined behavior." Any time that compiling with different optimization levels results in the program behaving in different ways, that tells you that somewhere, your program depends on undefined behavior. – Solomon Slow Feb 04 '23 at 20:42
  • 1
    *According to my understanding, there is a shared register that stores the c value when we use `-O3` flag* Not correct. Given `const int c = 1; ... while (c){...}`, the compiler is free **at any optimization level** to replace all of `c` and the loop with `while(1){...}` and not check **anything** because `c` is `const` and **can't change**. The fact that you *try* to modify `c` via a pointer is undefined behavior **and the compiler can do anything it wants with that *because* it's undefined behavior**, including ignoring the change completely. – Andrew Henle Feb 04 '23 at 22:17
  • 1
    It's also **critically** important to realize that adding `volatile` and papering over and hiding the undefined behavior invoked when a pointer is used to sidestep `const` and modify a `const` value anyway does not remove the undefined behavior. That is **still** undefined behavior. – Andrew Henle Feb 04 '23 at 22:37
  • @AndrewHenle Thank you very much. But on my computer, Adding the `volatile` keyword works actually as the program finally terminates if I compile it with `-O3`. – Ruibin Zhang Feb 04 '23 at 22:49
  • @AndrewHenle does 'undefined behavior` means that the result is uncertain? – Ruibin Zhang Feb 04 '23 at 22:50
  • 3
    *Adding the `volatile` keyword works...* Again - **it doesn't "work"**. It *hides* the undefined behavior but it does **not** make it go away. Any compiler is completely free to remove or ignore the update of `c` via a pointer in any circumstances in any way at any time **because modifying `const int c` is undefined behavior no matter what you do to hide it from the compiler**. Your professor saying `volatile` fixes the problem is a **HORRIBLY WRONG** lesson. The undefined behavior from modifying a `const` value does not disappear, and `volatile` is **not** viable for multithreaded use. – Andrew Henle Feb 04 '23 at 22:59
  • 1
    [Undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior) means the C standard simply does not apply - the code is operating outside the bounds of the standard. That means anything can happen. "Appears to work" is one of those things. But the behavior still remains undefined. Which is why this lesson is IMO such a bad one - the "fix" is just hiding the root cause of the issue, resulting in dangerous code. It's dangerous because everyone will think it's OK and "works", but it's a time bomb waiting to go off - change the compiler, change the compilation options, and it might break – Andrew Henle Feb 04 '23 at 23:06
  • 3
    "*I guess all threads will read c from the main memory and write back to the main memory if they change the c value.*" Why would you guess that? They could just as well be reading it from a shared cache on the CPU. They could just as well be reading it from unshared caches on the CPU that are synchronized using MESI or something similar. – David Schwartz Feb 04 '23 at 23:08
  • Thank you! The guess was raised because I had a cursory read of the beginning of [this blog](https://www.baeldung.com/java-volatile-variables-thread-safety#:~:text=Unlike%20other%20variables%2C%20volatile%20variables,value%20of%20a%20volatile%20variable.). I may need to know `MESI` . – Ruibin Zhang Feb 04 '23 at 23:27
  • Thank you, @AndrewHenle! I get much knowledge from your comments. As I just previewed the slide of my lecture(software security), I may not understand those slides very well and not be able to describe this problem clearly. I will study on **undefined behavior** of C. Thank you very much. – Ruibin Zhang Feb 04 '23 at 23:32
  • 1
    Re, _I will study on undefined behavior._ There's nothing to "study." When the C language spec says, "...undefined behavior" (UB) it's saying, "don't do this." It's saying, "if you do this, then you have no right to expect your program to work." But, it's also saying to compiler writers, "We understand that it's hard for a compiler to reliably detect this wrong thing, so we won't require you to flag it as an error." When you use UB, your program is not a valid C program. Questions about whether or not it will work cannot be answered by referring to how C programs are _supposed_ to work. – Solomon Slow Feb 05 '23 at 14:57

3 Answers3

5

The professor's explanation is not quite right.

The initial value of c is 1, which is truthy. It's declared as a constant, so its value can't change. Thus, the condition in while (c) is guaranteed to always be true, so there's no need to test the variable at all when the program is running. Just generate code for an infinite loop.

This optimization of not reading the variable is not done when optimization is disabled. In practice, declaring the variable volatile also forces it to be read whenever the variable is referenced in code.

Note that optimizations are implementation-dependent. Assigning to a const variable by accessing it through a non-const pointer results in undefined behavior, so any result is possible.

The typical use of a const volatile variable is for variables that reference read-only hardware registers that can be changed asynchronously (e.g. I/O ports on microcontrollers). This allows the application to read the register but code that tries to assign to the variable will not compile.

Barmar
  • 741,623
  • 53
  • 500
  • 612
  • 1
    And, of course, there's another lot of UB when `i` is incremented too far. However, the chances of that causing problems are quite small. – Jonathan Leffler Feb 04 '23 at 16:49
  • @JonathanLeffler Yeah, I was struggling to understand how multiple threads were being created when the condition `i == 1000` should only be true once, until I realized that it was wrapping around. – Barmar Feb 04 '23 at 16:53
  • @JonathanLeffler Lucky in this case the `-O3` didn't cause `i` to be optimized to nothing more than a 64-bit register value. The time for that to wrap could probably be measured in decades. – Andrew Henle Feb 04 '23 at 17:09
5

This is undefined behavior. Per 6.7.3 Type qualifiers, paragraph 6 of the (draft) C11 standard:

If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined.

There's no requirement for any particular behavior on the program. How it behaves is literally outside the specifications of the C language.

Your professor's observation of how it behaves may be correct. But he goes off the rails. There is no "why" for undefined behavior. What happens can change with changes to compiler options, particulars of the source code, time of day, or phase of the moon. Anything. Any expectation for any particular behavior is unfounded.

And

Solution « volatile »

is flat-out WRONG.

volatile does not provide sufficient guarantees for multithreaded access. See Why is volatile not considered useful in multithreaded C or C++ programming?.

volatile can appear to "work" because of particulars of the system, or just because any race conditions just don't happen to be triggered in an observable manner, but that doesn't make it correct. It doesn't "work" - you just didn't observe any failure. "I didn't see it break" does not mean "it works".

Note that some C implementations do define volatile much more extensively than the C standard requires. Microsoft in particular defines volatile much more expansively, making volatile much more effective and even useful and correct in multithreaded programs.

But that does not apply to all C implementations. And if you read that link, you'll find it doesn't even apply to Microsoft-compiled code running on ARM hardware...

Andrew Henle
  • 32,625
  • 3
  • 24
  • 56
1

The explanation of "The variable c is likely to stay local in a register, hence it will not be shared." is not quite right. Or I'm having trouble parsing its precise meaning.

Once you take a pointer to it, the compiler has to put it into memory, unless it can convince itself that the pointer will not be used.

Here https://godbolt.org/z/YavbYxqoE

mov     DWORD PTR [rsp+4], 1

and

lea     rcx, [rsp+4]

suggest to me that the compiler has put the variable on the stack.

It's just that the while loop is not checking it for changes due to it being advertised as const.

teapot418
  • 1,239
  • 1
  • 3
  • 9
  • The fact that it's declared `const` means any use of a pointer to modify it invokes undefined behavior. If the compiler can deduce the pointer refers to a `const` value, it doesn't have to "put it" anywhere. – Andrew Henle Feb 04 '23 at 16:52
  • @AndrewHenle But it doesn't know that the pointer will be used to modify it, so it still has to pass the correct pointer to the thread function. – Barmar Feb 04 '23 at 16:54
  • @Barmar It *could*. The question provides a single translation unit, so determining the only use of the pointer invokes UB is possible, so it could do anything with that pointer - including eliding it entirely. – Andrew Henle Feb 04 '23 at 17:01
  • True, if it knows what `pthread_create` does. – Barmar Feb 04 '23 at 17:04
  • @Barmar There might be [a specification for that...](https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/functions/pthread_create.html) :-) – Andrew Henle Feb 04 '23 at 17:36
  • 1
    @AndrewHenle Yeah, but it depends on whether the C implementation incorporates the POSIX spec into its optimizations. – Barmar Feb 04 '23 at 17:37