Felix Klock (@pnkfelix
), Mozilla
space: next slide; esc: overview; arrows navigate https://bit.ly/2pWTjC6
No segmentation faults
No undefined behavior
No data races
msg passing via channels
shared state (R/W-capabilities controlled via types)
use native threads... or scoped threads... or work-stealing...
Add even values in an integer range.
Rust:
fn do_partial_sum(start: i64, to_do: i64) -> i64 {
let mut result = 0;
for i in start..(start + to_do) {
if i % 2 == 0 { result += i; }
}
result
}
C++:
int64_t do_partial_sum(int64_t start, int64_t to_do) {
int64_t result = 0;
for (int64_t i = start; i < start + to_do; i++) {
if (i % 2 == 0) { result += i; }
}
return result;
}
(avoiding reduction to closed form solution; we're benchmarking!)
play: https://bit.ly/2PH9te8
let mut threads = Vec::new();
result = 0;
let mut start = 0;
while start < MAX {
let incr = if each + val > MAX { MAX - val } else { each };
threads.push(std::thread::spawn(|| {
result += do_partial_sum(start, incr);
}));
start += sum_per_thread;
}
for t in threads {
t.join().unwrap();
}
int64_t each = (MAX + thread_count - 1) / thread_count;
result = 0;
std::vector<std::thread> threads;
for (int64_t val = 0; val < MAX; val += each) {
int64_t incr = (each + val > MAX) ? (MAX - val) : each;
threads.push_back(std::thread([&, val, incr] () {
result += do_partial_sum(val, incr);
}));
}
for (int i = 0; i < thread_count; i++) {
threads[i].join();
}
threads | average time | notes |
---|---|---|
1 | (TBD) | |
4 | (TBD) | |
1000 | (TBD) |
Four runs, 1 thread
.
.
.
.
.
.
.
.
threads | average time | notes |
---|---|---|
1 | (TBD) | |
4 | (TBD) | |
1000 | (TBD) |
Four runs, 1 thread
sum of evens < 2000000000 result: 999999999000000000
1.08s
sum of evens < 2000000000 result: 999999999000000000
1.08s
sum of evens < 2000000000 result: 999999999000000000
1.07s
sum of evens < 2000000000 result: 999999999000000000
1.07s
threads | average time | notes |
---|---|---|
1 | 1.075s | |
4 | (TBD) | |
1000 | (TBD) |
Four runs, 1 thread
sum of evens < 2000000000 result: 999999999000000000
1.08s
sum of evens < 2000000000 result: 999999999000000000
1.08s
sum of evens < 2000000000 result: 999999999000000000
1.07s
sum of evens < 2000000000 result: 999999999000000000
1.07s
threads | average time | notes |
---|---|---|
1 | 1.075s | |
4 | (TBD) | |
1000 | (TBD) |
Four runs, 4 threads
.
.
.
.
.
.
.
.
threads | average time | notes |
---|---|---|
1 | 1.075s | |
4 | (TBD) | |
1000 | (TBD) |
Four runs, 4 threads
sum of evens < 2000000000 result: 999999999000000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.29s
threads | average time | notes |
---|---|---|
1 | 1.075s | |
4 | 0.290s | |
1000 | (TBD) |
Four runs, 4 threads
sum of evens < 2000000000 result: 999999999000000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.29s
threads | average time | notes |
---|---|---|
1 | 1.075s | |
4 | 0.290s | |
1000 | (TBD) |
Four runs, 1000 threads
.
.
.
.
.
.
.
.
threads | average time | notes |
---|---|---|
1 | 1.075s | |
4 | 0.290s | |
1000 | (TBD) |
Four runs, 1000 threads
sum of evens < 2000000000 result: 999999999000000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.30s
sum of evens < 2000000000 result: 999598999001000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.30s
threads | average time | notes |
---|---|---|
1 | (but | |
4 | its all | |
1000 | bogus!) |
Four runs, 1000 threads
sum of evens < 2000000000 result: 999999999000000000
0.29s
sum of evens < 2000000000 result: 999999999000000000
0.30s
sum of evens < 2000000000 result: 999598999001000000
0.29s ---------[uh oh]
sum of evens < 2000000000 result: 999999999000000000
0.30s
threads.push_back(std::thread([&, val, incr] () {
// ~
result += do_partial_sum(val, incr);
// ~~~~~~
}));
play: https://bit.ly/2PH9te8
let mut threads = Vec::new();
result = 0;
let mut start = 0;
while start < MAX {
let incr = if each + val > MAX { MAX - val } else { each };
threads.push(std::thread::spawn(|| {
result += do_partial_sum(start, incr);
}));
start += sum_per_thread;
}
for t in threads {
t.join().unwrap();
}
play: https://bit.ly/2PH9te8
let mut threads = Vec::new();
result = 0;
let mut start = 0;
while start < MAX {
let incr = if each + val > MAX { MAX - val } else { each };
threads.push(std::thread::spawn(|| {
result += do_partial_sum(start, incr);
}));
start += sum_per_thread;
}
for t in threads {
t.join().unwrap();
}
play: https://bit.ly/2PH9te8
error[E0373]: closure may outlive the current function, but it
borrows `result`, which is owned by the current function
--> partsum/src/v0.rs:47:37
|
47 | threads.push(std::thread::spawn(|| {
| ^^ may outlive borrowed
| value `result`
48 | result += do_partial_sum(start, incr);
| ------ `result` is borrowed here
help: to force the closure to take ownership of `result` (and any
other referenced variables), use the `move` keyword
(our array indexes start at 0)
play: https://bit.ly/2OuTOC8
let mut threads = Vec::new();
result = 0;
let mut start = 0;
while start < MAX {
let incr = if each + val > MAX { MAX - val } else { each };
threads.push(std::thread::spawn(move || { // <-- `move` is new
result += do_partial_sum(start, incr);
}));
start += sum_per_thread;
}
for t in threads {
t.join().unwrap();
}
play: https://bit.ly/2OuTOC8
Four runs, 1 thread
.
.
.
.
.
.
.
.
play: https://bit.ly/2OuTOC8
Four runs, 1 thread
result: 0
0.03s
result: 0
0.03s
result: 0
0.03s
result: 0
0.03s
Not this time
(sorry to disappoint)
Rust will not prove your code correct
let mut threads = Vec::new();
result = 0;
let mut start = 0;
while start < MAX {
let incr = if each + val > MAX { MAX - val } else { each };
threads.push(std::thread::spawn(move || { // <-- `move` is new
result += do_partial_sum(start, incr); // <-- `result` copied!
}));
start += sum_per_thread;
}
for t in threads {
t.join().unwrap();
}
fn choose<T>(x: T, y: T) -> T { return x; }
fn sum<T: Add>(x: T, y: T) -> T::Output { return x + y; }
fn sum3<T>(x: T, y: T, z: T) -> T where T: Add<Output=T> {
return x + y + z;
}
Type Constructions
Move | Copy | Copy if T:Copy |
---|---|---|
Vec<T> , String , ... |
i32 , char , ... |
[T; n] , (T1,T2,T3) , ... |
Ownership enables: | which removes: |
---|---|
RAII-style destructors | a source of memory leaks (or fd leaks, etc) |
no dangling pointers | many resource management bugs |
no data races | many multithreading heisenbugs |
Do I need to take ownership here, accepting the associated resource management responsibility? Would temporary access suffice?
Good developers ask this already!
Rust forces function signatures to encode the answers
(and they are checked by the compiler)
References to data
&mut T
, &T
(plus library reference types: Box<T>
, Cow<T>
, Rc<T>
, ...)
play: https://bit.ly/2Crmsx1
let mut threads = Vec::new();
result = 0;
let mut start = 0;
while start < MAX {
let result_ptr = &mut result; // <-- *borrowing* explictly
let incr = if each + start > MAX { MAX - start } else { each };
threads.push(std::thread::spawn(move || {
*result_ptr += do_partial_sum(start, incr);
}));
start += each;
}
for t in threads {
t.join().unwrap();
}
play: https://bit.ly/2Crmsx1
error[E0597]: `result` does not live long enough
--> partsum/src/v2.rs:46:35
|
46 | let result_ptr = &mut result;
| ^^^^^^ borrowed value does not
| live long enough
...
59 | }
| - borrowed value only lives until here
|
= note: borrowed value must be valid for the static lifetime...
and std::thread::spawn
creates a thread that may outlive its parent!
Do all threads terminate at arbitrary times?
play: https://bit.ly/2pZOTe4
let mut result = 0;
crossbeam::thread::scope(|the_scope| { // <-- create scope here
let mut threads = Vec::new();
let mut start = 0;
let incr = if each + start > MAX { MAX - start } else { each };
while start < MAX {
let result_ptr = &mut result; // <-- (still borrowing explicitly)
threads.push(the_scope.spawn(move || {
*result_ptr += do_partial_sum(start, incr);
}));
start += each;
}
for t in threads {
t.join().unwrap();
} // now we *know* all threads finish while `result` still on stack
});
play: https://bit.ly/2pZOTe4
error[E0499]: cannot borrow `result` as mutable more than once at a
time
--> src/main.rs:24:30
|
24 | let result_ptr = &mut result;
| ^^^^^^^^^^^ mutable borrow starts here in
| previous iteration of loop
&
-reference types?Distinguish exclusive access from shared access
Enables safe, parallel API's
Ownership | T |
|
Exclusive access | &mut T |
("mutable") |
Shared access | &T |
("read-only") |
Rust does not allow simultaneous mutable borrows of unsynchronized data
This is usually ensured via static analysis.
(But some standard library types enforce this rule via dynamic checks.)
result
Point is: Rust forces you to resolve this bug
C++ allows silent, scheduler dependent failure (which may arise only rarely)
play: https://bit.ly/2QXsCsq
let mut threads = Vec::new();
result = 0;
let mut start = 0;
while start < MAX {
let incr = if each + start > MAX { MAX - start } else { each };
threads.push(std::thread::spawn(move || {
return do_partial_sum(start, incr);
}));
start += each;
}
for t in threads {
// updates result solely on main thread, after each join.
result += t.join().unwrap();
}
play: https://bit.ly/2QXsCsq
threads | average time | notes |
---|---|---|
1 | 0.820s | Slightly faster |
4 | 0.220s | than earlier C++ code; |
1000 | 0.235s | dunno why |
Four runs, 1000 threads
sum of evens < 2000000000 result: 999999999000000000
0.23s
sum of evens < 2000000000 result: 999999999000000000
0.23s
sum of evens < 2000000000 result: 999999999000000000
0.24s
sum of evens < 2000000000 result: 999999999000000000
0.24s
Many things in Rust that leverage or extend ownership and borrowing to get fast, flexible, safe code
Send
and Sync
traitshttp://rustconf.com/, https://www.rust-belt-rust.com/ (USA)
https://rustfest.eu/ (EU)
Rust Paris: https://www.meetup.com/Rust-Paris/
Programming in Rust has made me look at C++ code in a whole new light
www.rust-lang.org |
Hack Without Fear |