Von C zu Rust wechseln?



  • @Ethon: Du musst blind sein. Rust ist der einzige weg in eine bessere Zukunft!

    ~@dot: 😃 yay!~



  • Wieviel Performance bzw. Rechenzyklen kostet denn diese ganze Sicherheit, die Rust bieten soll?

    Was soll hier ein Entwickler machen, für den die Geschwindigkeit wichtig ist?



  • asfdlol schrieb:

    An dieser Knacknuss war ich gestern den ganzen Tag dran:

    int* Datas = new int(0);
    try
    {
    	std::cin >> *reinterpret_cast<int*>(&Datas);
    }
    catch(...)
    {
    	std::cerr << "Fehler aufgetreten, Programm wird terminiert!" << std::endl << std::endl;
    	delete Datas;
    	return -1;
    }
    f(*Datas);
    delete Datas;
    

    In Java oder in Rust wäre mir das nicht passiert...

    Wenn das kein Scherz ist, dann musst du ganz dringend eines von zwei Dingen tun:

    1. Lern C++
    2. Lass C++ bleiben


  • Elliosso schrieb:

    Wieviel Performance bzw. Rechenzyklen kostet denn diese ganze Sicherheit, die Rust bieten soll?

    So gut wie gar nichts. Temporale Speichersicherheit ist eine Sache, die komplett zur Übersetzungszeit gewährleistet wird, also ohne Laufzeitchecks auskommt. Spatiale Speichersicherheit wird, falls nötig, über Laufzeitchecks erhalten. Wenn solche Checks irgendwo stattfinden, weil sie nicht wegoptimiert wurden, dann fallen sie trotzdem nicht besonders ins Gewicht, wenn man das mal mit SoftBound+CTS vergleicht; denn in Rust gibt es keine Zeigerarithmetik im herkömmlichen Sinne, die solche Checks kompliziert werden lassen würden (Shadow Tables und co). In Rust arbeitet man in der Hinsicht mit „Slices“. Das sind „fette Zeiger“, die nicht nur auf ein einzelnes Element sondern quasi auf einen ganzen Bereich zeigen, den man nur verkleinern darf. Das einfache Iterieren über ein Array ist z.B. genauso billig wie in C, wenn man das idiomatisch über Iteratoren macht.

    Elliosso schrieb:

    Was soll hier ein Entwickler machen, für den die Geschwindigkeit wichtig ist?

    Was soll der schon machen? Deine Frage setzt voraus, dass Rust vom Sprachdesign her in der Hinsicht ein Problem hätte. Das hat es nicht. Diejenigen, denen Geschwindigkeit wichtig ist, werden nicht unbedingt ein Problem mit Rust haben. Und wenn sie z.B. über einen Profiler doch feststellen, dass das Bounds Checking an irgendeiner heißen Stelle wirklich stört, dann hoffe ich doch mal, dass in der Lage sind, diese Situation richtig zu bewerten. Zur Not gibt es eben noch get_unchecked , worüber man Slice-Elemente ohne Bounds-Checking in unsafe -Blöcken dereferenzieren kann. Das vergrößert natürlich die Angriffsfläche. Wenn man da etwas falsch macht, ist es hin mit den Sicherheitsgarantien.

    Ich finde, das ist ein sehr guter Kompromiss. Die Stellen, wo man Fehler machen kann, die Löcher in die Speichersicherheit reißen würden, sind klar als solche zu erkennen ( unsafe -Blöcke) und sollten nur einen sehr kleinen Teil des kompletten Codes ausmachen, wenn es sie überhaupt in einem Projekt gibt. Es bietet sich auch eine automatisierte Vollstreckung einer Review-Policy an, die verlangt, dass ein zusätzlicher Reviewer nochmal über den Code rüber schaut, wenn ein unsafe -Block bei einer Änderung angefasst oder neu eingeführt wurde.



  • Mal ne GANZ doofe Frage: wie geht Rust mit Kopien von immutable Typen wie z.B. (hoffentlich) Strings um? Ref-Counting? Per Library oder eingebaut? Ganz was anderes?



  • hustbaer schrieb:

    Mal ne GANZ doofe Frage: wie geht Rust mit Kopien von immutable Typen wie z.B. (hoffentlich) Strings um? Ref-Counting? Per Library oder eingebaut? Ganz was anderes?

    Wenn man nicht kopiert, sondern

    let x = irgendeinString;
    

    schreibt, wird das per move gemacht. Meistens benutzt man aber string-slices, diese bieten naemlich die gleichen operationen wie immutable strings und sind sowas wie std::string_view, aber besser in die Sprache integriert. String literale sind zum Beispiel string slices und so ersetzen sie const char* im Anwendercode komplett. Erst dann, wenn ein String kopiert wird (explizit mit .clone()), wird wirklich Speicher reserviert.
    Alleine dadurch braucht man kaum noch kopien. Ansonsten ist vieles noch nicht fertig und so sind strings zur Zeit intern noch durch ein std::Vec<u8> repraesentiert. Ich vermute, dass mindestens small string optimization noch irgendwann kommen wird.



  • Ich meine wenn man ein immutable Objekt an mehrere Stellen übergibt.
    Kopieren ist ja eher ineffizient, speziell wenn man dafür dynamisch Speicher besorgen muss.

    Muss ja irgend einen Mechanismus geben es ohne Kopieren mehreren Stellen zugänglich zu machen.
    => wie?



  • hustbaer schrieb:

    Ich meine wenn man ein immutable Objekt an mehrere Stellen übergibt.
    Kopieren ist ja eher ineffizient, speziell wenn man dafür dynamisch Speicher besorgen muss.

    Muss ja irgend einen Mechanismus geben es ohne Kopieren mehreren Stellen zugänglich zu machen.
    => wie?

    (Ich nehmen an du meinst zur Laufzeit erstellte Objekte, die immutable sein sollen)

    Da ist nichts automatisch, der Entwickler muss den Ownership selber festlegen. Wenn gewünscht ist, dass es geshared wird, dann nimmt er halt den nicht-threadsicheren shared_ptr oder den threadsicheren shared_ptr. Beide beinhalten immutable Daten.



  • hustbaer, fragst Du dich gerade nach dem Äquivalent von "const T&"? Falls ja: Das gibt's natürlich auch, nennt sich "borrowing":

    …
        let mut phonebook = HashMap::new();
        …
        foo(&phonebook);
        // phonebook is still valid at this point
        foo(&phonebook);
        …
    
    fn foo(x: &HashMap<String,String>) {
        for (k, v) in x {
            println!("{}: {}", *k, *v);
        }
    }
    

    playpen-Link

    Wenn Du jetzt eher in Richtung shared_ptr gehen willst, geht das auch:

    use std::rc::Rc;
    
    fn main() {
        let s = Rc::new("Hallo Welt".to_string());
        foo(s.clone()); // <-- clone will refer to the same object
        println!("{}", *s); // <-- s is still valid here
    }
    
    fn foo(s: Rc<String>) {
        println!("{}", *s);
    }
    

    playpen-Link

    Statt Rc<T> (reference-counted) gibt es noch Arc<T> (atomically reference-counted), mit dem man dann auch über Thread-Grenzen hinweg kann, sofern TSync ist“, also sicher von mehreren Threads aus „lesbar“ ist.

    Der Unterschied zwischen &T und Rc<T> ist natürlich, dass &T nicht besitzend sondern nur „ausgeliehen“ ist. Beide Arten von Zeigern lassen sich aber vervielfältigen (mit clone() und im ersten Falle auch ohne, weil er trivial kopierbar ist).

    Bei Vektoren und Strings gibt es noch eine andere Art des Ausleihens. Statt &Vec<T> und &String sollte man &[T] und &str verwenden, weil sie allgemeiner sind und sich nicht unbedingt auf das Innere eines Vec s oder String s beziehen müssen. Zeichenkettenliterale sind z.B. vom Typ &str . Das schöne ist, dass diese Typen besonders gut zusammen arbeiten. So ist es z.B. möglich, für das Lookup in der obigen HashMap auch einfach &str statt &String als Argumenttyp zu verwenden (weil man sich ein String -Objekt in Form eines &str „ausleihen“ kann). Ein Wert des Typs &str ist im Prinzip eine Anfangsspeicheradresse gepaart mit einer Länge, die angibt, wieviele UTF8 code units darüber addressierbar sind. &[T] ist ähnlich (Adresse + Länge). Der Unterschied zwischen str und [u8] ist aber, dass bei str garantiert wird, dass es sich um eine korrekt kodierte UTF8 Zeichenkette handelt. &str und &[T] sind diese „Slices“ oder „fat pointers“, von denen ich im vorherigen Beitrag sprach.



  • Rc<T> bzw. Arc<T> war das was ich wissen wollte.
    Also auf Library-Ebene behandelt. Schade.
    Ich glaube nämlich dass es gescheiter wäre das der VM/Runtime aufs Auge zu drücken, da man dann mehr Möglichkeiten zur Optimierung hat.



  • Es soll bald auch Gc<T> geben.



  • ownie schrieb:

    Es soll bald auch Gc<T> geben.

    Nicht mehr. Gc wurde nie vollstaendig implementiert, hat immer mit referenz-zaehlen gearbeitet und wurde irgendwann entfernt. Das war sogar schon deutlich laenger geplant, wurde aber nicht gemacht, weil es noch code in Servo und rustc gab, die gc brauchten.



  • hustbaer schrieb:

    Rc<T> bzw. Arc<T> war das was ich wissen wollte.
    Also auf Library-Ebene behandelt. Schade.

    Nee... Gut so! Es hat in Rust so angefangen, wie Du es für besser hältst. Aber man will eben auch für eine „Systemprogrammiersprache“ eine minimale Runtime haben. Und der Sprachkern war dann irgendwann mächtig genug, dass man es in Form einer Bibliothek anbieten konnte.

    hustbaer schrieb:

    Ich glaube nämlich dass es gescheiter wäre das der VM/Runtime aufs Auge zu drücken, da man dann mehr Möglichkeiten zur Optimierung hat.

    Und ich glaube, dass das überhaupt gar keine Rolle spielt. Shared ownership benutze ich selten bis gar nicht. Wo siehst Du denn da Optimierungspotential?



  • Interessanter Vortrag über Servo.
    Leider haben die zu weniger Entwicklerresourcen. Vielleicht will einer von euch mithelfen kleinere Aufgaben zu implementieren? Einigen von euch traue ich das zu!

    https://www.youtube.com/watch?v=7q9vIMXSTzc

    Viele der noch umzusetzenden Elemente sind aber eher einfache Kopien aus den jeweiligen Spezifikationen, weshalb auch viele Studenten Code als Kursaufgaben beitragen, wie Matthews erklärt.

    http://www.golem.de/news/mozilla-servo-der-wikipedia-browser-1502-112145.html



  • krümelkacker schrieb:

    hustbaer schrieb:

    Ich glaube nämlich dass es gescheiter wäre das der VM/Runtime aufs Auge zu drücken, da man dann mehr Möglichkeiten zur Optimierung hat.

    Und ich glaube, dass das überhaupt gar keine Rolle spielt. Shared ownership benutze ich selten bis gar nicht. Wo siehst Du denn da Optimierungspotential?

    Mir geht es um "immutable" Zeugs.
    Das kann man entweder kopieren, oder man braucht Shared-Ownership. Anwendungsbeispiele gibt's genügend.

    z.B. wenn man riesen XML Files laden möchte, ist es nett, wenn man die Namen von Tags nicht zigtausendfach kopiert abspeichern muss. Und hier wäre auch wichtig dass keine Performance verschenkt wird.

    Bzw. überall wo "ausborgen" nicht reicht, aber nicht kopiert werden müsste, weil halt nix geändert werden muss. z.B. wenn ich nen String (oder sonst ein Objekt) aus meiner Settings-Klasse nehme und es als Parameter für ne asynchrone Operation (HTTP Request, ...) verwende. Zugegeben, hier spielt die Performance so-gut-wie keine Rolle. Konzeptuell ist es aber das selbe. Man müsste nicht kopieren, muss aber doch. Oder man braucht Shared-Ownership.

    Oder wenn man multi-level (infinite-level) Undo implementieren will: eine Variante das zu machen ist mit Dokumenten zu arbeiten die mit "mittlerer" Granularität in Stücke aufgeteilt sind. Wobei die Stücke so gross sind dass es nicht zu sehr bremst und so klein dass nicht zu viel Overhead entsteht. Wenn man ein Dokumenten ändert, erzeugt man einfach ein neues mit 2-3 neuen Stücken und einer neuen Stückliste. Und die letzten paar, paar hundert (oder unendlich) behält man sich einfach in der Undo Liste. Ohne Sharing verbietet sich das auf Grund des Speicherbedarfs und oft auch auf Grund der Performance (komplexe riesen Dokumente zu kopieren braucht halt auch seine Zeit).

    Wobei die Anwendungen wo die Performance wirklich kritisch ist mit vielen vielen kleinen bis winzigen Objekten arbeiten, wie eben beim erwähnten XML Parser. Andere Parser werden aber vermutlich genau so betroffen sein, also speziell wenn man nen Browser damit entwickeln möchte ...

    Marthog schrieb:

    ownie schrieb:

    Es soll bald auch Gc<T> geben.

    Nicht mehr. Gc wurde nie vollstaendig implementiert, (...)

    Wieder schade.
    Ein GC im Sprachkern (zusätzlich zum deterministischen Gedöns ala C++) hat auch Vorteile wenn man hochperformante Programme schreiben will. Etliche lock-freie Datenstrukturen werden damit z.B. immens viel einfacher.
    Wobei ich diese Entscheidung noch eher verstehe. GC heisst immer dass Threads eingefroren werden müssen. Automatisch ist doof, weil nicht kontrollierbar, manuell ist doof, weils keinen schert immer wieder den GC aufzurufen - alles doof.
    Praktisch aber doof 😃



  • Zum einen kann man sehr gut generischen Code schreiben, der sowohl auf owning, als auch non-owning types funktioniert und zwischen diesen Typen notfalls auch konvertieren kann oder auf verschiedenen Arten von smartpointern bzw eigenen Typen arbeitet und es gibt es auch einen copy-on-write-pointer, der diese konvertierungen zur Laufzeit erledigt.
    Ansonsten muss man was selber schreiben. rustc verwendet statt kopien eine riesige Codemap mit geteiltem Zugriff und die Parser greifen dann hauptsaechlich auf Teile daraus zu.

    Statt garbage collector kann man in bestimmten Situationen die Datenstrukturen Arena und TypedArena, quasi pool allocator ohne free-list, die im Destructor ihren gesamten Speicher auf einmal freigeben und alle Destructors der alloziierten Objekte aufrufen. Die sind aber noch nicht richtig brauchbar, weil man noch keine eigenen allocator fuer collections angeben kann (wobei sich das auch nur bei der list lohnt).



  • Marthog schrieb:

    Statt garbage collector kann man in bestimmten Situationen die Datenstrukturen Arena und TypedArena, quasi pool allocator ohne free-list, die im Destructor ihren gesamten Speicher auf einmal freigeben und alle Destructors der alloziierten Objekte aufrufen.

    Dieser Satz ein Verb zu wenig 😃
    Aber wie funktioniert das dann mit Arena/TypedArena.
    Also wenn ich da Strings drinnen verwalten möchte.
    Wie bekomme ich den String dann da raus, ohne ihn kopieren zu müssen?

    Also angenommen mein XML Parser will ein Document zurückgeben, und das Document hat Trillionen Strings. In so einer Arena.
    Wie tu' ich da wenn ich "von aussen" einen DOM-Walk machen möchte? Oder muss ich den DOM-Walk als Funktor implementieren, so dass das Dokument den Funktor mit ausgeborgten Strings "zurückrufen" kann?
    Oder kann man sich vom Dokument auch "geborgte" Strings (und Notes und...) holen, so lange diese die garantierte Lifetime des Dokuments nicht "überschreiten"?

    Marthog schrieb:

    Ansonsten muss man was selber schreiben. rustc verwendet statt kopien eine riesige Codemap mit geteiltem Zugriff und die Parser greifen dann hauptsaechlich auf Teile daraus zu.

    Jo. Dass man was selbst schreiben kann ist klar. Das Problem dabei ist nur dass man es eben machen muss. Und dass dabei oft Dinge rauskommen die nicht so wirklich gut "interoperable" sind mit anderen Libraries/Frameworks/...



  • Eine Arena kann man wohl fuer strings nicht benutzen. Man kann aber sehr gut selber ein Byte-Array erzeugen und daraus dann jeweils string-slices zurueckgeben oder den Stringinterner aus dem rust-parser klauen. Die meisten libraries werden wohl soweit moeglich direkt auf diesen String-slices arbeiten koennen und nur wenn noetig eigenen Speicher reservieren.
    Allerdings ist es bei sowas ziemlich schwer, sicherzustellen, dass die lifetimes alle passen.
    Eine Funktion rekursiv durch den ganzen Baum zu schicken ist wohl das einfachste, aber nicht immer erweiterbar, cache-effizient und parallelisierbar.



  • hustbaer schrieb:

    krümelkacker schrieb:

    Wo siehst Du denn da Optimierungspotential?

    […]

    Nee, ich meine Optimierungspotential, welches angeblich unerschöpft bleibt, weil Rust und C++ nichts an „schlauen“ Zeigern (oder was auch immer) im Sprachkern drin haben. Da kann ich mir nämlich nicht so viel drunter vorstellen. Gut, Du hast die Behauptung aufgestellt, dass lockfreie Datenstrukturen mit GC ggf einfacher wären. Das habe ich so oder so ähnlich schon mehrfach gehört. Aber: Kennst Du dafür Belege? Würd' mich mal interessieren.



  • krümelkacker schrieb:

    Gut, Du hast die Behauptung aufgestellt, dass lockfreie Datenstrukturen mit GC ggf einfacher wären. Das habe ich so oder so ähnlich schon mehrfach gehört. Aber: Kennst Du dafür Belege? Würd' mich mal interessieren.

    Überall wo man ohne GC Hazard Pointer braucht geht's mit GC einfacher (und vermutlich auch performanter).
    http://en.wikipedia.org/wiki/Hazard_pointer


Anmelden zum Antworten