Skip to content

Commit 891fbf7

Browse files
authored
Merge pull request #4575 from RalfJung/weakmem-origin
weak memory tests: add more info on where they come from, and test list of behaviors more thoroughly
2 parents 3ad5863 + f00d344 commit 891fbf7

File tree

6 files changed

+375
-343
lines changed

6 files changed

+375
-343
lines changed

tests/pass/0weak_memory_consistency.rs renamed to tests/pass/0weak_memory/consistency.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
//@compile-flags: -Zmiri-ignore-leaks -Zmiri-disable-stacked-borrows -Zmiri-disable-validation -Zmiri-provenance-gc=10000
1+
//@compile-flags: -Zmiri-ignore-leaks -Zmiri-disable-stacked-borrows -Zmiri-disable-validation
22
// This test's runtime explodes if the GC interval is set to 1 (which we do in CI), so we
33
// override it internally back to the default frequency.
4+
//@compile-flags: -Zmiri-provenance-gc=10000
45

56
// The following tests check whether our weak memory emulation produces
67
// any inconsistent execution outcomes
@@ -14,13 +15,6 @@
1415
// To mitigate this, each test is ran enough times such that the chance
1516
// of spurious success is very low. These tests never spuriously fail.
1617

17-
// Test cases and their consistent outcomes are from
18-
// http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/
19-
// Based on
20-
// M. Batty, S. Owens, S. Sarkar, P. Sewell and T. Weber,
21-
// "Mathematizing C++ concurrency", ACM SIGPLAN Notices, vol. 46, no. 1, pp. 55-66, 2011.
22-
// Available: https://ss265.host.cs.st-andrews.ac.uk/papers/n3132.pdf.
23-
2418
use std::sync::atomic::Ordering::*;
2519
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering, fence};
2620
use std::thread::spawn;
@@ -56,6 +50,8 @@ fn spin_until_bool(loc: &AtomicBool, ord: Ordering, val: bool) -> bool {
5650
val
5751
}
5852

53+
/// Test matching https://www.doc.ic.ac.uk/~afd/homepages/papers/pdfs/2017/POPL.pdf, Figure 7.
54+
/// (The Figure 8 test is in `weak.rs`.)
5955
fn test_corr() {
6056
let x = static_atomic(0);
6157
let y = static_atomic(0);
@@ -75,19 +71,25 @@ fn test_corr() {
7571
let j3 = spawn(move || { // | |
7672
spin_until_i32(&y, Acquire, 1); // <---------+ |
7773
x.load(Relaxed) // <----------------------------------------------+
78-
// The two reads on x are ordered by hb, so they cannot observe values
79-
// differently from the modification order. If the first read observed
80-
// 2, then the second read must observe 2 as well.
8174
});
8275

8376
j1.join().unwrap();
8477
let r2 = j2.join().unwrap();
8578
let r3 = j3.join().unwrap();
79+
// The two reads on x are ordered by hb, so they cannot observe values
80+
// differently from the modification order. If the first read observed
81+
// 2, then the second read must observe 2 as well.
8682
if r2 == 2 {
8783
assert_eq!(r3, 2);
8884
}
8985
}
9086

87+
/// This test case is from:
88+
/// http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/, "WRC"
89+
/// Based on
90+
/// M. Batty, S. Owens, S. Sarkar, P. Sewell and T. Weber,
91+
/// "Mathematizing C++ concurrency", ACM SIGPLAN Notices, vol. 46, no. 1, pp. 55-66, 2011.
92+
/// Available: https://www.cl.cam.ac.uk/~pes20/cpp/popl085ap-sewell.pdf.
9193
fn test_wrc() {
9294
let x = static_atomic(0);
9395
let y = static_atomic(0);
@@ -114,6 +116,8 @@ fn test_wrc() {
114116
assert_eq!(r3, 1);
115117
}
116118

119+
/// Another test from http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/:
120+
/// MP (na_rel+acq_na)
117121
fn test_message_passing() {
118122
let mut var = 0u32;
119123
let ptr = &mut var as *mut u32;
@@ -139,7 +143,8 @@ fn test_message_passing() {
139143
assert_eq!(r2, 1);
140144
}
141145

142-
// LB+acq_rel+acq_rel
146+
/// Another test from http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/:
147+
/// LB+acq_rel+acq_rel
143148
fn test_load_buffering_acq_rel() {
144149
let x = static_atomic(0);
145150
let y = static_atomic(0);

tests/pass/0weak_memory_consistency_sc.rs renamed to tests/pass/0weak_memory/consistency_sc.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
//@compile-flags: -Zmiri-ignore-leaks -Zmiri-disable-stacked-borrows -Zmiri-disable-validation -Zmiri-provenance-gc=10000
1+
//@compile-flags: -Zmiri-ignore-leaks -Zmiri-disable-stacked-borrows -Zmiri-disable-validation
22
// This test's runtime explodes if the GC interval is set to 1 (which we do in CI), so we
33
// override it internally back to the default frequency.
4+
//@compile-flags: -Zmiri-provenance-gc=10000
45

56
// The following tests check whether our weak memory emulation produces
67
// any inconsistent execution outcomes
@@ -348,7 +349,7 @@ fn test_sc_relaxed() {
348349
}
349350

350351
pub fn main() {
351-
for _ in 0..50 {
352+
for _ in 0..32 {
352353
test_sc_store_buffering();
353354
test_iriw_sc_rlx();
354355
test_cpp20_sc_fence_fix();
File renamed without changes.

tests/pass/0weak_memory/weak.rs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//@compile-flags: -Zmiri-ignore-leaks -Zmiri-fixed-schedule
2+
// This test's runtime explodes if the GC interval is set to 1 (which we do in CI), so we
3+
// override it internally back to the default frequency.
4+
//@compile-flags: -Zmiri-provenance-gc=10000
5+
6+
// Tests showing weak memory behaviours are exhibited, even with a fixed scheule.
7+
// We run all tests a number of times and then check that we see the desired list of outcomes.
8+
9+
// Spurious failure is possible, if you are really unlucky with
10+
// the RNG and always read the latest value from the store buffer.
11+
12+
use std::sync::atomic::Ordering::*;
13+
use std::sync::atomic::{AtomicUsize, fence};
14+
use std::thread::spawn;
15+
16+
#[allow(dead_code)]
17+
#[derive(Copy, Clone)]
18+
struct EvilSend<T>(pub T);
19+
20+
unsafe impl<T> Send for EvilSend<T> {}
21+
unsafe impl<T> Sync for EvilSend<T> {}
22+
23+
// We can't create static items because we need to run each test multiple times.
24+
fn static_atomic(val: usize) -> &'static AtomicUsize {
25+
Box::leak(Box::new(AtomicUsize::new(val)))
26+
}
27+
28+
// Spins until it reads the given value
29+
fn spin_until(loc: &AtomicUsize, val: usize) -> usize {
30+
while loc.load(Relaxed) != val {
31+
std::hint::spin_loop();
32+
}
33+
val
34+
}
35+
36+
/// Check that the function produces the intended set of outcomes.
37+
#[track_caller]
38+
fn check_all_outcomes<T: Eq + std::hash::Hash + std::fmt::Debug>(
39+
expected: impl IntoIterator<Item = T>,
40+
generate: impl Fn() -> T,
41+
) {
42+
use std::collections::HashSet;
43+
44+
let expected: HashSet<T> = HashSet::from_iter(expected);
45+
let mut seen = HashSet::new();
46+
// Let's give it N times as many tries as we are expecting values.
47+
let tries = expected.len() * 12;
48+
for i in 0..tries {
49+
let val = generate();
50+
assert!(expected.contains(&val), "got an unexpected value: {val:?}");
51+
seen.insert(val);
52+
if i > tries / 2 && expected.len() == seen.len() {
53+
// We saw everything and we did quite a few tries, let's avoid wasting time.
54+
return;
55+
}
56+
}
57+
// Let's see if we saw them all.
58+
for val in expected {
59+
if !seen.contains(&val) {
60+
panic!("did not get value that should be possible: {val:?}");
61+
}
62+
}
63+
}
64+
65+
fn relaxed() {
66+
check_all_outcomes([0, 1, 2], || {
67+
let x = static_atomic(0);
68+
let j1 = spawn(move || {
69+
x.store(1, Relaxed);
70+
// Preemption is disabled, so the store above will never be the
71+
// latest store visible to another thread.
72+
x.store(2, Relaxed);
73+
});
74+
75+
let j2 = spawn(move || x.load(Relaxed));
76+
77+
j1.join().unwrap();
78+
let r2 = j2.join().unwrap();
79+
80+
// There are three possible values here: 0 (from the initial read), 1 (from the first relaxed
81+
// read), and 2 (the last read).
82+
r2
83+
});
84+
}
85+
86+
// https://www.doc.ic.ac.uk/~afd/homepages/papers/pdfs/2017/POPL.pdf Figure 8
87+
fn seq_cst() {
88+
check_all_outcomes([1, 3], || {
89+
let x = static_atomic(0);
90+
91+
let j1 = spawn(move || {
92+
x.store(1, Relaxed);
93+
});
94+
95+
let j2 = spawn(move || {
96+
x.store(2, SeqCst);
97+
x.store(3, SeqCst);
98+
});
99+
100+
let j3 = spawn(move || x.load(SeqCst));
101+
102+
j1.join().unwrap();
103+
j2.join().unwrap();
104+
let r3 = j3.join().unwrap();
105+
106+
// Even though we force t3 to run last, it can still see the value 1.
107+
// And it can *never* see the value 2!
108+
r3
109+
});
110+
}
111+
112+
fn initialization_write(add_fence: bool) {
113+
check_all_outcomes([11, 22], || {
114+
let x = static_atomic(11);
115+
116+
let wait = static_atomic(0);
117+
118+
let j1 = spawn(move || {
119+
x.store(22, Relaxed);
120+
// Relaxed is intentional. We want to test if the thread 2 reads the initialisation write
121+
// after a relaxed write
122+
wait.store(1, Relaxed);
123+
});
124+
125+
let j2 = spawn(move || {
126+
spin_until(wait, 1);
127+
if add_fence {
128+
fence(AcqRel);
129+
}
130+
x.load(Relaxed)
131+
});
132+
133+
j1.join().unwrap();
134+
let r2 = j2.join().unwrap();
135+
136+
r2
137+
});
138+
}
139+
140+
fn faa_replaced_by_load() {
141+
check_all_outcomes([true, false], || {
142+
// Example from https://github.com/llvm/llvm-project/issues/56450#issuecomment-1183695905
143+
pub fn rdmw(storing: &AtomicUsize, sync: &AtomicUsize, loading: &AtomicUsize) -> usize {
144+
storing.store(1, Relaxed);
145+
fence(Release);
146+
// sync.fetch_add(0, Relaxed);
147+
sync.load(Relaxed);
148+
fence(Acquire);
149+
loading.load(Relaxed)
150+
}
151+
152+
let x = static_atomic(0);
153+
let y = static_atomic(0);
154+
let z = static_atomic(0);
155+
156+
let t1 = spawn(move || rdmw(y, x, z));
157+
158+
let t2 = spawn(move || rdmw(z, x, y));
159+
160+
let a = t1.join().unwrap();
161+
let b = t2.join().unwrap();
162+
(a, b) == (0, 0)
163+
});
164+
}
165+
166+
/// Checking that the weaker release sequence example from
167+
/// <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0982r0.html> can actually produce the
168+
/// new behavior (`Some(0)` in our version).
169+
fn weaker_release_sequences() {
170+
check_all_outcomes([None, Some(0), Some(1)], || {
171+
let x = static_atomic(0);
172+
let y = static_atomic(0);
173+
174+
let t1 = spawn(move || {
175+
x.store(2, Relaxed);
176+
});
177+
let t2 = spawn(move || {
178+
y.store(1, Relaxed);
179+
x.store(1, Release);
180+
x.store(3, Relaxed);
181+
});
182+
let t3 = spawn(move || {
183+
if x.load(Acquire) == 3 {
184+
// In C++11, if we read the 3 here, and if the store of 1 was just before the store
185+
// of 3 in mo order (which it is because we fix the schedule), this forms a release
186+
// sequence, meaning we acquire the release store of 1, and we can thus never see
187+
// the value 0.
188+
// In C++20, this is no longer a release sequence, so 0 can now be observed.
189+
Some(y.load(Relaxed))
190+
} else {
191+
None
192+
}
193+
});
194+
195+
t1.join().unwrap();
196+
t2.join().unwrap();
197+
t3.join().unwrap()
198+
});
199+
}
200+
201+
pub fn main() {
202+
relaxed();
203+
seq_cst();
204+
initialization_write(false);
205+
initialization_write(true);
206+
faa_replaced_by_load();
207+
weaker_release_sequences();
208+
}

0 commit comments

Comments
 (0)