Skip to content

Instantly share code, notes, and snippets.

@vladfaust
Last active September 2, 2019 22:11
Show Gist options
  • Save vladfaust/ab25631f50b83436fcf8fb2df812b1f7 to your computer and use it in GitHub Desktop.
Save vladfaust/ab25631f50b83436fcf8fb2df812b1f7 to your computer and use it in GitHub Desktop.

This gist is intended to give a choice between two Onyx memory models — weak and strong.

The weak model would imply possibility of memory reorderings done by the compiler, which would result in great performance by default, but create issues when doing lock-free programming.

The strong model would be simpler to newcomers, but slower, requiring to write more code to make it faster.

Note that in all examples above, async means executing in another thread, as soon as possible.

Please see the introduction to memory ordering, Hhppens-before relation and memory fences posts.

Onyx may use a weak memory model.

Upon compiling a program, the Onyx compiler would reorder memory operations for optimizing purposes. The example below could theoretically output Ready and then 0 instead of expected 42. It could happen if the order of x and ready store operations changes. And you can't know in advance whether the ordering would happen or not.

ready = false
x = 0

async do
  x = 42
  ready = true
end

async do
  if ready
    puts "Ready"
    puts x
  else
    puts "Not ready"
  end
end

sleep

There are multiple ways of mitigating the issue. In all three examples below the output is guaranteed to be either Ready 42 or Not ready.

The first way is using volatile blocks. Memory operations within such blocks are guaranteed to never be reordered. It implies lack of memory optimisations (thus poorer performance) within the block.

async do
  volatile do
    x = 42
    ready = true
  end
end

Another way is to use memory fences. Fences allow more precise control over optimisation, which implies lesser performance penalties.

async do
  x = 42
  fence(:release)
  ready = true
end

And the third way is to use atomics. Atomics allow the best control over optimisations and memory, resulting in otherwise great performance.

ready = Atomic(false)

async do
  x = 42
  ready.set(true, :release) # :seqcos (SequentialConsistency) by default
end

async do
  if ready.get(:acquire) # :seqcos by default. The :release/:acquire pair forms a happens-before edge *
    puts "Ready"
    puts x
  else
    puts "Not ready"
  end
end

* Read more about Happens-Before relation.

Onyx may use a strong memory model instead, ensuring the sequential consistency.

In short, memory operations would execute in the way they're printed in source files, without any optimisations. The following code would be guaranteed to always output either Ready 42 or Not ready with the strong memory model:

ready = false
x = 0

async do
  x = 42
  ready = true
end

async do
  if ready
    puts "Ready"
    puts x
  else
    puts "Not ready"
  end
end

sleep

As a trade-off, the performance would be poor by default. It would be possible, though, to explicitly disable strict ordering to allow the optimizer to do its work.

The unordered block would allow the compiler to freely reorder any memory operations within the block.

ready = false
x = 0

async do
  # May lead to the `Ready 0` output, if the operations got reordered!
  unordered do
    x = 42
    ready = true
  end
end

async do
  if ready
    puts "Ready"
    puts x
  else
    puts "Not ready"
  end
end

sleep

It would also be possible to use memory fences and atomics within the unordered block, so the output would be guaranteed to be either Ready 42 or Not ready, again.

async do
  unordered do
    x = 42
    fence(:release)
    ready = true
  end
end
ready = Atomic(false)

async do
  unordered do
    x = 42
    ready.set(true, :release)
  end
end

async do
  unordered do
    is_ready = ready.get(:acquire)
    the_x = x
  end
  
  if is_ready
    puts "Ready"
    puts the_x
  else
    puts "Not ready"
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment