-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
new builtin: @volatileUse #6168
Comments
This is a very good idea. |
I'm not familiar with exactly what Zig guarantees with respect to inline assembly, so forgive me if this is already explained somewhere. What is the precise guarantee that this intrinsic is supposed to provide? For example, if you have code like const r = @declareSideEffect(a + b) does that guarantee that the floating point addition If you have
does that guarantee that
does this guarantee that the Do the answer to these questions change if you're talking about inline assembly, local variable invocations, internal pointer assignments? |
It guarantees that the final value of Without |
@CurtisFenner Assuming this behaves like the asm version, all it guarantees is that the result of the computation exists in a register. If the result of the computation is a pointer, it also escapes the data that is pointed to, which forces future memory clobbers to invalidate that data. So to answer the questions, const c = ...;
const a = c - b;
const r = @declareSideEffect(a + b); In this case, the compiler may realize that a + b is just c, and optimize away both c-b and a+b. But it will not mark the computation of c as dead code, even if r is never used. fn add(a: var, b: var) @typeOf(a, b) {
return a + 0.0 + b;
}
const r = @declareSideEffect(add(a, b)); Again, assuming the floating point rules allow the optimizer to remove + 0.0, it will. It may also inline the If we can implement There are some other fine-grained clobber functions that might be nice to include, either as functionality in this builtin or as a small family of builtins. I'll go through examples of those from other benchmarking libraries below: Google's Benchmark library has a second function, asm volatile("" : : : "memory"); This clobbers any escaped memory without forcing a specific pointer value to be computed. It can be useful in cases where pointers may alias, but you don't want to force the pointer to be computed if the compiler can prove no aliasing. Looking at Facebook's Folly benchmarking library, they have four variants of this function (slightly modified for clarity, /// for non-pointer types that fit in registers
/// ensures that the value is not marked as dead code
template <typename T>
void doNotOptimizeAwayPrimitive(const T& datum) {
// The "r" constraint forces the compiler to make datum available
// in a register to the asm block, which means that it must have
// computed/loaded it. We use this path for things that are <=
// sizeof(long) (they have to fit), trivial (otherwise the compiler
// doesn't want to put them in a register), and not a pointer (because
// doNotOptimizeAway(&foo) would otherwise be a foot gun that didn't
// necessarily compute foo).
asm volatile("" ::"r"(datum));
}
/// for pointer types (T may not be a pointer, the pointer is the reference)
/// ensures that the pointer value is not marked as dead code
/// also escapes this pointer and clobbers memory, making the
/// compiler think that the data that is pointed to may have been modified
template <typename T>
void doNotOptimizeAwayPointer(const T& datum) {
// This version of doNotOptimizeAway tells the compiler that the asm
// block will read datum from memory, and that in addition it might read
// or write from any memory location. If the memory clobber could be
// separated into input and output that would be preferrable.
asm volatile("" ::"m"(datum) : "memory");
}
/// for types that fit in a register, make the compiler think that the value
/// was read and then modified.
template <typename T>
void makeUnpredictablePrimitive(T& datum) {
/// "+r" marks the datum as both an input and an output
asm volatile("" : "+r"(datum));
}
/// for types that are too large for a register, make the compiler think
/// that the memory region containing the value was read and then
/// modified. This also escapes the memory region and clobbers
/// any other escaped regions. This does not clobber the actual
/// pointer value, just the data that it points to.
template <typename T>
void makeUnpredictablePointer(T& datum) {
asm volatile("" ::"m"(datum) : "memory");
} With these, they are trying to make more fine grained tools for performing the exact type of invalidation that is needed. But these tools are imperfect. In Overall, if we're looking at integrating clobbers into the language, having some more finely tuned clobbers might be beneficial. |
until ziglang#6168 is implemented, partially revert 0bd53dd in order to restore the ability to use std.math in comptime functions.
Another option: reuse the fn x(y: *u8) void {
// This will at runtime perform some addition operation regardless of optimize mode:
volatile 1 + 1;
// This will write to the pointer for sure, which means in this case the pointer
// can be used like a `*volatile u8` pointer without needing to cast.
// This is useful when you need the operation to happen for sure only sometimes.
volatile y.* = 1;
} Additionally, this would also simplify And the keyword can of course also be used for blocks to make sure all operations in a block happen: volatile { ... } |
That's an interesting take, but I don't know what we would lower it to for LLVM once it's so generalized. |
I was thinking about it mostly as a syntactical difference, avoiding a new builtin. But I guess the fact that |
Use cases:
forceEval
) - used to make the CPU do float operations in case we want to guarantee triggering hardware interrupts or hardware-based error handlingStatus quo solution:
The comptime behavior is consistent with the math use case (in fact improves it to allow those functions to execute at comptime), and not applicable to the benchmark use case.
The main benefits over using the status quo solution would be supporting types that have trouble getting sent into inline assembly, as well as working in comptime scope. It reduces the burden on new architectures being supported because implementation of this builtin would be done architecture-independent instead of relying on inline assembly which is necessarily architecture-specific. Downsides would be causing confusion about when to use this versus
volatile
and atomics, which people already get confused about.This builtin would be defined to be equivalent to this Zig code, with
dummy
being intentionally inaccessible anywhere else:However, it is to be a builtin so that backend-specific lowering nonsense doesn't end up in userland.
The text was updated successfully, but these errors were encountered: