1

Is there a way to tell the C++ compiler that an object that's reachable through a pointer has changed, even if it has every reason to believe it hasn't?

My use-case is something like this:

// Three argument system call.
inline unsigned long do_syscall(unsigned short callnum,
                                unsigned long p1,
                                unsigned long p2,
                                unsigned long p3) noexcept
{
   val_t retval;
   asm volatile (
      "syscall\n\t"
       :"=a"(retval)
       :"a"(static_cast<::std::uint64_t>(callnum)), "D"(p1), "S"(p2), "d"(p3)
       :"%rcx", "%r11"
      );
   return retval;
}

inline void read(int fd, char *buf, int size) noexcept {
   do_syscall(0, fd, reinterpret_cast<unsigned long>(buf), size);
}

char buf[1024] = {'\0'};
read(0, buf, 1024);
// Here, the compiler needs to believe it knows nothing about what's in buf.

The compiler has full access to the code for read, and it has every reason to believe that read doesn't modify the contents of buf. Is there anything I can put in read that would doesn't result in any code by itself, but does tell the compiler it must read the contents of buf from memory again if it's accessed?

An answer specific to gcc and/or clang would be acceptable, but I would prefer a method that was standard C++.

Omnifarious
  • 54,333
  • 19
  • 131
  • 194
  • 4
    use a volatile variable `volatile char buf[1024] = {'\0'}` – AspectOfTheNoob Jan 10 '23 at 23:55
  • 2
    @MooingDuck But that's the background assumption here we must accept. The program has defined a `read` function which is fully inlined and it's obvious that it doesn't actually modify the input. If that function has no other effect, the call to it can be removed. – Kaz Jan 10 '23 at 23:56
  • @Kaz - The function calls a function that has an inline assembly block that executes the `syscall` instruction. – Omnifarious Jan 10 '23 at 23:57
  • @AspectOfTheNoob - That puts a huge burden on callers. :-/ – Omnifarious Jan 10 '23 at 23:58
  • 2
    @Omnifarious: I don't understand. Either the compiler knows everything that is happening to `buf` within `read`... or it doesn't. If it does, you can't make it pretend not to. And if it doesn't, then it won't know and therefore won't make any assumptions. – Nicol Bolas Jan 10 '23 at 23:59
  • @NicolBolas - I updated the question to make it clearer. – Omnifarious Jan 11 '23 at 00:05
  • 1
    `The function calls a function that has an inline assembly block` So why not put `memory` in the clobber? – tkausl Jan 11 '23 at 00:06
  • @tkausl - Because that tells the compiler that _all_ memory must be considered dirty. The code I currently have does this, and I've verified that a global variable that is completely untouched is nonetheless considered to have been possibly changed by the compiler when the `memory` clobber is there. – Omnifarious Jan 11 '23 at 00:07
  • @SamVarshavchik - I'm writing my own libc. So, no, it won't. – Omnifarious Jan 11 '23 at 00:09
  • 2
    @Omnifarious: "*it has every reason to believe that read doesn't modify the contents of buf.*" Citation needed. It can clearly see you are converting that address into an integer of a type (presumably) large enough to store it, and then that integer disappears into inline assembly. The compiler has ever reason to assume that the assembly can use this integer as an address from which to access memory. – Nicol Bolas Jan 11 '23 at 00:10
  • 1
    Put `read` in a separate compilation unit and turn off LTO. Then (at the call site) the compiler won't know what `read` is doing and so can't make any unwanted assumptions. Since you're doing a syscall, I can't see any tangible benefit in inlining it anyway. – Paul Sanders Jan 11 '23 at 00:11
  • @NicolBolas - If you want me to past the assembly output of the compiler to prove to you that the compiler thinks that memory has not been modified, I can. – Omnifarious Jan 11 '23 at 00:11
  • @PaulSanders - Half the reason I'm doing this is that all the system calls will be inlined. – Omnifarious Jan 11 '23 at 00:12
  • But syscalls are expensive, so why bother? – Paul Sanders Jan 11 '23 at 00:12
  • @PaulSanders - Partly because another thing that happens at a function call boundary is that all globals are also considered modified. Honestly, if I just put the `memory` clobber back in, I'd achieve almost the same effect as the function call boundary would. – Omnifarious Jan 11 '23 at 00:16
  • OK. Any particular reason for all this micro-optimisation? – Paul Sanders Jan 11 '23 at 00:17
  • @PaulSanders - My biggest reason is that I really hate the libc system call interface. errno is a blight invented in the era of single threaded programs. If I'm going to rewrite it anyway, I might as well do it like this. – Omnifarious Jan 11 '23 at 00:22
  • I'm not sure that answers my question, but nevermind. POSIX requires that `errno` be threadsafe. – Paul Sanders Jan 11 '23 at 00:32
  • @PaulSanders - Yes, POSIX requires errno be thread safe, which means accesses to it must use thread-local storage, which is invariably really slow to access. The whole thing is annoyingly stupid. Someone should make a low-level libc-like library that implements the system call interface using something like Boost's expected concept. – Omnifarious Jan 11 '23 at 00:36
  • 1
    For inline asm statements specifically, [How can I indicate that the memory \*pointed\* to by an inline ASM argument may be used?](https://stackoverflow.com/q/56432259) is how to tell the compiler about pointed-to arrays that asm (such as a system call) reads and/or writes. Your code is buggy, not using a `"memory"` clobber to make it work generically, or a dummy input to make it work for `write` or dummy output to make it work for `read` specifically when you know which operand is the pointer. – Peter Cordes Jan 11 '23 at 12:58
  • @PeterCordes - It looks like that might be the answer to my question. *sigh* It's going to be a huge pain to make that work with a generic `do_syscall` function. And it'll be quite the adventure to see how well templates and inline assembly work together. :-) – Omnifarious Jan 11 '23 at 13:07
  • 1
    @Omnifarious: Yeah, I'd just use a `"memory"` clobber. Some possible extra spill/reload is minor vs. the overhead of a system call. Or else use C preprocessor macros the way https://github.com/linux-on-ibm-z/linux-syscall-support does. – Peter Cordes Jan 11 '23 at 13:11
  • FYI, the memory clobber is the correct answer to fixing your existing code, but [`std::launder`](https://en.cppreference.com/w/cpp/utility/launder) is the general purpose solution to the original question. – Useless Jan 11 '23 at 13:14

0 Answers0