Was sind Umzugssemantik in Rust?

9

In Rust gibt es zwei Möglichkeiten, eine Referenz zu nehmen

  1. Ausleihen , d. h., Sie nehmen eine Referenz, lassen aber das Referenzziel nicht mutieren. Der Operator & entleiht den Besitz von einem Wert.

  2. Ausleihen , d. h., nehmen Sie Bezug auf das Ziel zu mutieren. Der &mut -Operator übernimmt das Eigentum von einem Wert.

Die Rust-Dokumentation über Ausleihregeln lautet:

  

Erstens muss jeder Kredit für einen kleineren Umfang als der Eigentümer dauern. Zweitens, Sie können die eine oder andere dieser beiden Arten von Anleihen haben, aber nicht beide gleichzeitig:

     
  • 0 bis N Referenzen (& amp; T) zu einer Ressource.

  •   
  • genau eine veränderbare Referenz (& amp; mut T)

  •   

Ich glaube, dass das Erstellen einer Referenz einen Zeiger auf den Wert erstellt und auf den Wert durch den Zeiger zugreift. Dies könnte vom Compiler wegoptimiert werden, wenn es eine einfachere äquivalente Implementierung gibt.

Allerdings verstehe ich nicht, was move bedeutet und wie es implementiert wird.

Für Typen, die das Merkmal Copy implementieren, bedeutet das Kopieren von z. indem Sie die Struktur memberweise von der Quelle oder einem memcpy() zuweisen. Für kleine Strukturen oder für Primitive ist diese Kopie effizient.

Und für move ?

Diese Frage ist kein Duplikat von Was sind Bewegungssemantiken? , weil Rust und C ++ verschiedene Sprachen sind und die Bewegungssemantik ist Unterschied zwischen den beiden.

    
nalply 17.05.2015, 15:46
quelle

4 Antworten

9

Semantik

Rust implementiert ein Affine Type System :

  

Affine Typen sind eine Version von linearen Typen, die schwächere Bedingungen auferlegen, die affiner Logik entsprechen. Eine affine Ressource kann nur einmal verwendet werden , während eine lineare Ressource einmal verwendet werden muss.

Typen, die nicht Copy sind und daher verschoben werden, sind affine Typen: Sie können sie entweder einmal oder nie verwenden, sonst nichts.

Rust qualifiziert dies als Eigentumsübertragung in seiner eigentumszentrischen Sicht der Welt (*).

(*) Einige der Leute, die an Rust arbeiten, sind viel qualifizierter als ich in CS, und sie haben wissentlich ein Affines Type System implementiert; Im Gegensatz zu Haskell, der die mathematischen / cs-y-Konzepte aufdeckt, neigt Rust dazu, pragmatischere Konzepte zu enthüllen.

Hinweis: Es könnte argumentiert werden, dass Affine-Typen, die von einer mit #[must_use] getaggten Funktion zurückgegeben werden, tatsächlich lineare Typen sind.

Implementierung

Es kommt darauf an. Bedenken Sie bitte, dass Rust eine Sprache ist, die auf Geschwindigkeit ausgelegt ist, und es gibt zahlreiche Optimierungen, die hier ausgeführt werden, die von dem verwendeten Compiler abhängen (in unserem Fall rustc + LLVM).

Innerhalb eines Funktionskörpers ( link ):

%Vor%

Wenn Sie die LLVM IR (in Debug) überprüfen, sehen Sie:

%Vor%

So ruft rustc unterhalb der Covers ein memcpy von s bis t auf. Es mag zwar ineffizient sein, wenn Sie die gleiche IR im Freigabemodus überprüfen, werden Sie feststellen, dass LLVM die Kopie beendet hat (dabei ist s nicht verwendet).

Die gleiche Situation tritt auf, wenn eine Funktion aufgerufen wird: Theoretisch "verschiebt" man das Objekt in den Funktionsstapelrahmen. In der Praxis kann der rustc-Compiler jedoch, wenn das Objekt groß ist, stattdessen einen Zeiger übergeben.

Eine andere Situation ist Rückgabe von einer Funktion, aber selbst dann könnte der Compiler "Rückgabewertoptimierung" anwenden und direkt im Stapelrahmen des Aufrufers aufbauen - das heißt, der Aufrufer übergibt einen Zeiger, in den um den Rückgabewert zu schreiben, der ohne Zwischenspeicher verwendet wird.

Die Besitz- / Entleihungsbedingungen von Rust ermöglichen Optimierungen, die in C ++ schwer zu erreichen sind (die auch RVO hat, aber nicht in vielen Fällen anwenden kann).

Also, die Digest-Version:

  • das Bewegen großer Objekte ist ineffizient, aber es gibt eine Reihe von Optimierungen, die die Verschiebung insgesamt verhindern können
  • moving beinhaltet ein memcpy von std::mem::size_of::<T>() bytes, also ist das Verschieben eines großen String effizient, weil es nur ein paar Bytes ist, egal wie groß der zugewiesene Puffer ist, den sie auf
  • speichern
Matthieu M. 17.05.2015 17:48
quelle
6

Wenn Sie ein Element verschieben , übertragen Sie das Eigentum dieses Elements. Das ist eine Schlüsselkomponente von Rust.

Nehmen wir an, ich hätte eine Struktur und dann die Struktur von einer Variablen zu einer anderen. Standardmäßig wird dies ein Zug sein, und ich habe die Eigentumsrechte übertragen. Der Compiler wird diesen Eigentümerwechsel verfolgen und verhindern, dass ich die alte Variable mehr verwende:

%Vor%
  

wie es implementiert wird.

Das Bewegen von etwas braucht nicht etwas zu tun. In dem obigen Beispiel gäbe es keinen Grund, irgendwo tatsächlich Speicherplatz zuzuweisen und dann die zugewiesenen Daten zu verschieben, wenn ich sie einer anderen Variablen zuweise. Ich weiß eigentlich nicht, was der Compiler macht, und wahrscheinlich ändert er sich auf der Ebene der Optimierung.

Aus praktischen Gründen können Sie jedoch denken, dass wenn Sie etwas bewegen, die Bits, die dieses Element darstellen, wie über memcpy dupliziert werden. Dies hilft zu erklären, was passiert, wenn Sie eine Variable an eine Funktion übergeben, die verbraucht oder wenn Sie einen Wert von einer Funktion zurückgeben (wiederum kann der Optimierer andere Dinge tun, um ihn effizienter zu machen konzeptionell):

%Vor%

"Aber warte!", sagst du, " memcpy kommt nur ins Spiel mit Typen, die Copy !" implementieren. Das ist meistens richtig, aber der große Unterschied ist, dass, wenn ein Typ Copy implementiert, sowohl die Quelle als auch das Ziel nach der Kopie gültig sind!

Eine Art zu denken, die Semantik von Bewegungen ist dieselbe wie die Semantik von Kopien, aber mit der zusätzlichen Einschränkung, dass das Objekt, von dem aus es verschoben wird, kein gültiges Objekt mehr ist.

Es ist jedoch oft einfacher, anders darüber nachzudenken: Die grundlegendste Sache, die Sie tun können, ist das Verschieben / den Besitz wegzugeben, und die Fähigkeit, etwas zu kopieren, ist ein zusätzliches Privileg. So modelliert Rust das.

Das ist eine schwierige Frage für mich! Nach der Verwendung von Rust für eine Weile ist die Bewegungssemantik natürlich. Lass mich wissen, welche Teile ich weggelassen oder schlecht erklärt habe.

    
Shepmaster 17.05.2015 16:04
quelle
1

Bitte lassen Sie mich meine eigene Frage beantworten. Ich hatte Probleme, aber indem ich hier eine Frage stellte, tat ich Gummiente Problemlösung . Jetzt verstehe ich:

Ein move ist eine Eigentumsübertragung des Werts.

Zum Beispiel die Zuweisung let x = a; überträgt Besitz: Zuerst a besaß den Wert. Nach dem let ist es x , wem der Wert gehört. Rust verbietet, a danach zu verwenden.

Wenn Sie println!("a: {:?}", a); nach let eingeben, sagt der Rust-Compiler:

%Vor%

Vollständiges Beispiel:

%Vor%

Und was bedeutet diese Bewegung ?

Es scheint, dass das Konzept von C ++ 11 kommt. Ein Dokument über die C ++ - Semantik zum Verschieben lautet:

  

Aus Sicht des Client-Codes bedeutet das Wählen von Verschieben anstelle von Kopieren, dass es Ihnen egal ist, was mit dem Status der Quelle geschieht.

Aha. C ++ 11 ist es egal, was mit der Quelle passiert. In diesem Sinne kann Rust frei entscheiden, die Quelle nach einer Bewegung zu verbieten.

Und wie ist es implementiert?

Ich weiß es nicht. Aber ich kann mir vorstellen, dass Rust buchstäblich nichts tut. x ist nur ein anderer Name für denselben Wert. Namen werden normalerweise kompiliert (außer natürlich Debugging-Symbolen). Es ist also der gleiche Maschinencode, ob die Bindung den Namen a oder x hat.

Es sieht so aus, als ob C ++ im Kopierkonstruktor Elision ausführt.

Nichts tun ist so effizient wie möglich.

    
nalply 17.05.2015 16:54
quelle
1

Das Übergeben eines Werts an die Funktion führt auch zu einer Eigentumsübertragung; es ist sehr ähnlich zu anderen Beispielen:

%Vor%

Daher der erwartete Fehler:

%Vor%     
Akavall 17.05.2015 17:25
quelle

Tags und Links