Can statements be re-ordered by the compiler?

I have a critical section in my code where ordering matters.

I need to append to a queue (“transactions”) before I send the data for the transaction over the network. The append must happen before the send because any thread may receive a response for the transaction.
If the append happens after the send, it is possible for another thread to receive the response for a transaction not yet in the queue and discard it.

  // We need to append the transaction before we send.
  // Because we may recv from any thread.
  {
      self.transactions_mutex.lock();
      defer self.transactions_mutex.unlock();
      self.transactions.append(transaction);
  }
  _ = self.link_layer.send(out) catch return error.LinkError;

send is a function in a vtable.

The ordering must be maintained in the following environments:

  1. When send() makes a syscall.
  2. When send() does not make a syscall, and instead operates against a simulator where all network operations are actually just copying some memory around.

Do I need to do something to prevent the compiler from reordering my send() to before my append()?

Loads and stores may be reordered. However, there are rules that limit how they may be reordered. For instance, locking a mutex is going to either do an atomic store with “acquire” semantics, meaning that loads and stores cannot be moved before the mutex lock, or it will make a syscall with inline assembly. Here are both examples:

  1. zig/lib/std/Thread/Mutex.zig at bb79c85cb7ad591e0d8d4fe94b3c32883173c5fa · ziglang/zig · GitHub

  2. zig/lib/std/os/linux/x86_64.zig at bb79c85cb7ad591e0d8d4fe94b3c32883173c5fa · ziglang/zig · GitHub

In the second case, the inline assembly is marked as having side effects and that evaluating the asm clobbers “memory”. That means loads and stores cannot move across that inline assembly (and therefore across calls to the function it resides in).

Same deal applies to unlocking a mutex.

So, essentially, neither your append statement cannot move before lock, or after unlock. You have indeed locked that statement in place with a mutex.

As for send being moved, presumably send is making a syscall, so it will have the same memory clobbering annotation, meaning that no memory loads or stores can be moved across that statement.

4 Likes

I’m thinking this is impossible actually, since my simulator will be multithreaded, there will be some mutex protecting the internal socket memory anyway, which would have the same effect as when send() is a syscall, (since Mutex.lock() is a syscall).

And if my simulator is in a single threaded environment, none of this matters anyway since I cant be interrupted.