Search code examples
ccompiler-constructionlanguage-lawyervolatile

Semantics of volatile


For the sake of this question, let us look at only reads on volatile variables. All the discussions I have read, the only conclusion is that multiple reads on the same variable declared volatile cannot be optimized out to a single effect.

But I believe that is a bit strict. Consider two reads on a variable, which do not have any side effect between them, or do not have read of any other volatile variable between them.

Now we know that the value in a volatile variable can change any time (without the compiler having a hint of it). But there is no way for the programmer to ensure that the change will happen between the two reads. This means that both the reads seeing the same value is a valid behavior for the program.

So can't the compiler enforce this behavior? Doing a single read and using the value twice.

For example

int foo(volatile int * x) {
    return *x + *x;
}

Can the compiler do a single read in this case?

I hope my query is clear.

Also I am assuming a system where the read itself doesn't have a side effect (like increment of a counter, or value changing with every read). Or do such systems exist?

I have looked at the assembly generated from gcc and clang and they do insert two reads even with maximum optimizations. My question is are they overly conservative?

Edit: To not complicate my question and avoid confusion with implementation defined order of evaluation of sub expressions, we can look at the example -

int foo(volatile int * x) {
    int a = *x;
    int b = *y;
    return a + b;
}

But I am also retaining the previous example because some answers and comments have referenced that.


Solution

  • Reading from a memory location can have a side effect. The program has to use more than standard C, of course. Reading can only have a side effect in a program that relies on implementation-defined behavior.

    A common example would be reading from a memory-mapped peripheral. On many architectures, the main processor exchanges data with peripherals when data is read or written to particular ranges of memory locations. If a memory location is mapped to a peripheral, doing two reads sends two read requests to the peripheral. The peripheral may perform a non-idempotent operation on each read.

    For example, reading a byte from a serial communication peripheral transfers the next byte in the peripheral's input queue each time. So if foo is called with the address of that serial peripheral's byte read register, then it pulls two successive bytes from the peripheral's read buffer. The compiler is not allowed to change the behavior to read only one byte.

    Well, except that the behavior is undefined because there's no sequence point between the reads, and reading from a volatile is a side effect. A correct function would be

    int foo2(volatile int *x) {
        int x1 = *x;
        int x2 = *x;
        return x1 + x2;
    }
    

    I'd expect most compilers to generate the same code for foo as for foo2 though.