curl https://sh.rustup.rs -sSf | sh
Stefan Lankes | Jens Breitbart |
---|---|
System Software @ RWTH | Here for fun and no profit |
Einstieg: https://www.rust-lang.org
Playground: https://play.rust-lang.org
Tutorial: https://rustbyexample.com
curl https://sh.rustup.rs -sSf | sh
Rust ist eine (relativ) neue Programmiersprache für systemnahe Software
fn main() {
// Die Statements werden ausgeführt sobald
// das compilierte Binary gestartet wird.
// Ausgabe auf stdout
println!("Hello para//el 2018!");
}
Bekannt u.a. für den Einsatz in Firefox
⇒ Rust Code läuft somit auf Millionen von Rechnern
Rust ist ein open-source (MIT + Apache) Projekt
Wird aktuell primär von Mozilla Research gesponsort
Die Weiterentwicklung selbst wird allerdings stark durch die Community getrieben
C/C++ ähnliche Performance
Compilerbasierte Überprüfungen welche z.B.
Speichersicherheit (ohne Garbage Collection) garantieren
Data Races verhindern
⇒ Falscher Code compiliert nicht
#[repr(C)]
struct RustObject {
number: c_int
}
#[link(name = "libprinto")]
extern {
fn c_print_object(object: *mut RustObject) -> c_int;
}
fn main() {
let mut rust_object = /* TODO */;
unsafe { c_print_object(&mut *rust_object); }
}
std::vector<std::string>* x = nullptr;
{
std::vector<std::string> z;
z.push_back("Hello para//el 2018!");
x = &z;
}
std::cout << (*x)[0] << std::endl;
Ist dieses C++-Beispiel problematisch?
let x;
{
let z = vec!("Hello para//el 2018!");
x = &z;
}
println!("{}", x[0]);
error[E0597]: `z` does not live long enough
--> src/main.rs:9:8
|
9 | x = &z;
| ^ borrowed value does not live long enough
10 | }
| - `z` dropped here while still borrowed
...
13 | }
| - borrowed value needs to live until here
Variablen werden an einen Besitzer (Owner) gebunden
Wird der Scope des Besitzers verlassen, wird die Variable freigeben
Yehuda Katz: Ownership is the right to destroy
Mit Hilfe von Referenzen kann der Besitzt ausgeliehen werden
Der Besitz geht automatisch wieder zurück, wenn die Referenz nicht mehr existiert
let mut x = vec!("Hello para//el 2018!");
{
let z = &mut x;
// Do something with z...
}
println!("{}", x[0]);
Für num_steps Rechtecke die Höhen bestimmen
Höhen Aufsummieren, zum Schluß mit der Breite multiplizieren
const int num_steps = 100000000;
double sum = 0.0;
double step = 1.0 / static_cast<double>(num_steps);
for (int i = 0; i < num_steps; ++i) {
double x = (i + 0.5) * step;
sum += 4.0 / (1.0 + x * x);
}
std::cout << "Pi = " << sum * step << std::endl;
Äquivalenter Code in Rust
const NUM_STEPS: u64 = 100000000;
let step = 1.0 / NUM_STEPS as f64;
let mut sum = 0.0;
for i in 0..NUM_STEPS {
let x = (i as f64 + 0.5) * step;
sum += 4.0 / (1.0 + x * x);
}
println!("Pi: {}", sum * step);
Verteilung der Rechtecke über die Threads
Hier: Wettlaufsituation um die Variable sum
const double step = 1.0 / NUM_STEPS;
double sum = 0.0;
std::thread t([&](int start, int end){
for (int i = start; i < end; i++) {
double x = (i + 0.5) * step;
sum += 4.0 / (1.0 + x * x);
}
}, (NUM_STEPS / nthreads) * tid
, (NUM_STEPS / nthreads) * (tid + 1));
Versuch einer Wettlaufsituation in Rust
let step = 1.0 / NUM_STEPS as f64;
let mut sum = 0.0 as f64;
let threads: Vec<_> = (0..nthreads)
.map(|tid| {
thread::spawn(|| {
let start = (NUM_STEPS / nthreads) * tid;
let end = (NUM_STEPS / nthreads) * (tid+1);
for i in start..end {
let x = (i as f64 + 0.5) * step;
sum += 4.0 / (1.0 + x * x);
}
})
}).collect();
for t in threads {
t.join().unwrap();
}
Ausgeliehene Objekte könnten das Original überleben
|
37 | thread::spawn(|| {
| ^^ may outlive borrowed value `**step`
...
42 | let x = (i as f64 + 0.5) * step;
| ---- `**step` is borrowed
help: to force the closure to take ownership of `**step`
|
37 | thread::spawn(move || {
| ^^^^^^^
⇒ Ein std::thread darf nur auf Variablen zugreifen die er besitzt oder welche static lifetime haben
Übergabe der Ownership (Compiler Vorschlag)
let step = 1.0 / NUM_STEPS as f64;
let mut sum = 0.0 as f64;
let threads: Vec<_> = (0..nthreads)
.map(|tid| {
thread::spawn(move || {
let start = (NUM_STEPS / nthreads) * tid;
let end = (NUM_STEPS / nthreads) * (tid+1);
for i in start..end {
let x = (i as f64 + 0.5) * step;
sum += 4.0 / (1.0 + x * x);
}
})
}).collect();
Objekte werden als unveränderliche übergeben
Wettlaufsituation wird verhindert
Keine Lösung für die Pi-Berechnung
error: cannot assign to immutable captured outer variable
|
43 | sum += 4.0 / (1.0 + x * x);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
Statische Element können gelesen werden
unsafe-Blöcke für Änderungen zwingend nötig
Entwickler wird sich den Gefahren bewußt
static readonly_number: u64 = 42;
static mut counter: u64 = 0;
pub fn init() {
let i = readonly_number;
unsafe {
counter = i;
}
}
Rust-Mutexe nehmen zu schützendes Objekt auf
lock-Methode liefert Objekt zum Zugriff zurück
Automatische Freigabe nach Zerstörung des Objekts
static readonly_number: u64 = 42;
static counter: Mutex<u64> = Mutex::new(0);
pub fn init() {
let guard = counter.lock().unwrap();
guard = readonly_number;
}
RWLock bietet ein ähnliches Interface.
Heap-Allokation ermöglicht längere Lebenszeit
Speicherschutz über reference counting
std::{Rc|Arc}<T>
alloziert T
auf dem Heap
std::Arc
is thread-sicher
scoped Threads aus dem Crossbeam crate (~OpenMP Thread Model) ermöglicht teilen von Stackvariablen
⇒ Bis jetzt aber nur unveränderliche Variablen
Atomare Variablen (std::sync::atomic::*
)
Schwierig zu benutzen
Folgt dem C11 Speichermodell mit Acquire/Release Semantik
let reference_count: AtomicUsize = 0;
reference_count.fetch_add(1, Ordering::Relaxed);
Grundsätzlich: Der trait std::marker::Sync
muß für gemeinsame veränderliche Variablen implementiert werden
let sum = Arc::new(Mutex::new(0.0 as f64));
let threads: Vec<_> = (0..nthreads).map(|tid| {
let sum = sum.clone();
thread::spawn(move || {
let start = (NUM_STEPS / nthreads) * tid;
let end = (NUM_STEPS / nthreads) * (tid+1);
for i in start..end {
let x = (i as f64 + 0.5) * step;
*sum.lock().unwrap() += 4.0 / (1.0 + x * x);
}
})
}).collect();
Der Mutex serialisiert die Berechnung
Idee: Teilergebnisse berechnen & zusammenführen
let step = 1.0 / NUM_STEPS as f64;
let sum = 0.0 as f64;
let threads: Vec<_> = (0..nthreads)
.map(|tid| {
thread::spawn(move || {
let mut partial_sum = 0 as f64;
for i in start..end {
let x = (i as f64 + 0.5) * step;
partial_sum += 4.0 / (1.0 + x * x);
}
partial_sum
})}).collect();
Ergebnisse der Threads stehen beim join
zur Verfügung
for t in threads {
sum += t.join().unwrap();
}
Ergebnisse durch Kanäle zusammenführen
Analogie zu Communicating Sequential Processes (CSP) und Coroutines
fn term(start: u64, end: u64) -> f64
{
let step = 1.0 / NUM_STEPS as f64;
let mut sum = 0.0;
for i in start..end {
let x = (i as f64 + 0.5) * step;
sum += 4.0 / (1.0 + x * x);
}
sum
}
Teilergebnisse berechnen und versenden
let (tx, rx) = mpsc::channel();
for id in 0..nthreads {
let thread_tx = tx.clone();
let start = (NUM_STEPS / nthreads as u64) * id;
let end = (NUM_STEPS / nthreads as u64) * (id+1);
thread::spawn(move || {
let partial_sum = term(start, end);
thread_tx.send(partial_sum).unwrap();
});
};
Ergebnisse empfangen und aufaddieren
let mut sum = 0.0;
for _ in 0..nthreads {
sum = sum + rx.recv().unwrap();
}
Diese Lösung skaliert wie gewünscht
Wie sieht es mit Lastbalanzierung aus?
Unterstützt parallele Berechungen auf Basis von Task-Parallelität und Work Stealing
Ähnlich zu Cilk (daher auch der Name)
Biete aber auch Daten-Parallelität über Iteratoren
Serielle Pi-Berechnung mit Hilfe von Iteratoren
let step = 1.0 / NUM_STEPS as f64;
let sum: f64 = (0..NUM_STEPS).into_iter()
.map(|i| {
let x = (i as f64 + 0.5) * step;
4.0 / (1.0 + x * x)
}).sum();
println!("Pi: {}", sum * (1.0 / NUM_STEPS as f64));
Parallele Pi-Berechnung mit Rayon
Lohnt sich nur bei größeren Aufgaben
let step = 1.0 / NUM_STEPS as f64;
let sum: f64 = (0..NUM_STEPS).into_par_iter()
.map(|i| {
let x = (i as f64 + 0.5) * step;
4.0 / (1.0 + x * x)
}).sum();
println!("Pi: {}", sum * (1.0 / NUM_STEPS as f64));
Wärme-Verteilung innerhalb einer Platte
Gelöst mit Jacobi Over Relaxation (JOR)
Ownership / Borrowing ist für einen old school Entwickler gewönnungsbedürftig
Fearless concurency
Der Compiler verhindert race conditions
std hat Threads, Mutex, RW Lock und Arc
Rayon und Crossbeam vereinfachen viele Aufgaben