Further clarification on volatile

my starting point is this post, which itself points to the official documentation on volatile

while memory-mapped I/O (MMIO) is clearly a use-case for volatile pointers, there is a common idiom in embedded programming that (i believe) also requires a similar solution…

specifically, consider the following psuedo-code which tests a (software) flag set within an interrupt service routine:

var flag: bool = undefined;

fn app() void {
    ...
    flag = false;
    `enable some hardware interrupt`;
    while (!flag) {
        `pause execution (eg., a WFI instruction)`;
    }
    ...
}

fn interruptHandler() void {
    `service hardware interrupt`
    flag = true;
}

depending upon a variety of conditions, simply polling flag after awaiting an interrupt doesn’t always work… rather, something like this is needed:

fn app() void {
    ...
    flag = false;
    const vp: *volatile bool = &flag;
    while (!vp.*) {
        ...

in simple terms, i want to ensure the value in flag (which is not a MMIO register, strictly speaking) is always “re-read” in my while loop…

clearly, if flag were a complex data-structure being polled, the issue of atomicity enters the picture; but that’s often solved within embedded systems by simply enabling/disabling interrupts…

is this use of volatile correct (since without it, my code can surely fail)??? or is there a better way to express this in zig without incurring undue performance overhead???

You can use volatile only when you are addressing I/O and there is no real memory involved.

If you have memory, volatile is useless!
For memory you must use atomics or std.atomic.Value


    while (!@atomicLoad(bool, &flag, .acquire) {
         ...
    }
    ...
    @atomicStore(bool, &flag, true, .release);

or

var flag = std.atomic.Value(bool).init(false);
    ...
    while (!flag.load(.acquire)) {
         ...
    }
    ...
    flag.store(true, .release);

There’s some uncertainty about the meaning of volatile in Zig when used for anything but MMIO. There was a thread awhile back about volatile in benchmarking which is worth perusing.

You might consider using std.mem.doNotOptimizeAway here, which should have the correct effect. While it’s likely that any idiomatic-to-C use of volatile in Zig will work right now, @andrewrk has been pretty clear on the sole use case it’s intended to support, which is MMIO, so if the compiler starts breaking code which doesn’t use volatile specifically to that purpose, we were fairly warned.

I believe it is correct. In C, this would be correct, and I think Zig just inherited volatile from C. Zig has no way of knowing if a memory address is MMIO or just a normal address, so if it would work for MMIO, it has to work for this use case.

i’ll need to look at the generated code here before i try something like this…

but here’s the strange part… if i have a *volatile, does the compiler really “know” that the actual address value of this pointer references a MMIO location or a location in my SRAM??? of course not!!!

regardless of the address, i do however know that the compiler will read-through this pointer everytime i access it… and that’s exactly the behavior that i want!!!

while i could use std.mem.doNotOptimizeAway as @mnemnion suggests, volatile for me has always suggested a “side-effect” which requires the compiler to be more conservative in its assumptions… said another way, it’s a way for me to assert to the compiler that “hardware” can asynchronously change the value read from memory-location flag

in the example above, i’ve effectively created my own MMIO location by applying a common pattern… and believe me, there is very likely ANOTHER MCU behind what we perceive as a MMIO address actually reading/writing data through a volatile C variable…

so again, if zig’s volatile is only for MMIO, would the compiler eventually flag my program with an error???

volatile is fine and must be used when there are side effects such as accessing MMIO or buffers that change with DMA.

See the volatile documentation

The flag is so simple case that might work with volatile and normally works with relaxed (.monotonic) atomic because instructions order seems to not matter in this simple case.

Quoting Linus Torvalds:

If the memory barriers are right, then the “volatile” doesn’t matter.
And if the memory barriers aren’t right, then “volatile” doesn’t help.

See: Why the “volatile” type class should not be used

2 Likes

quoting from linus: The key point to understand with regard to volatile is that its purpose is to suppress optimization.

exactly!!! i’m absolutely clear that volatile and atomic are distinct (and in fact orthogonal) concepts…

i’m sticking with volatile

This will work, today. It might also continue to work.

Just please bear in mind that Zig isn’t C, and isn’t beholden to C’s semantics. If I had to bet on it, I’d say that for what you’re specifically up to here, volatile will continue to do what you expect. There are some unanswered questions in the thread I linked to, but your application does fall into

If a given load or store should have side effects, such as Memory Mapped Input/Output (MMIO), use volatile.

As the documentation puts it, so it should be fine. Just be aware that it’s an under-specified corner of the language.

I asked some very specific questions about exactly that, which went unanswered. I think you have a correct explanation of volatile’s current behavior, and a reasonable guess as to its intended future behavior. It would be nice to get some ‘further clarification on volatile’, though.

“Pay no attention to the man behind the curtain…”

From the source code of doNotOptimizeAway:

asm volatile (""
    :
    : [val2] "r" (val2),
);

and further down:

fn doNotOptimizeAwayC(ptr: anytype) void {
    const dest = @as(*volatile u8, @ptrCast(&deopt_target));
    for (asBytes(ptr)) |b| {
        dest.* = b;
    }
    dest.* = 0;
}
1 Like

Yep, if you want to know what a program or language does right now, there’s no substitute for examining the source.

If you want to know what a keyword is intended to mean in a pre-1.0 language, things are not so simple.

This one could be reasoned through in a few ways:

  • If I were writing C, I would use volatile here, so I’m going to do it. I would not unconditionally recommend this.
  • std.mem.doNotOptimizeAway is telling the compiler and the reader that the read in the while loop is mandatory. The creator of the language has been rather insistent that volatile is for MMIO, which this is not. So I’ll use doNotOptimizeAway, which does end up doing the same thing currently. I favor this approach.
  • In terms of the mechanics of how CPUs work, it’s hard to imagine a meaningful volatile keyword which doesn’t perform basically the semantics of the word in C. So changes which later invalidate that usage are unlikely to occur. There is a strong case to be made here, but perhaps not a conclusive one.
  • Looking at the source code, they do the same thing, so it doesn’t matter. This is not, in general, a good policy to follow. Here, maybe it’s ok.
2 Likes

If you’re ever writing to the flag from your thread, you should use atomics or temporarily disable interrupts. However, in simple situations such as these wfi instructions typically include a compiler memory fence for this exact reason. You’re still vulnerable to TOCTOU race conditions in more complex cases, but it will at the very least ensure the flag is always read after resuming the thread from an interrupt:

pub fn wfi() void {
    asm volatile("wfi" ::: "memory");
}

and even more important, the write to flag IS atomic by virtue of occuring inside an interrupt routine (which conventionally is entered with interrupts disable)…

again, this is a very common pattern in deeply-embedded systems… anything more complex would, of course, require more advanced methods for synchronization…

As of now (zig version 0.14.0-dev.1211+82b676ef7), std.mem.doNotOptimizeAway has a ill-defined name, imprecise documentation, and faulty implementation. There is a 0% chance it will remain in its current state when it is time to scrutinize the standard library.

1 Like