Open bhansconnect opened 1 month ago
I think the compiler (LLVM, really) is well within its rights to assume that normal-looking code won't just suddenly switch to running on a different thread out of nowhere. You have not in any way told the compiler that such a thing could happen, so it's completely reasonable for it to cache computations of threadlocal
variable addresses. I'd bet you could also run into some fun bugs around atomics and memory ordering here.
What's needed here is a way to communicate to the compiler that it shouldn't do that caching. See https://github.com/llvm/llvm-project/issues/98479 and https://github.com/llvm/llvm-project/issues/111257. This is not a problem we can solve on the Zig side without LLVM having some mechanism for this.
Totally out of my depth, but for asm
writing to memory there's this memory clobber annotation I've seen around.
Maybe a similar solution could be to introduce a type of threadlocal
(address) clobber for asm
statements?
(Which probably needs to include LLVM support for our LLVM backend.)
Or alternatively, maybe you could find a way to load the TLS address yourself in an asm
block (might need to be volatile asm
?) so the compilation pipeline doesn't interfere with your intent?
That would still mean normal userland uses of threadlocal
are broken in a function that transitively contains context switching, but maybe a helper function load_tls("name")
is a desirable workaround to re-implement a library solution in userland?
think the compiler (LLVM, really) is well within its rights to assume that normal-looking code won't just suddenly switch to running on a different thread out of nowhere.
Totally fair. Some reason, I thought this Worked in clang. But it doesn't: https://godbolt.org/z/T5rTcfE3Y So not a zig specific issue.
Looks like the way to make clang understand this is to add an extra layer of indirection with a volatile pointer. So I guess I should expect the same from zig.
I guess what suprises me the most and looks to be a difference between zig and clang is that the threadlocal seems to always be loaded at the beginning of the function in zig. This still fails in zig:
fn repro() void {
// We context switch to suspend this execution
switch_context(&repro_context, &main_context);
// We load the address after the context switch (but zig actually loads it before)
const thread2_addr = &thread_local;
std.log.info("thread 1 addr: {*}", .{thread1_addr});
std.log.info("Error -> thread 2 addr: {*}", .{thread2_addr});
workaround();
// Go back to main to exit the program.
switch_context(&repro_context, &main_context);
}
Doing the same in clang will generate the expected results. It will not load the threadlocal until after returning from switch_context
. So I would arguably say that this part is still a bug.
clobber for asm statements?
global assembly doesn't get clobbers.
might need to be volatile asm?
volatile
doesn't mean anything for global assembly.
Zig Version
0.13.0
Steps to Reproduce and Observed Behavior
zig run main.zig
on this file on aarch64 unix or x86_64 unix (I specifically testing on aarch64 macos and x86_64 linux).output:
main.zig
Expected Behavior
The address of threadlocals should be loaded at every single time it is used. Zig only loads the address once for the entire repro function. This leads to printing the incorrect address. Even without the first print, zig will load the address at the start of the function. This means it still incorrectly prints the thread one address.
Expected:
Just to be more explicit, the change in the output is that address on the middle line matches the last line instead of the first line.