Rust: Allokatormerkmale und std :: heap

Erstellt am 8. Apr. 2016  ·  412Kommentare  ·  Quelle: rust-lang/rust

📢 Diese Funktion verfügt über eine eigene Arbeitsgruppe . Bitte richten Sie Kommentare und Bedenken an das Repo der Arbeitsgruppe .

Ursprünglicher Beitrag:


FCP-Vorschlag: https://github.com/rust-lang/rust/issues/32838#issuecomment -336957415
FCP-Kontrollkästchen: https://github.com/rust-lang/rust/issues/32838#issuecomment -336980230


Tracking-Problem für rust-lang / rfcs # 1398 und das Modul std::heap .

  • [x] landen struct Layout , trait Allocator und Standardimplementierungen in alloc crate (https://github.com/rust-lang/rust/pull/42313)
  • [x] entscheiden, wo Teile leben sollen (z. B. hat die Standardimplementierung eine Abhängigkeit von alloc crate, aber Layout / Allocator _kann_ in libcore ...) (https://github.com/rust-lang/rust/pull/42313)
  • [] Fixme aus dem Quellcode: Standardimplementierungen prüfen (in Layout auf Überlauffehler (möglicherweise nach Bedarf auf overflowing_add und overflowing_mul umschalten).
  • [x] entscheiden, ob realloc_in_place durch grow_in_place und shrink_in_place ( Kommentar ) (https://github.com/rust-lang/rust/pull/42313)
  • [] Argumente für / gegen den zugehörigen Fehlertyp überprüfen (siehe Unterfaden hier )
  • [] Bestimmen Sie, welche Anforderungen an die Ausrichtung von fn dealloc . (Siehe Diskussion zu Allocator RFC und Global Allocator RFC und Merkmal Alloc PR .)

    • Ist es erforderlich, die Zuordnung zu den genauen align aufzuheben, denen Sie zuweisen? Es wurden Bedenken geäußert, dass Allokatoren wie jemalloc dies nicht benötigen, und es ist schwierig, sich einen Allokator vorzustellen, der dies erfordert. ( mehr Diskussion ). @ruuda und @rkruppe scheinen die meisten Gedanken dazu zu haben.

  • [] sollte AllocErr stattdessen Error ? ( Kommentar )
  • [x] Muss die Zuordnung mit der genauen Größe, die Sie zuweisen, aufgehoben werden? Mit dem usable_size -Geschäft möchten wir beispielsweise zulassen, dass Sie, wenn Sie (size, align) zuweisen, eine Zuordnung mit einer Größe irgendwo im Bereich von size...usable_size(size, align) vornehmen müssen. Es scheint, dass jemalloc damit völlig einverstanden ist (es ist nicht erforderlich, dass Sie die Zuordnung zu einem genauen size aufheben, dem Sie zuweisen), und dies würde es auch Vec ermöglichen, die überschüssige Kapazität jemalloc natürlich auszunutzen gibt es, wenn es eine Zuordnung macht. (Obwohl dies tatsächlich auch etwas orthogonal zu dieser Entscheidung ist, stärken wir nur Vec ). Bisher hat @Gankro die meisten Gedanken dazu. ( @alexcrichton glaubt, dass dies aufgrund der Definition von "Passungen" in https://github.com/rust-lang/rust/pull/42313 geregelt wurde)
  • [] ähnlich wie bei der vorherigen Frage: Ist es erforderlich, die Zuordnung mit der genauen Ausrichtung aufzuheben, die Sie zugewiesen haben? (Siehe Kommentar vom 5. Juni 2017 )
  • [x] OSX / alloc_system ist bei großen Ausrichtungen fehlerhaft (z. B. eine Ausrichtung von 1 << 32 ) https://github.com/rust-lang/rust/issues/30170 # 43217
  • [] Sollte Layout eine fn stride(&self) -Methode bereitstellen? (Siehe auch https://github.com/rust-lang/rfcs/issues/1397, https://github.com/rust-lang/rust/issues/17027)
  • [x] Allocator::owns als Methode? https://github.com/rust-lang/rust/issues/44302

Status von std::heap nach https://github.com/rust-lang/rust/pull/42313 :

pub struct Layout { /* ... */ }

impl Layout {
    pub fn new<T>() -> Self;
    pub fn for_value<T: ?Sized>(t: &T) -> Self;
    pub fn array<T>(n: usize) -> Option<Self>;
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
    pub fn align_to(&self, align: usize) -> Self;
    pub fn padding_needed_for(&self, align: usize) -> usize;
    pub fn repeat(&self, n: usize) -> Option<(Self, usize)>;
    pub fn extend(&self, next: Self) -> Option<(Self, usize)>;
    pub fn repeat_packed(&self, n: usize) -> Option<Self>;
    pub fn extend_packed(&self, next: Self) -> Option<(Self, usize)>;
}

pub enum AllocErr {
    Exhausted { request: Layout },
    Unsupported { details: &'static str },
}

impl AllocErr {
    pub fn invalid_input(details: &'static str) -> Self;
    pub fn is_memory_exhausted(&self) -> bool;
    pub fn is_request_unsupported(&self) -> bool;
    pub fn description(&self) -> &str;
}

pub struct CannotReallocInPlace;

pub struct Excess(pub *mut u8, pub usize);

pub unsafe trait Alloc {
    // required
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);

    // provided
    fn oom(&mut self, _: AllocErr) -> !;
    fn usable_size(&self, layout: &Layout) -> (usize, usize);
    unsafe fn realloc(&mut self,
                      ptr: *mut u8,
                      layout: Layout,
                      new_layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn alloc_zeroed(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn alloc_excess(&mut self, layout: Layout) -> Result<Excess, AllocErr>;
    unsafe fn realloc_excess(&mut self,
                             ptr: *mut u8,
                             layout: Layout,
                             new_layout: Layout) -> Result<Excess, AllocErr>;
    unsafe fn grow_in_place(&mut self,
                            ptr: *mut u8,
                            layout: Layout,
                            new_layout: Layout) -> Result<(), CannotReallocInPlace>;
    unsafe fn shrink_in_place(&mut self,
                              ptr: *mut u8,
                              layout: Layout,
                              new_layout: Layout) -> Result<(), CannotReallocInPlace>;

    // convenience
    fn alloc_one<T>(&mut self) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn dealloc_one<T>(&mut self, ptr: Unique<T>)
        where Self: Sized;
    fn alloc_array<T>(&mut self, n: usize) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn realloc_array<T>(&mut self,
                               ptr: Unique<T>,
                               n_old: usize,
                               n_new: usize) -> Result<Unique<T>, AllocErr>
        where Self: Sized;
    unsafe fn dealloc_array<T>(&mut self, ptr: Unique<T>, n: usize) -> Result<(), AllocErr>
        where Self: Sized;
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}
B-RFC-approved B-unstable C-tracking-issue Libs-Tracked T-lang T-libs disposition-merge finished-final-comment-period

Hilfreichster Kommentar

@alexcrichton Die Entscheidung, von -> Result<*mut u8, AllocErr> auf -> *mut void zu wechseln, kann für Leute, die die ursprüngliche Entwicklung der Allokator-RFCs verfolgt haben, eine bedeutende Überraschung sein.

Ich bin nicht anderer Meinung als die Punkte, die Sie machen , aber es schien dennoch, als wäre eine ganze Reihe von Menschen bereit gewesen, mit der "Schwergewichtigkeit" von Result über die erhöhte Wahrscheinlichkeit zu leben, eine Null zu verpassen Überprüfen Sie den zurückgegebenen Wert.

  • Ich ignoriere die vom ABI auferlegten Probleme mit der Laufzeiteffizienz, da ich wie @alexcrichton davon lösen könnten.

Gibt es eine Möglichkeit, die Sichtbarkeit dieser späten Änderung allein zu verbessern?

Ein Weg (aus dem Kopf): Ändern Sie die Signatur jetzt in einer PR für sich selbst in der Hauptniederlassung, während Allocator immer noch instabil ist. Und dann sehen Sie, wer sich über die PR beschwert (und wer feiert!).

  • Ist das zu hartnäckig? Es scheint per Definition weniger hartnäckig zu sein, als eine solche Änderung mit einer Stabilisierung zu verbinden ...

Alle 412 Kommentare

Ich habe leider nicht genau genug darauf geachtet, dies in der RFC-Diskussion zu erwähnen, aber ich denke, dass realloc_in_place durch zwei Funktionen ersetzt werden sollte, grow_in_place und shrink_in_place für zwei Gründe dafür:

  • Ich kann mir keinen einzigen Anwendungsfall vorstellen (kurz vor der Implementierung von realloc oder realloc_in_place ), bei dem nicht bekannt ist, ob die Größe der Zuweisung zunimmt oder abnimmt. Durch die Verwendung spezialisierterer Methoden wird etwas klarer, was los ist.
  • Die Codepfade zum Erhöhen und Verkleinern von Zuordnungen sind in der Regel radikal unterschiedlich. Beim Vergrößern wird geprüft, ob benachbarte Speicherblöcke frei sind, und sie beansprucht, während beim Verkleinern Teilblöcke mit der richtigen Größe entfernt und freigegeben werden. Während die Kosten für eine Zweigstelle innerhalb von realloc_in_place recht gering sind, erfasst die Verwendung von grow und shrink die unterschiedlichen Aufgaben, die ein Allokator ausführen muss, besser.

Beachten Sie, dass diese abwärtskompatibel neben realloc_in_place hinzugefügt werden können. Dies würde jedoch einschränken, welche Funktionen standardmäßig in Bezug auf welche anderen implementiert werden.

Aus Gründen der Konsistenz möchte realloc wahrscheinlich auch in grow und split , aber der einzige Vorteil einer überladbaren realloc -Funktion, die ich kenne ist in der Lage zu sein, die Remap-Option von mmap zu verwenden, die keine solche Unterscheidung aufweist.

Außerdem denke ich, dass die Standardimplementierungen von realloc und realloc_in_place leicht angepasst werden sollten - anstatt mit usable_size , sollte realloc zuerst versuchen realloc_in_place . Im Gegenzug sollte realloc_in_place standardmäßig mit der verwendbaren Größe verglichen werden und im Falle einer kleinen Änderung den Erfolg zurückgeben, anstatt allgemein einen Fehler zurückzugeben.

Dies macht es einfacher, eine Hochleistungsimplementierung von realloc zu erstellen: Alles, was erforderlich ist, ist die Verbesserung von realloc_in_place . Die Standardleistung von realloc leidet jedoch nicht, da die Prüfung gegen usable_size weiterhin durchgeführt wird.

Ein weiteres Problem: Das Dokument für fn realloc_in_place besagt, dass wenn es Ok zurückgibt, man sicher ist, dass ptr jetzt zu new_layout "passt".

Für mich bedeutet dies, dass überprüft werden muss, ob die Ausrichtung der angegebenen Adresse mit einer durch new_layout implizierten Einschränkung übereinstimmt.

Ich glaube jedoch nicht, dass die Spezifikation für die zugrunde liegende fn reallocate_inplace -Funktion impliziert, dass _it_ eine solche Prüfung durchführen wird.

  • Darüber hinaus erscheint es vernünftig, dass jeder Kunde, der sich mit fn realloc_in_place , selbst dafür sorgt, dass die Ausrichtungen funktionieren (in der Praxis bedeutet dies vermutlich, dass für den jeweiligen Anwendungsfall überall dieselbe Ausrichtung erforderlich ist ...).

Sollte die Implementierung von fn realloc_in_place wirklich mit der Überprüfung belastet sein, ob die Ausrichtung der angegebenen ptr mit der von new_layout kompatibel ist? In diesem Fall (bei dieser einen Methode) ist es wahrscheinlich besser, diese Anforderung an den Anrufer zurückzusenden ...

@gereeter du machst gute Punkte; Ich werde sie der Checkliste hinzufügen, die ich in der Problembeschreibung sammle.

(An diesem Punkt warte ich auf die Unterstützung von #[may_dangle] um mit dem Zug in den Kanal beta , damit ich ihn dann im Rahmen der Allokatorintegration für Standard-Sammlungen verwenden kann.)

Ich bin neu bei Rust, also vergib mir, wenn dies an anderer Stelle besprochen wurde.

Gibt es Überlegungen zur Unterstützung objektspezifischer Allokatoren? Einige Allokatoren wie Platten-Allokatoren und Magazin-Allokatoren sind an einen bestimmten Typ gebunden und erledigen die Arbeit, neue Objekte zu konstruieren, konstruierte Objekte zwischenzuspeichern, die "freigegeben" wurden (anstatt sie tatsächlich zu löschen), bereits konstruierte zwischengespeicherte Objekte zurückzugeben und Löschen von Objekten, bevor der zugrunde liegende Speicher bei Bedarf an einen zugrunde liegenden Allokator freigegeben wird.

Derzeit enthält dieser Vorschlag nichts in der Art von ObjectAllocator<T> , wäre aber sehr hilfreich. Insbesondere arbeite ich an einer Implementierung einer Objekt-Caching-Ebene für Magazin-Allokatoren (Link oben). Dabei kann ich nur ein Allocator umbrechen und Objekte im Caching erstellen und ablegen Ebene selbst, es wäre großartig, wenn ich auch andere Objektzuweiser (wie einen Plattenzuweiser) umschließen und wirklich eine generische Caching-Ebene sein könnte.

Wo würde ein Objektzuordnungstyp oder -merkmal in diesen Vorschlag passen? Würde es für einen zukünftigen RFC übrig bleiben? Etwas anderes?

Ich glaube, das wurde noch nicht besprochen.

Sie können Ihr eigenes ObjectAllocator<T> schreiben und dann impl<T: Allocator, U> ObjectAllocator<U> for T { .. } ausführen, sodass jeder reguläre Allokator als objektspezifischer Allokator für alle Objekte dienen kann.

Zukünftige Arbeiten würden darin bestehen, Sammlungen so zu ändern, dass Ihr Merkmal für ihre Knoten verwendet wird, anstatt nur einfache (generische) Allokatoren.

@pnkfelix

(An dieser Stelle warte ich auf die Unterstützung von # [may_dangle], um den Zug in den Beta-Kanal zu fahren, damit ich ihn dann im Rahmen der Allokator-Integration für Standard-Sammlungen verwenden kann.)

Ich denke das ist passiert?

@ Ericson2314 Ja, mein eigenes zu schreiben ist definitiv eine Option für experimentelle Zwecke, aber ich denke, es wäre viel ObjectAllocator<T> Merkmal oder so etwas eine Diskussion wert ist. Obwohl es scheint, dass es für einen anderen RFC am besten ist? Ich bin nicht besonders vertraut mit den Richtlinien, wie viel in einen einzelnen RFC gehört und wann Dinge in separate RFCs gehören ...

@ Joshlf

Wo würde ein Objektzuordnungstyp oder -merkmal in diesen Vorschlag passen? Würde es für einen zukünftigen RFC übrig bleiben? Etwas anderes?

Ja, es wäre ein weiterer RFC.

Ich bin nicht besonders vertraut mit den Richtlinien, wie viel in einen einzelnen RFC gehört und wann Dinge in separate RFCs gehören ...

Dies hängt vom Umfang des RFC selbst ab, der von der Person, die ihn schreibt, festgelegt wird. Anschließend wird von allen Feedback gegeben.

Da es sich jedoch um ein Tracking-Problem für diesen bereits akzeptierten RFC handelt, ist das Nachdenken über Erweiterungen und Designänderungen nicht wirklich für diesen Thread geeignet. Sie sollten ein neues im RFCs-Repo öffnen.

@joshlf Ah, ich dachte, ObjectAllocator<T> sollte ein Merkmal sein. Ich meinte Prototyp des Merkmals, kein spezifischer Allokator. Ja, dieses Merkmal würde einen eigenen RFC verdienen , wie


@steveklabnik ja jetzt wäre die Diskussion woanders besser. @Joshlf sprach jedoch auch das Problem an, damit kein bisher unvorhergesehener Fehler im akzeptierten, aber nicht implementierten API-Design aufgedeckt wird. In diesem Sinne stimmt es mit den früheren Beiträgen in diesem Thread überein.

@ Ericson2314 Ja, ich dachte das ist was du

@steveklabnik Hört sich gut an; Ich werde mich mit meiner eigenen Implementierung beschäftigen und einen RFC einreichen, wenn dies eine gute Idee ist.

@joshlf Ich habe keinen Grund, warum benutzerdefinierte

@alexreg Hier geht es nicht um einen bestimmten benutzerdefinierten Allokator, sondern um ein Merkmal, das den Typ aller Allocator ) definiert, das der Typ eines Allokators auf niedriger Ebene ist, frage ich nach einem Merkmal ( ObjectAllocator<T> ), das der Typ eines Allokators ist, der kann Objekte vom Typ T zuweisen / freigeben und konstruieren / löschen.

@alexreg Siehe meinen frühen Punkt zur Verwendung von Standardbibliothekssammlungen mit benutzerdefinierten objektspezifischen Allokatoren.

Sicher, aber ich bin mir nicht sicher, ob das in die Standardbibliothek gehören würde. Könnte leicht in eine andere Kiste gehen, ohne Verlust der Funktionalität oder Benutzerfreundlichkeit.

Am 4. Januar 2017, um 21:59 Uhr, schrieb Joshua Liebow-Feeser [email protected] :

@alexreg https://github.com/alexreg Hier geht es nicht um einen bestimmten benutzerdefinierten Allokator, sondern um ein Merkmal, das den Typ aller Allokatoren angibt, die für einen bestimmten Typ parametrisch sind. Genau wie RFC 1398 ein Merkmal (Allocator) definiert, das der Typ eines Allokators auf niedriger Ebene ist, frage ich nach einem Merkmal (ObjectAllocator)) Dies ist der Typ eines Allokators, der Objekte vom Typ T zuordnen / freigeben und konstruieren / löschen kann.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499064 an oder schalten Sie den Thread https://github.com/notifications/unsubscribe-auth/ stumm.

Ich denke, Sie möchten Standardbibliothekssammlungen (jeden Heap-zugewiesenen Wert) mit einem beliebigen benutzerdefinierten Allokator verwenden. dh nicht auf objektspezifische beschränkt.

Am 4. Januar 2017, um 22:01 Uhr, schrieb John Ericson [email protected] :

@alexreg https://github.com/alexreg Siehe meinen frühen Punkt zur Verwendung von Standardbibliothekssammlungen mit benutzerdefinierten objektspezifischen Zuordnern.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270499628 an oder schalten Sie den Thread https://github.com/notifications/unsubscribe-auth/ stumm.

Sicher, aber ich bin mir nicht sicher, ob das in die Standardbibliothek gehören würde. Könnte leicht in eine andere Kiste gehen, ohne Verlust der Funktionalität oder Benutzerfreundlichkeit.

Ja, aber Sie möchten wahrscheinlich, dass sich einige Standardbibliotheksfunktionen darauf verlassen (z. B. was @ Ericson2314 vorgeschlagen hat).

Ich denke, Sie möchten Standardbibliothekssammlungen (jeden Heap-zugewiesenen Wert) mit einem beliebigen benutzerdefinierten Allokator verwenden. dh nicht auf objektspezifische beschränkt.

Idealerweise möchten Sie beides - um beide Arten von Allokatoren zu akzeptieren. Die Verwendung des objektspezifischen Caching bietet erhebliche Vorteile. Beispielsweise bieten sowohl die Zuweisung von Platten als auch das Zwischenspeichern von Magazinen erhebliche Leistungsvorteile. Schauen Sie sich die Artikel an, auf die ich oben verlinkt habe, wenn Sie neugierig sind.

Das Objektzuweisungsmerkmal könnte jedoch einfach ein Untermerkmal des allgemeinen Zuweisungsmerkmals sein. So einfach ist das für mich. Sicher, bestimmte Arten von Allokatoren können effizienter sein als Allzweck-Allokatoren, aber weder der Compiler noch der Standard müssen (oder sollten) wirklich darüber Bescheid wissen.

Am 4. Januar 2017, um 22:13 Uhr, schrieb Joshua Liebow-Feeser [email protected] :

Sicher, aber ich bin mir nicht sicher, ob das in die Standardbibliothek gehören würde. Könnte leicht in eine andere Kiste gehen, ohne Verlust der Funktionalität oder Benutzerfreundlichkeit.

Ja, aber Sie möchten wahrscheinlich, dass sich einige Standardbibliotheksfunktionen darauf verlassen (z. B. was @ Ericson2314 https://github.com/Ericson2314 vorgeschlagen hat).

Ich denke, Sie möchten Standardbibliothekssammlungen (jeden Heap-zugewiesenen Wert) mit einem beliebigen benutzerdefinierten Allokator verwenden. dh nicht auf objektspezifische beschränkt.

Idealerweise möchten Sie beides - um beide Arten von Allokatoren zu akzeptieren. Die Verwendung des objektspezifischen Caching bietet erhebliche Vorteile. Beispielsweise bieten sowohl die Zuweisung von Platten als auch das Zwischenspeichern von Magazinen erhebliche Leistungsvorteile. Schauen Sie sich die Artikel an, auf die ich oben verlinkt habe, wenn Sie neugierig sind.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270502231 an oder schalten Sie den Thread https://github.com/notifications/unsubscribe-auth/ stumm.

Das Objektzuweisungsmerkmal könnte jedoch einfach ein Untermerkmal des allgemeinen Zuweisungsmerkmals sein. So einfach ist das für mich. Sicher, bestimmte Arten von Allokatoren können effizienter sein als Allzweck-Allokatoren, aber weder der Compiler noch der Standard müssen (oder sollten) wirklich darüber Bescheid wissen.

Ah, das Problem ist also, dass die Semantik unterschiedlich ist. Allocator ordnet rohe Byte-Blobs zu und gibt sie frei. ObjectAllocator<T> würde andererseits bereits erstellte Objekte zuweisen und wäre auch dafür verantwortlich, diese Objekte zu löschen (einschließlich der Möglichkeit, konstruierte Objekte zwischenzuspeichern, die später beim Erstellen eines neu zugewiesenen Objekts ausgegeben werden könnten , was teuer ist). Das Merkmal würde ungefähr so ​​aussehen:

trait ObjectAllocator<T> {
    fn alloc() -> T;
    fn free(t T);
}

Dies ist nicht kompatibel mit Allocator , dessen Methoden sich mit rohen Zeigern befassen und keine Vorstellung vom Typ haben. Bei Allocator s liegt es außerdem in der Verantwortung des Anrufers, drop das Objekt freizugeben, das zuerst freigegeben wird. Dies ist wirklich wichtig - wenn Sie den Typ T können ObjectAllocator<T> Dinge wie die Methode T drop aufrufen, und seit free(t) verschiebt t in free , der Anrufer kann t zuerst fallen lassen - es liegt stattdessen in der Verantwortung von ObjectAllocator<T> . Grundsätzlich sind diese beiden Merkmale nicht miteinander kompatibel.

Ah richtig, ich verstehe. Ich dachte, dieser Vorschlag enthielt bereits so etwas, dh einen Allokator auf „höherer Ebene“ über der Byte-Ebene. In diesem Fall ein vollkommen fairer Vorschlag!

Am 4. Januar 2017, um 22:29 Uhr, schrieb Joshua Liebow-Feeser [email protected] :

Das Objektzuweisungsmerkmal könnte jedoch einfach ein Untermerkmal des allgemeinen Zuweisungsmerkmals sein. So einfach ist das für mich. Sicher, bestimmte Arten von Allokatoren können effizienter sein als Allzweck-Allokatoren, aber weder der Compiler noch der Standard müssen (oder sollten) wirklich darüber Bescheid wissen.

Ah, das Problem ist also, dass die Semantik unterschiedlich ist. Allocator ordnet rohe Byte-Blobs zu und gibt sie frei. ObjectAllocatorAuf der anderen Seite würden bereits erstellte Objekte zugewiesen und wären auch dafür verantwortlich, diese Objekte zu löschen (einschließlich der Möglichkeit, konstruierte Objekte zwischenzuspeichern, die später ausgegeben werden könnten, um ein neu zugewiesenes Objekt zu erstellen, was teuer ist). Das Merkmal würde ungefähr so ​​aussehen:

Merkmal ObjectAllocator{
fn alloc () -> T;
fn frei (t T);
}}
Dies ist nicht kompatibel mit Allocator, dessen Methoden sich mit Rohzeigern befassen und keinen Typbegriff haben. Bei Allocators liegt es außerdem in der Verantwortung des Anrufers, das zuerst freigegebene Objekt zu löschen. Dies ist sehr wichtig - wenn Sie den Typ T kennen, können Sie ObjectAllocator verwendenUm Dinge wie die Drop-Methode von T aufzurufen, und da free (t) t in free verschiebt, kann der Aufrufer t nicht zuerst löschen - es ist stattdessen der ObjectAllocatorVerantwortung. Grundsätzlich sind diese beiden Merkmale nicht miteinander kompatibel.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270505704 an oder schalten Sie den Thread https://github.com/notifications/unsubscribe-auth/ stumm.

@alexreg Ah ja, das habe ich auch gehofft :) Na ja - es muss auf einen weiteren RFC warten.

Ja, starten Sie diesen RFC, ich bin sicher, er würde viel Unterstützung bekommen! Und danke für die Klarstellung (ich hatte mit den Details dieses RFC überhaupt nicht Schritt gehalten).

Am 5. Januar 2017 um 00:53 schrieb Joshua Liebow-Feeser [email protected] :

@alexreg https://github.com/alexreg Ah ja, das habe ich auch gehofft :) Na ja - es muss auf einen weiteren RFC warten.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail, zeigen Sie sie auf GitHub https://github.com/rust-lang/rust/issues/32838#issuecomment-270531535 an oder schalten Sie den Thread https://github.com/notifications/unsubscribe-auth/ stumm.

Eine Kiste zum Testen von benutzerdefinierten Allokatoren wäre nützlich.

Verzeihen Sie mir, wenn mir etwas Offensichtliches fehlt, aber gibt es einen Grund für das in diesem RFC beschriebene Merkmal Layout , Copy sowie Clone nicht zu implementieren, da es nur so ist POD?

Mir fällt nichts ein.

Es tut mir leid, dass ich das so spät angesprochen habe, aber ...

Könnte es sich lohnen, Unterstützung für eine dealloc -ähnliche Funktion hinzuzufügen, die keine Methode, sondern eine Funktion ist? Die Idee wäre, die Ausrichtung zu verwenden, um aus einem Zeiger schließen zu können, wo sich im Speicher sein übergeordneter Allokator befindet, und somit frei zu sein, ohne eine explizite Allokatorreferenz zu benötigen.

Dies könnte ein großer Gewinn für Datenstrukturen sein, die benutzerdefinierte Allokatoren verwenden. Dies würde es ihnen ermöglichen, keinen Verweis auf den Allokator selbst zu behalten, sondern nur für den Typ des Allokators parametrisch zu sein, um die richtige dealloc -Funktion aufrufen zu können. Wenn beispielsweise Box irgendwann geändert wird, um benutzerdefinierte Zuordnungen zu unterstützen, kann es weiterhin nur ein einzelnes Wort (nur der Zeiger) sein, anstatt auf zwei Wörter erweitert werden zu müssen, um eine Referenz zu speichern auch an den Allokator.

In einem ähnlichen Zusammenhang kann es auch nützlich sein, eine nicht methodische alloc -Funktion zu unterstützen, um globale Allokatoren zu ermöglichen. Dies würde sich gut mit einer nicht methodischen dealloc -Funktion zusammensetzen lassen - für globale Allokatoren wäre es nicht erforderlich, irgendeine Art von Zeiger-zu-Allokator-Inferenz durchzuführen, da es nur eine einzige statische Instanz von geben würde Allokator für das gesamte Programm.

@joshlf Mit dem aktuellen Design können Sie dies erreichen, indem Sie Ihren Allokator nur als Einheitentyp (Größe Null) struct MyAlloc; , auf dem Sie dann das Merkmal Allocator implementieren.
Das Speichern von Referenzen oder gar nichts ist immer weniger allgemein als das Speichern des Allokator-Nachwerts.

Ich konnte sehen, dass dies für einen direkt eingebetteten Typ zutrifft, aber was ist, wenn eine Datenstruktur beschließt, stattdessen eine Referenz beizubehalten? Nimmt ein Verweis auf einen Typ mit der Größe Null keinen Platz ein? Das heißt, wenn ich habe:

struct Foo()

struct Blah{
    foo: &Foo,
}

Hat Blah eine Größe von Null?

Selbst wenn es möglich ist, möchten Sie möglicherweise nicht, dass Ihr Allokator die Größe Null hat. Beispielsweise haben Sie möglicherweise einen Allokator mit einer Größe ungleich Null, den Sie _from_ zuweisen, der jedoch Objekte freigeben kann, ohne über den ursprünglichen Allokator Bescheid zu wissen. Dies wäre immer noch nützlich, um ein Box nur ein Wort nehmen zu lassen. Sie hätten so etwas wie Box::new_from_allocator , für das ein Allokator als Argument herangezogen werden müsste - und es könnte sich um einen Allokator ungleich Null handeln -, aber wenn der Allokator das Freigeben ohne die ursprüngliche Allokatorreferenz unterstützt, wird Box<T> könnte vermeiden, einen Verweis auf den Allokator zu speichern, der im ursprünglichen Aufruf von Box::new_from_allocator .

Beispielsweise haben Sie möglicherweise einen Zuweiser mit einer Größe ungleich Null, aus dem Sie zuweisen, der jedoch Objekte freigeben kann, ohne über den ursprünglichen Zuweiser Bescheid zu wissen.

Ich erinnere mich, dass ich vor langer, langer Zeit aus diesem Grund vorgeschlagen habe, getrennte Allokator- und Deallocator-Merkmale (wobei assoziierte Typen die beiden verbinden) herauszurechnen.

Darf / soll der Compiler die Zuordnungen mit diesen Zuordnungen optimieren?

Darf / soll der Compiler die Zuordnungen mit diesen Zuordnungen optimieren?

@ Zoxc Was meinst du?

Ich erinnere mich, dass ich vor langer, langer Zeit aus diesem Grund vorgeschlagen habe, getrennte Allokator- und Deallocator-Merkmale (wobei assoziierte Typen die beiden verbinden) herauszurechnen.

Lassen Sie mich für die Nachwelt diese Aussage klarstellen (ich habe offline mit @ Ericson2314 darüber gesprochen): Die Idee ist, dass ein Box nur auf einem Deallocator parametrisch sein kann. Sie könnten also die folgende Implementierung haben:

trait Allocator {
    type D: Deallocator;

    fn get_deallocator(&self) -> Self::D;
}

trait Deallocator {}

struct Box<T, D: Deallocator> {
    ptr: *mut T,
    d: D,
}

impl<T, D: Deallocator> Box<T, D> {
    fn new_from_allocator<A: Allocator>(x: T, a: A) -> Box<T, A::D> {
        ...
        Box {
            ptr: ptr,
            d: a.get_deallocator()
        }
    }
}

Auf diese Weise nimmt beim Aufrufen von new_from_allocator , wenn A::D ein Typ mit der Größe Null ist, das Feld d von Box<T, A::D> die Größe Null an Größe des resultierenden Box<T, A::D> ist ein einzelnes Wort.

Gibt es einen Zeitplan für die Landung? Ich arbeite an einigen Allokator-Sachen, und es wäre schön, wenn diese Sachen für mich da wären, um daraus aufzubauen.

Wenn es Interesse gibt, würde ich gerne ein paar Zyklen verleihen, aber ich bin relativ neu bei Rust, so dass die Betreuer möglicherweise mehr Arbeit in Bezug auf die Codeüberprüfung des Codes eines Neulings haben. Ich möchte niemandem auf die Zehen treten und ich möchte nicht mehr Arbeit für die Menschen machen.

Ok, wir haben uns kürzlich getroffen, um den Status der Allokatoren zu bewerten, und ich denke, es gibt auch einige gute Neuigkeiten dafür! Es sieht so aus, als ob der Support für diese APIs noch nicht in libstd gelandet ist, aber alle sind immer noch zufrieden damit, dass sie jederzeit landen!

Eine Sache, die wir besprochen haben, ist, dass das Umschalten aller libstd-Typen aufgrund möglicher Inferenzprobleme etwas verfrüht sein kann, aber unabhängig davon scheint es eine gute Idee zu sein, das Merkmal Allocator und das Merkmal Layout zu landen std::heap um an anderer Stelle im Ökosystem zu experimentieren!

@joshlf wenn du hier helfen

@alexcrichton Ich denke dein Link ist kaputt? Es zeigt hierher zurück.

Eine Sache, die wir besprochen haben, ist, dass das Umschalten aller libstd-Typen aufgrund möglicher Inferenzprobleme etwas verfrüht sein kann

Das Hinzufügen des Merkmals ist ein guter erster Schritt, aber ohne die Umgestaltung vorhandener APIs, um es zu verwenden, werden sie nicht viel genutzt. In https://github.com/rust-lang/rust/issues/27336#issuecomment -300721558 schlage ich vor, dass wir die Kisten hinter der Fassade sofort umgestalten können, aber Newtype-Wrapper in std hinzufügen. Ärgerlich zu tun, aber erlaubt uns, Fortschritte zu machen.

@alexcrichton Was wäre der Prozess, um Objektzuordnungen zu erhalten? Meine bisherigen Experimente (bald öffentlich; ich kann Sie zum privaten GH-Repo hinzufügen, wenn Sie neugierig sind) und die Diskussion hier haben mich zu der Annahme geführt, dass es eine nahezu perfekte Symmetrie zwischen den Allokatormerkmalen und dem Objekt geben wird Allokatormerkmale. ZB haben Sie so etwas wie (Ich habe Address in *mut u8 geändert, um Symmetrie mit *mut T von ObjectAllocator<T> erzielen. Wir würden wahrscheinlich mit Address<T> enden.

unsafe trait Allocator {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);
}
unsafe trait ObjectAllocator<T> {
    unsafe fn alloc(&mut self) -> Result<*mut T, AllocErr>;
    unsafe fn dealloc(&mut self, ptr: *mut T);
}

Daher denke ich, dass es nützlich sein könnte, gleichzeitig mit Allokatoren und Objektallokatoren zu experimentieren. Ich bin mir jedoch nicht sicher, ob dies der richtige Ort dafür ist oder ob es einen anderen RFC oder zumindest einen separaten PR geben sollte.

Oh, ich wollte hier verlinken @ Joshlf ist das was du denkst?

Es hört sich so an, als ob @alexcrichton eine PR wünscht, die das Merkmal Allocator und den Typ Layout bietet, auch wenn es in keiner Sammlung in libstd .

Wenn ich das richtig verstehe, kann ich dafür eine PR erstellen. Ich hatte es nicht getan, weil ich immer wieder versuche, zumindest eine Integration mit RawVec und Vec Prototypen zu erreichen. (Zu diesem Zeitpunkt habe ich RawVec erledigt, aber Vec ist aufgrund der vielen anderen Strukturen, die darauf aufbauen, wie Drain und IntoIter etwas herausfordernder

Tatsächlich scheint mein aktueller Zweig tatsächlich aufgebaut zu sein (und der eine Test für die Integration mit RawVec bestanden), also habe ich ihn veröffentlicht: # 42313

@hawkw fragte:

Verzeihen Sie mir, wenn mir etwas Offensichtliches fehlt, aber gibt es einen Grund für das in diesem RFC beschriebene Layout-Merkmal, Copy und Clone nicht zu implementieren, da es sich nur um POD handelt?

Der Grund, warum ich Layout nur Clone und nicht Copy implementieren, ist, dass ich die Möglichkeit offen lassen wollte, dem Typ Layout mehr Struktur hinzuzufügen. Insbesondere bin ich immer noch daran interessiert, den Versuch Layout versuchen, eine Typstruktur zu verfolgen, die zum Erstellen verwendet wurde (z. B. 16-Array von struct { x: u8, y: [char; 215] } ), damit Allokatoren die Option haben Anzeigen von Instrumentierungsroutinen, die angeben, aus welchen Typen ihr aktueller Inhalt besteht.

Dies müsste mit ziemlicher Sicherheit eine optionale Funktion sein, dh es scheint, als ob die Flut entschieden dagegen ist, Entwickler zur Verwendung der typangereicherten Layout -Konstruktoren zu zwingen. Daher müsste jede Instrumentierung dieser Form so etwas wie eine Kategorie "unbekannte Speicherblöcke" enthalten, um Zuordnungen zu verarbeiten, die nicht über die Typinformationen verfügen.

Trotzdem waren solche Funktionen der Hauptgrund, warum ich mich nicht dafür entschieden habe, Layout Copy implementieren zu lassen. Ich dachte im Grunde, dass eine Implementierung von Copy eine vorzeitige Einschränkung für Layout selbst sein würde.

@alexcrichton @pnkfelix

Es sieht so aus, als ob @pnkfelix dieses Fahrt gewinnt. Lassen Sie uns einfach damit weitermachen. Ich schaue darüber nach und mache jetzt Kommentare, und es sieht toll aus!

Derzeit lautet die Signatur für Allocator::oom :

    fn oom(&mut self, _: AllocErr) -> ! {
        unsafe { ::core::intrinsics::abort() }
    }

Ich wurde jedoch darauf aufmerksam gemacht , dass Gecko zumindest die Zuordnungsgröße auch bei OOM gerne kennt. Wir möchten dies möglicherweise bei der Stabilisierung berücksichtigen, um möglicherweise einen Kontext wie Option<Layout> hinzuzufügen, warum OOM stattfindet.

@alexcrichton
Könnte es sich lohnen, entweder mehrere oom_xxx -Varianten oder eine Aufzählung verschiedener Argumenttypen zu haben? Es gibt verschiedene Signaturen für Methoden, die fehlschlagen könnten (z. B. alloc nimmt ein Layout an, realloc nimmt einen Zeiger, ein ursprüngliches Layout und ein neues Layout usw.), und dies könnte der Fall sein Dies sind Fälle, in denen eine oom -ähnliche Methode über alle Bescheid wissen möchte.

@ Joshlf das stimmt, ja, aber ich bin mir nicht sicher, ob es nützlich ist. Ich möchte nicht nur Funktionen hinzufügen, weil wir können, sie sollten weiterhin gut motiviert sein.


Ein Punkt für die Stabilisierung ist hier auch "Bestimmen, welche Anforderungen an die Ausrichtung von fn dealloc ", und die aktuelle Implementierung von dealloc unter Windows verwendet align , um zu bestimmen, wie richtig frei. @ruuda Sie könnten an dieser Tatsache interessiert sein.

Ein Punkt für die Stabilisierung ist hier auch "Bestimmen, welche Anforderungen an die Ausrichtung von fn dealloc ", und die aktuelle Implementierung von dealloc unter Windows verwendet align , um zu bestimmen, wie richtig frei.

Ja, ich denke, so bin ich anfangs darauf gestoßen; Mein Programm stürzte deshalb unter Windows ab. Da HeapAlloc keine Ausrichtungsgarantien gibt, weist allocate einen größeren Bereich zu und speichert den ursprünglichen Zeiger in einem Header. Als Optimierung wird dies jedoch vermieden, wenn die Ausrichtungsanforderungen trotzdem erfüllt würden. Ich frage mich, ob es eine Möglichkeit gibt, HeapAlloc in einen ausrichtungsbewussten Allokator umzuwandeln, für den keine kostenlose Ausrichtung erforderlich ist, ohne diese Optimierung zu verlieren.

@ruuda

Da HeapAlloc keine Ausrichtungsgarantien gibt

Es bietet eine Mindestausrichtungsgarantie von 8 Byte für 32 Bit oder 16 Byte für 64 Bit. Es bietet jedoch keine Möglichkeit, eine höhere Ausrichtung zu gewährleisten.

Der _aligned_malloc durch die CRT unter Windows bereitgestellt wird, kann Zuweisungen höherer Ausrichtung bieten, aber vor allem muss es mit gepaart werden _aligned_free , mit free illegal ist. Wenn Sie also nicht wissen, ob eine Zuweisung über malloc oder _aligned_malloc ist, stecken Sie in demselben Rätsel wie alloc_system unter Windows, wenn Sie dies nicht tun. Ich kenne die Ausrichtung für deallocate . Die CRT bietet nicht die Standardfunktion aligned_alloc , die mit free gepaart werden kann, sodass selbst Microsoft dieses Problem nicht lösen konnte. (Obwohl es sich um eine C11-Funktion handelt und Microsoft C11 nicht unterstützt, ist dies ein schwaches Argument.)

Beachten Sie, dass deallocate nur um die Ausrichtung kümmert, um zu wissen, ob sie überausgerichtet ist. Der tatsächliche Wert selbst ist irrelevant. Wenn Sie ein deallocate möchten, das wirklich unabhängig von der Ausrichtung ist, können Sie einfach alle Zuordnungen als überausgerichtet behandeln, aber Sie würden viel Speicher für kleine Zuordnungen verschwenden.

@alexcrichton schrieb :

Derzeit lautet die Signatur für Allocator::oom :

    fn oom(&mut self, _: AllocErr) -> ! {
        unsafe { ::core::intrinsics::abort() }
    }

Ich wurde jedoch darauf aufmerksam gemacht , dass Gecko zumindest die Zuordnungsgröße auch bei OOM gerne kennt. Wir möchten dies möglicherweise bei der Stabilisierung berücksichtigen, um möglicherweise einen Kontext wie Option<Layout> hinzuzufügen, warum OOM stattfindet.

Das AllocErr trägt bereits das Layout in der Variante AllocErr::Exhausted . Wir könnten auch einfach die Layout zur AllocErr::Unsupported -Variante hinzufügen, was meiner Meinung nach im Hinblick auf die Kundenerwartungen am einfachsten wäre. (Es hat den Nachteil, die Seite der AllocErr -Aufzählung selbst leise zu vergrößern, aber vielleicht sollten wir uns darüber keine Sorgen machen ...)

Oh, ich vermute, das ist alles was benötigt wird, danke für die Korrektur @pnkfelix!

Ich werde damit beginnen, dieses Problem für das Tracking-Problem für std::heap im Allgemeinen neu zu verwenden, wie es nach den Landungen von https://github.com/rust-lang/rust/pull/42727 der Fall sein wird. Ich werde ein paar andere verwandte Themen dafür schließen.

Gibt es ein Tracking-Problem beim Konvertieren von Sammlungen? Jetzt, wo die PRs zusammengeführt sind, würde ich gerne

  • Besprechen Sie den zugehörigen Fehlertyp
  • Erläutern Sie die Konvertierung von Sammlungen zur Verwendung eines beliebigen lokalen Allokators (insbesondere die Nutzung des zugehörigen Fehlertyps).

Ich habe https://github.com/rust-lang/rust/issues/42774 geöffnet, um die Integration von Alloc in Standard-Sammlungen zu verfolgen. Mit historischen Diskussionen im libs-Team befindet sich dies wahrscheinlich auf einem anderen Weg der Stabilisierung als ein erster Durchgang des Moduls std::heap .

Bei der Überprüfung von Allokator-bezogenen Problemen bin ich auch auf https://github.com/rust-lang/rust/issues/30170 gestoßen, das vor @pnkfelix war . Es sieht so aus, als ob der OSX-Systemzuweiser mit hohen Ausrichtungen fehlerhaft ist, und wenn dieses Programm mit jemalloc ausgeführt wird, tritt zumindest während der Freigabe unter Linux ein Segfault auf. Bei der Stabilisierung erwägenswert!

Ich habe # 42794 als Ort geöffnet, um die spezifische Frage zu diskutieren, ob Zuordnungen mit der Größe Null mit der angeforderten Ausrichtung übereinstimmen müssen.

(Oh, warte, Zuordnungen mit der Größe Null sind in Benutzerzuordnungen illegal !)

Da die alloc::heap::allocate -Funktion und Freunde jetzt in Nightly verschwunden sind, habe ich Servo aktualisiert, um diese neue API zu verwenden. Dies ist Teil des Diff:

-use alloc::heap;
+use alloc::allocator::{Alloc, Layout};
+use alloc::heap::Heap;
-        let ptr = heap::allocate(req_size as usize, FT_ALIGNMENT) as *mut c_void;
+        let layout = Layout::from_size_align(req_size as usize, FT_ALIGNMENT).unwrap();
+        let ptr = Heap.alloc(layout).unwrap() as *mut c_void;

Ich finde die Ergonomie nicht besonders gut. Wir sind vom Importieren eines Elements zum Importieren von drei Elementen aus zwei verschiedenen Modulen übergegangen.

  • Wäre es sinnvoll, eine bequeme Methode für allocator.alloc(Layout::from_size_align(…)) ?
  • Wäre es sinnvoll, <Heap as Alloc>::_ Methoden als freie Funktionen oder inhärente Methoden zur Verfügung zu stellen? (Um ein Element weniger zu importieren, muss das Merkmal Alloc .)

Könnte das Merkmal Alloc alternativ im Auftakt stehen oder ist es zu nisch für einen Anwendungsfall?

@SimonSapin IMO Es macht wenig Sinn, die Ergonomie einer solchen Low-Level-API zu optimieren.

@ SimonSapin

Ich finde die Ergonomie nicht besonders gut. Wir sind vom Importieren eines Elements zum Importieren von drei Elementen aus zwei verschiedenen Modulen übergegangen.

Ich hatte genau das gleiche Gefühl mit meiner Codebasis - sie ist jetzt ziemlich klobig.

Wäre es sinnvoll, eine bequeme Methode für allocator.alloc(Layout::from_size_align(…))? zu haben?

Meinen Sie im Merkmal Alloc oder nur für Heap ? Eine Sache, die hier berücksichtigt werden muss, ist, dass es jetzt eine dritte Fehlerbedingung gibt: Layout::from_size_align gibt Option , sodass zusätzlich zu den normalen Fehlern, die beim Zuweisen auftreten können, None werden kann .

Könnte das Merkmal Alloc alternativ im Auftakt stehen oder ist es zu nisch für einen Anwendungsfall?

IMO macht es wenig Sinn, die Ergonomie einer solchen Low-Level-API zu optimieren.

Ich stimme zu, dass es wahrscheinlich zu niedrig ist, um das Vorspiel einzuleiten, aber ich denke immer noch, dass es sinnvoll ist, die Ergonomie zu optimieren (zumindest egoistisch - das war ein wirklich nerviger Refaktor 😝).

@ SimonSapin haben Sie OOM vorher nicht behandelt? Auch in std alle drei Typen im Modul std::heap verfügbar (sie sollen sich in einem Modul befinden). Haben Sie sich auch vorher nicht mit dem Fall überlaufender Größen befasst? Oder Typen mit der Größe Null?

Haben Sie OOM noch nicht behandelt?

Wenn es existierte, gab die Funktion alloc::heap::allocate einen Zeiger ohne Result und ließ keine Auswahl bei der OOM-Behandlung. Ich denke, es hat den Prozess abgebrochen. Jetzt habe ich .unwrap() hinzugefügt, um den Thread in Panik zu versetzen.

Sie sollen in einem Modul sein

Ich sehe jetzt, dass heap.rs pub use allocator::*; . Aber als ich in dem auf der rustdoc-Seite aufgeführten Gerät für Heap auf Alloc geklickt habe, wurde ich an alloc::allocator::Alloc .

Im Übrigen habe ich mich nicht darum gekümmert. Ich portiere einen großen Stapel Code, der vor Jahren geschrieben wurde, auf einen neuen Compiler. Ich denke, das sind Rückrufe für FreeType, eine C-Bibliothek.

Wenn es existierte, gab die Funktion alloc :: heap :: allocate einen Zeiger ohne Ergebnis zurück und ließ keine Auswahl bei der OOM-Behandlung.

Es gab dir eine Wahl. Der zurückgegebene Zeiger könnte ein Nullzeiger gewesen sein, der angibt, dass der Heap-Allokator nicht zugeordnet werden konnte. Deshalb bin ich so froh, dass es auf Result umgestellt wurde, damit die Leute nicht vergessen, diesen Fall zu behandeln.

Na ja, vielleicht hat der FreeType eine Nullprüfung durchgeführt, ich weiß es nicht. Wie auch immer, ja, ein Ergebnis zurückzugeben ist gut.

Angesichts von # 30170 und # 43097 bin ich versucht, das OS X-Problem mit lächerlich großen Alignments zu lösen, indem ich einfach spezifiziere, dass Benutzer keine Alignments anfordern können> = 1 << 32 .

Eine sehr einfache Möglichkeit, dies durchzusetzen: Ändern Sie die Schnittstelle Layout so, dass align mit u32 anstelle von usize .

@alexcrichton hast du

@pnkfelix Layout::from_size_align würde immer noch usize und einen Fehler beim u32 Überlauf zurückgeben, oder?

@SimonSapin Welchen Grund gibt es dafür, dass usize , wenn eine statische Voraussetzung ist, dass es unsicher ist, einen Wert> = 1 << 32 ?

und wenn die Antwort lautet "Nun, einige Allokatoren unterstützen möglicherweise eine Ausrichtung> = 1 << 32 ", dann kehren wir zum Status Quo zurück und Sie können meinen Vorschlag ignorieren. Der Punkt meines Vorschlags ist im Grunde ein "+1" für Kommentare wie diesen

Weil std::mem::align_of usize zurückgibt

@ SimonSapin ah, gute alte stabile APIs ... seufz.

@pnkfelix Beschränkung auf 1 << 32 erscheint mir vernünftig!

@rfcbot fcp zusammenführen

Ok, dieses Merkmal und seine Typen haben sich schon eine Weile gebacken und waren seit seiner Einführung auch die zugrunde liegende Implementierung der Standardsammlungen. Ich würde vorschlagen, mit einem besonders konservativen Erstangebot zu beginnen, nämlich nur die folgende Schnittstelle zu stabilisieren:

pub struct Layout { /* ... */ }

extern {
    pub type void;
}

impl Layout {
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
}

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> *mut void;
    unsafe fn alloc_zeroed(&mut self, layout: Layout) -> *mut void;
    unsafe fn dealloc(&mut self, ptr: *mut void, layout: Layout);

    // all other methods are default and unstable
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}

Ursprünglicher Vorschlag

pub struct Layout { /* ... */ }

impl Layout {
    pub fn from_size_align(size: usize, align: usize) -> Option<Layout>;
    pub unsafe fn from_size_align_unchecked(size: usize, align: usize) -> Layout;

    pub fn size(&self) -> usize;
    pub fn align(&self) -> usize;
}

// renamed from AllocErr today
pub struct Error {
    // ...
}

impl Error {
    pub fn oom() -> Self;
}

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, Error>;
    unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);

    // all other methods are default and unstable
}

/// The global default allocator
pub struct Heap;

impl Alloc for Heap {
    // ...
}

impl<'a> Alloc for &'a Heap {
    // ...
}

/// The "system" allocator
pub struct System;

impl Alloc for System {
    // ...
}

impl<'a> Alloc for &'a System {
    // ...
}

Vor allem:

  • Derzeit werden nur die Methoden alloc , alloc_zeroed und dealloc für das Merkmal Alloc stabilisiert. Ich denke, dies löst das dringendste Problem, das wir heute haben, indem wir einen benutzerdefinierten globalen Allokator definieren.
  • Entfernen Sie den Typ Error um nur rohe Zeiger zu verwenden.
  • Ändern Sie den Typ u8 in der Benutzeroberfläche in void
  • Eine abgespeckte Version vom Typ Layout .

Es gibt noch offene Fragen, wie zum Beispiel, was mit dealloc und Ausrichtung zu tun ist (genaue Ausrichtung? Passt? Unsicher?), Aber ich hoffe, wir können sie während des FCP lösen, da es wahrscheinlich keine API ist. Veränderung brechen.

+1 um etwas zu stabilisieren!

Benennt AllocErr in Error und verschiebt die Benutzeroberfläche etwas konservativer.

Beseitigt dies die Option für Allokatoren, Unsupported anzugeben? Ich bin der Meinung, dass # 44557 immer noch ein Problem ist, da ich das Risiko habe, mehr an etwas zu spielen, an dem ich viel gearbeitet habe.

Layout

Es sieht so aus, als hätten Sie einige der Methoden aus Layout . Wollten Sie diejenigen, die Sie ausgelassen haben, tatsächlich entfernen lassen oder einfach als instabil belassen?

impl Error {
    pub fn oom() -> Self;
}

Ist dies ein Konstruktor für das, was heute AllocErr::Exhausted ? Wenn ja, sollte es keinen Layout -Parameter haben?

Teammitglied @alexcrichton hat vorgeschlagen, dies zusammenzuführen. Der nächste Schritt ist die Überprüfung durch die übrigen markierten Teams:

  • [x] @BurntSushi
  • [x] @Kimundi
  • [x] @alexcrichton
  • [x] @aturon
  • [x] @cramertj
  • [x] @dtolnay
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @sfackler
  • [x] @ohne Boote

Sorgen:

Sobald diese Prüfer einen Konsens erreicht haben, tritt dies in die endgültige Kommentierungsphase ein. Wenn Sie ein großes Problem entdecken, das zu keinem Zeitpunkt in diesem Prozess angesprochen wurde, melden Sie sich bitte!

In diesem Dokument finden Sie Informationen darüber, welche Befehle Teammitglieder mit Tags geben können.

Ich freue mich sehr darauf, einen Teil dieser Arbeit zu stabilisieren!

Eine Frage: Im obigen Thread haben @joshlf und @ Ericson2314 einen interessanten Punkt über die Möglichkeit der Trennung der Merkmale Alloc und Dealloc angesprochen, um für Fälle zu optimieren, in denen alloc erfordert einige Daten, aber dealloc erfordert keine zusätzlichen Informationen, sodass der Typ Dealloc Größe Null haben kann.

Wurde diese Frage jemals gelöst? Was sind die Nachteile der Trennung der beiden Merkmale?

@ Joshlf

Beseitigt dies die Option für Zuweiser, Nicht unterstützt anzugeben?

Ja und nein, es würde bedeuten, dass ein solcher Vorgang nicht sofort in stabilem Rost unterstützt wird, aber wir könnten ihn weiterhin in instabilem Rost unterstützen.

Wollten Sie diejenigen, die Sie ausgelassen haben, tatsächlich entfernen lassen oder einfach als instabil belassen?

Tatsächlich! Auch wenn ich nur eine stabile API-Oberfläche vorschlage, können wir alle anderen Methoden als instabil belassen. Im Laufe der Zeit können wir die Funktionalität weiter stabilisieren. Ich denke, es ist am besten, so konservativ wie möglich zu beginnen.


@ SimonSapin

Ist dies ein Konstruktor für das, was heute AllocErr :: Exhausted ist? Wenn ja, sollte es keinen Layout-Parameter haben?

Aha guter Punkt! Ich wollte zwar die Möglichkeit auslassen, Error einem Typ mit der Größe Null zu machen, wenn wir ihn wirklich brauchten, aber wir können natürlich die Methoden zur Layouterstellung instabil halten und sie bei Bedarf stabilisieren. Oder denken Sie, dass das layouterhaltende Error im ersten Durchgang stabilisiert werden sollte?


@cramertj

Ich hatte eine solche Frage / Sorge noch nicht persönlich gesehen (ich glaube, ich habe sie verpasst!), Aber ich würde sie persönlich nicht als wertvoll ansehen. Zwei Merkmale sind im Allgemeinen doppelt so groß wie das Boilerplate, da jetzt jeder beispielsweise Alloc + Dealloc in Sammlungen eingeben müsste. Ich würde erwarten, dass eine solche spezialisierte Verwendung nicht die Benutzeroberfläche informieren möchte, die alle anderen Benutzer letztendlich persönlich verwenden.

@cramertj @alexcrichton

Ich hatte eine solche Frage / Sorge noch nicht persönlich gesehen (ich glaube, ich habe sie verpasst!), Aber ich würde sie persönlich nicht als wertvoll ansehen.

Im Allgemeinen stimme ich zu, dass es sich mit einer Ausnahme nicht lohnt: Box . Box<T, A: Alloc> müsste angesichts der aktuellen Definition von Alloc mindestens zwei Wörter groß sein (der Zeiger, den es bereits hat, und mindestens ein Verweis auf ein Alloc ) außer im Fall von globalen Singletons (die als ZSTs implementiert werden können). Eine 2x (oder mehr) Explosion in dem Raum, der zum Speichern eines solchen gemeinsamen und grundlegenden Typs erforderlich ist, betrifft mich.

@alexcrichton

wie jetzt müsste jeder zum Beispiel Alloc + Dealloc in Sammlungen eingeben

Wir könnten so etwas hinzufügen:

trait Allocator: Alloc + Dealloc {}
impl<T> Allocator for T where T: Alloc + Dealloc {}

Eine 2x (oder mehr) Explosion in dem Raum, der erforderlich ist, um einen solchen gemeinsamen und grundlegenden Typ zu speichern

Nur wenn Sie einen benutzerdefinierten Allokator verwenden, der nicht prozessglobal ist. std::heap::Heap (Standardeinstellung) hat die Größe Null.

Oder denken Sie, dass der Layouterhaltungsfehler im ersten Durchgang stabilisiert werden sollte?

@alexcrichton Ich verstehe nicht wirklich, warum dieser vorgeschlagene erste Durchgang überhaupt ist. Es gibt kaum mehr, als durch den Missbrauch von Vec bereits erreicht werden könnte, und nicht genug, um beispielsweise https://crates.io/crates/jemallocator zu verwenden

Was muss noch gelöst werden, um das Ganze zu stabilisieren?

Nur wenn Sie einen benutzerdefinierten Allokator verwenden, der nicht prozessglobal ist. std :: heap :: Heap (Standardeinstellung) hat die Größe Null.

Das scheint der primäre Anwendungsfall für parametrische Allokatoren zu sein, nicht wahr? Stellen Sie sich die folgende einfache Definition eines Baumes vor:

struct Node<T, A: Alloc> {
    t: T,
    left: Option<Box<Node<T, A>>>,
    right: Option<Box<Node<T, A>>>,
}

Ein Baum, der aus solchen mit einem 1-Wort- Alloc würde im Vergleich zu einem ZST Alloc eine ~ 1,7-fache Vergrößerung der gesamten Datenstruktur aufweisen. Das scheint mir ziemlich schlecht zu sein, und diese Art von Anwendungen sind sozusagen der springende Punkt, wenn Alloc ein Merkmal ist.

@cramertj

Wir könnten so etwas hinzufügen:

Wir werden auch tatsächliche Trait-Aliase haben :) https://github.com/rust-lang/rust/issues/41517

@glaebhoerl Ja, aber die Stabilisierung scheint noch weit entfernt zu sein, da es noch keine Implementierung gibt. Wenn wir manuelle Impls von Allocator deaktivieren, können wir meiner Meinung nach rückwärtskompatibel zu Trait-Aliasen wechseln, wenn sie eintreffen;)

@ Joshlf

Eine 2x (oder mehr) Explosion in dem Raum, der zum Speichern eines solchen gemeinsamen und grundlegenden Typs erforderlich ist, betrifft mich.

Ich würde mir vorstellen, dass alle heutigen Implementierungen nur ein Typ mit der Größe Null oder ein großer Zeiger sind, oder? Ist die mögliche Optimierung nicht, dass einige Zeigergrößentypen eine Größe von Null haben können? (oder etwas ähnliches?)


@cramertj

Wir könnten so etwas hinzufügen:

Tatsächlich! Wir haben dann aber ein Merkmal auf drei gesetzt . In der Vergangenheit haben wir mit solchen Eigenschaften noch nie großartige Erfahrungen gemacht. Zum Beispiel wird Box<Both> nicht in Box<OnlyOneTrait> . Ich bin sicher, wir könnten warten, bis die Sprachfunktionen dies alles geglättet haben, aber es scheint, dass diese bestenfalls weit entfernt sind.


@ SimonSapin

Was muss noch gelöst werden, um das Ganze zu stabilisieren?

Ich weiß es nicht. Ich wollte mit der absolut kleinsten Sache beginnen, damit es weniger Debatten gibt.

Ich würde mir vorstellen, dass alle heutigen Implementierungen nur ein Typ mit der Größe Null oder ein großer Zeiger sind, oder? Ist die mögliche Optimierung nicht, dass einige Zeigergrößentypen eine Größe von Null haben können? (oder etwas ähnliches?)

Ja, die Idee ist, dass Sie mit einem Zeiger auf ein Objekt, das von Ihrem Allokatortyp zugewiesen wurde, herausfinden können, von welcher Instanz es stammt (z. B. mithilfe von Inline-Metadaten). Daher müssen Sie nur die Typinformationen freigeben, nicht die Laufzeitinformationen.

Um bei der Freigabe wieder zur Ausrichtung zurückzukehren, sehe ich zwei Möglichkeiten:

  • Stabilisieren Sie sich wie vorgeschlagen (mit Ausrichtung auf Freigabe). Das Verschenken des Eigentums an manuell zugewiesenem Speicher wäre nur möglich, wenn Layout enthalten ist. Insbesondere ist es unmöglich, einen Vec oder Box oder String oder einen anderen std Container mit einer strengeren Ausrichtung als erforderlich zu erstellen (zum Beispiel, weil Sie nicht anziehen Ich möchte nicht, dass das Box-Element eine Cache-Zeile überspannt, ohne es später manuell zu dekonstruieren und die Zuordnung aufzuheben (was nicht immer eine Option ist). Ein anderes Beispiel für etwas, das unmöglich wäre, ist das Füllen eines Vec mit simd-Operationen und das anschließende Verschenken.

  • Erfordern keine Ausrichtung bei der Freigabe und entfernen Sie die Optimierung für kleine Zuordnungen aus Windows ' HeapAlloc -basiertem alloc_system . Speichern Sie immer die Ausrichtung. @alexcrichton , als Sie diesen Code festgeschrieben haben, erinnern Sie sich, warum er überhaupt dort abgelegt wurde? Haben wir Beweise dafür, dass dadurch eine erhebliche Menge an Speicher für reale Anwendungen eingespart wird? (Mit Mikrobenchmarks ist es möglich, dass die Ergebnisse je nach Zuordnungsgröße in beide Richtungen angezeigt werden - es sei denn, HeapAlloc die Größen ohnehin aufgerundet.)

In jedem Fall ist dies ein sehr schwieriger Kompromiss. Die Auswirkungen auf Speicher und Leistung hängen stark von der Art der Anwendung ab. Welche zu optimieren ist, ist auch anwendungsspezifisch.

Ich denke, wir könnten tatsächlich Just Fine (TM) sein. Zitieren der Alloc -Dokumente :

Einige der Methoden erfordern, dass ein Layout zu einem Speicherblock
Was es für ein Layout bedeutet, in einen Speicherblock zu "passen", bedeutet (oder
Entsprechend ist für einen Speicherblock, der zu einem Layout "passt", dass der
Folgende zwei Bedingungen müssen gelten:

  1. Die Startadresse des Blocks muss auf layout.align() .

  2. Die Größe des Blocks muss in den Bereich [use_min, use_max] , wobei:

    • use_min ist self.usable_size(layout).0 und

    • use_max ist die Kapazität, die war (oder gewesen wäre)
      Wird zurückgegeben, wenn (wenn) der Block über einen Aufruf an zugewiesen wurde
      alloc_excess oder realloc_excess .

Beachten Sie, dass:

  • Die Größe des Layouts, das zuletzt zum Zuweisen des Blocks verwendet wurde
    liegt garantiert im Bereich von [use_min, use_max] und

  • Eine Untergrenze für use_max kann durch einen Aufruf von sicher angenähert werden
    usable_size .

  • wenn ein Layout k in einen Speicherblock passt (bezeichnet mit ptr )
    Derzeit über einen Allokator a zugewiesen, dann ist es legal zu
    Verwenden Sie dieses Layout, um die Zuordnung aufzuheben, dh a.dealloc(ptr, k); .

Beachten Sie die letzte Kugel. Wenn ich ein Layout mit der Ausrichtung a zuordne, sollte es für mich legal sein, die Zuordnung mit der Ausrichtung b < a aufzuheben, da ein Objekt, das an a ausgerichtet ist, auch an b ausgerichtet ist b passt zu einem Objekt, das einem Layout mit Ausrichtung a (und derselben Größe) zugeordnet ist.

Dies bedeutet, dass Sie in der Lage sein sollten, eine Ausrichtung zuzuweisen, die größer als die für einen bestimmten Typ erforderliche Mindestausrichtung ist, und dann zulassen, dass ein anderer Code die Zuordnung mit der Mindestausrichtung aufhebt, und dies sollte funktionieren.

Ist die mögliche Optimierung nicht, dass einige Zeigergrößentypen eine Größe von Null haben können? (oder etwas ähnliches?)

Vor kurzem gab es einen RFC dafür, und es ist sehr unwahrscheinlich, dass dies aufgrund von Kompatibilitätsproblemen durchgeführt werden kann: https://github.com/rust-lang/rfcs/pull/2040

Zum Beispiel wird Box<Both> nicht in Box<OnlyOneTrait> . Ich bin sicher, wir könnten warten, bis die Sprachfunktionen dies alles geglättet haben, aber es scheint, dass diese bestenfalls weit entfernt sind.

Das Upcasting von Merkmalsobjekten scheint dagegen unumstritten wünschenswert zu sein, und es geht hauptsächlich um Aufwand / Bandbreite / Willenskraft, um es umzusetzen. Vor kurzem gab es einen Thread: https://internals.rust-lang.org/t/trait-upcasting/5970

@ruuda Ich war derjenige, der diese alloc_system Implementierung ursprünglich geschrieben hat. alexcrichton hat es lediglich während der großen Allokator-Refaktoren von <time period> verschoben.

Die aktuelle Implementierung erfordert , dass Sie die Zuordnung mit derselben Ausrichtung aufheben, mit der Sie einen bestimmten Speicherblock zugewiesen haben. Unabhängig davon, was in der Dokumentation behauptet wird, ist dies die aktuelle Realität, an die sich jeder halten muss, bis alloc_system unter Windows geändert wird.

Zuweisungen unter Windows verwenden immer ein Vielfaches von MEMORY_ALLOCATION_ALIGNMENT (obwohl sie sich an die Größe erinnern, mit der Sie sie dem Byte zugewiesen haben). MEMORY_ALLOCATION_ALIGNMENT ist 8 auf 32bit und 16 auf 64bit. Bei überausgerichteten Typen ist der durch alloc_system verursachte Overhead konsistent der angegebene Ausrichtungsbetrag, sodass eine mit 64 Byte ausgerichtete Zuordnung 64 Byte Overhead hätte, da die Ausrichtung größer als MEMORY_ALLOCATION_ALIGNMENT ist.

Wenn wir uns entschließen würden, diesen überausgerichteten Trick auf alle Zuweisungen auszudehnen (wodurch die Anforderung, die Zuordnung mit derselben Ausrichtung aufzuheben, die Sie bei der Zuweisung angegeben haben, entfällt), hätten mehr Zuweisungen Overhead. Zuweisungen, deren Ausrichtungen mit MEMORY_ALLOCATION_ALIGNMENT identisch sind, verursachen einen konstanten Overhead von MEMORY_ALLOCATION_ALIGNMENT Bytes. Zuweisungen, deren Ausrichtungen weniger als MEMORY_ALLOCATION_ALIGNMENT betragen, verursachen ungefähr die Hälfte der Zeit einen Overhead von MEMORY_ALLOCATION_ALIGNMENT Bytes. Wenn die Größe der Zuordnung, aufgerundet auf MEMORY_ALLOCATION_ALIGNMENT größer oder gleich der Größe der Zuordnung plus der Größe eines Zeigers ist, entsteht kein Overhead, andernfalls. Wenn man bedenkt, dass 99,99% der Zuweisungen nicht überausgerichtet sind, möchten Sie wirklich einen solchen Overhead für all diese Zuweisungen verursachen?

@ruuda

Ich persönlich bin der Meinung, dass die Implementierung von alloc_system heute unter Windows ein größerer Vorteil ist als die Möglichkeit, das Eigentum an einer Zuordnung zu einem anderen Container wie Vec aufzugeben. AFAIK obwohl es keine Daten gibt, um die Auswirkung zu messen, wenn immer mit der Ausrichtung gepolstert wird und keine Ausrichtung auf die Freigabe erforderlich ist.

@ Joshlf

Ich denke, dieser Kommentar ist falsch, da alloc_system unter Windows darauf beruht, dass dieselbe Ausrichtung an die Freigabe übergeben wird, die bei der Zuweisung übergeben wurde.

Wenn man bedenkt, dass 99,99% der Zuweisungen nicht überausgerichtet sind, möchten Sie wirklich einen solchen Overhead für all diese Zuweisungen verursachen?

Es hängt von der Anwendung ab, ob der Overhead erheblich ist und ob Speicher oder Leistung optimiert werden sollen. Mein Verdacht ist, dass für die meisten Anwendungen beides in Ordnung ist, aber eine kleine Minderheit kümmert sich sehr um den Speicher und kann sich diese zusätzlichen Bytes wirklich nicht leisten. Und eine andere kleine Minderheit braucht Kontrolle über die Ausrichtung, und sie braucht sie wirklich .

@alexcrichton

Ich denke, dieser Kommentar ist falsch, da alloc_system unter Windows darauf beruht, dass dieselbe Ausrichtung an die Freigabe übergeben wird, die bei der Zuweisung übergeben wurde.

Bedeutet das nicht, dass alloc_system unter Windows das Merkmal Alloc nicht richtig implementiert (und daher sollten wir möglicherweise die Anforderungen des Merkmals Alloc ändern)?


@ retep998

Wenn ich Ihren Kommentar richtig lese, ist dieser Ausrichtungsaufwand nicht für alle Zuordnungen vorhanden, unabhängig davon, ob wir in der Lage sein müssen, die Zuordnung mit einer anderen Ausrichtung aufzuheben? Das heißt, wenn ich 64 Bytes mit 64-Byte-Ausrichtung zuordne und auch die Zuweisung mit 64-Byte-Ausrichtung freigebe, ist der von Ihnen beschriebene Overhead immer noch vorhanden. Daher ist es weniger ein Merkmal, die Zuordnung zu unterschiedlichen Ausrichtungen aufheben zu können, als vielmehr das Anfordern von Ausrichtungen, die größer als normal sind.

@joshlf Der durch alloc_system verursachte Overhead ist derzeit darauf zurückzuführen, dass mehr als normale Ausrichtungen angefordert werden. Wenn Ihre Ausrichtung kleiner oder gleich MEMORY_ALLOCATION_ALIGNMENT , entsteht kein Overhead durch alloc_system .

Wenn wir jedoch die Implementierung ändern, um die Freigabe mit unterschiedlichen Ausrichtungen zu fast alle Zuordnungen gelten, unabhängig von der Ausrichtung.

Ah ich sehe; macht Sinn.

Was bedeutet die Implementierung von Alloc für Heap und & Heap? In welchen Fällen würde der Benutzer eines dieser Geräte im Vergleich zum anderen verwenden?

Ist dies die erste Standardbibliotheks-API, in der *mut u8 "Zeiger auf was auch immer" bedeuten würde? Es gibt String :: from_raw_parts, aber das bedeutet wirklich Zeiger auf Bytes. Ich bin kein Fan von *mut u8 was "Zeiger auf was auch immer" bedeutet - sogar C macht es besser. Welche anderen Optionen gibt es? Vielleicht wäre ein Zeiger auf einen undurchsichtigen Typ sinnvoller.

@rfcbot betrifft * mut u8

@dtolnay Alloc for Heap ist eine Art "Standard" und Alloc for &Heap ist wie Write for &T wobei das Merkmal &mut self erfordert, die Implementierung jedoch nicht. Dies bedeutet insbesondere, dass Typen wie Heap und System threadsicher sind und beim Zuweisen nicht synchronisiert werden müssen.

Noch wichtiger ist jedoch, dass für die Verwendung von #[global_allocator] die Statik, an die es angehängt ist und die den Typ T hat, Alloc for &T . (aka alle globalen Allokatoren müssen threadsicher sein)

Für *mut u8 denke ich, dass *mut () interessant sein mag, aber ich persönlich fühle mich nicht gezwungen, dies hier per se "richtig zu machen".

Der Hauptvorteil von *mut u8 ist, dass es sehr praktisch ist, .offset mit Byte-Offsets zu verwenden.

Für *mut u8 denke ich, dass *mut () interessant sein mag, aber ich persönlich fühle mich nicht zu gezwungen, dies hier per se "richtig zu machen".

Wenn wir mit *mut u8 in einer stabilen Schnittstelle arbeiten, schließen wir uns dann nicht ein? Mit anderen Worten, wenn wir dies erst einmal stabilisiert haben, werden wir in Zukunft keine Chance mehr haben, dies richtig zu machen.

Außerdem scheint mir *mut () ein bisschen gefährlich zu sein, falls wir in Zukunft jemals eine Optimierung wie RFC 2040 vornehmen sollten.

Der Hauptvorteil von *mut u8 ist, dass es sehr bequem ist, .offset mit Byte-Offsets zu verwenden.

Stimmt, aber Sie könnten leicht let ptr = (foo as *mut u8) und dann Ihren fröhlichen Weg gehen. Das scheint nicht genug Motivation zu sein, bei *mut u8 in der API zu bleiben, wenn es überzeugende Alternativen gibt (die, um fair zu sein, ich bin mir nicht sicher, ob es solche gibt).

Außerdem scheint mir * mut () ein bisschen gefährlich zu sein, falls wir in Zukunft jemals eine Optimierung wie RFC 2040 vornehmen sollten.

Diese Optimierung wird wahrscheinlich schon nie stattfinden - sie würde zu viel vorhandenen Code beschädigen. Selbst wenn dies der Fall wäre, würde es auf &() und &mut () angewendet, nicht auf *mut () .

Wenn RFC 1861 kurz vor der Implementierung / Stabilisierung steht, würde ich vorschlagen, es zu verwenden:

extern { pub type void; }

pub unsafe trait Alloc {
    unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut void, Error>;
    unsafe fn dealloc(&mut self, ptr: *mut void, layout: Layout);
    // ...
}

Es ist wahrscheinlich zu weit weg, oder?

@joshlf Ich dachte, ich hätte eine PR über sie geöffnet, das verbleibende Unbekannte ist DynSized .

Funktioniert dies für Struktur-Hack-ähnliche Objekte? Angenommen, ich habe ein Node<T> , das so aussieht:

struct Node<T> {
   size: u32,
   data: T,
   // followed by `size` bytes
}

und ein Werttyp:

struct V {
  a: u32,
  b: bool,
}

Jetzt möchte ich Node<V> mit einer Zeichenfolge der Größe 7 in einer einzigen Zuordnung zuweisen. Idealerweise möchte ich eine Zuordnung der Größe 16 vornehmen, 4 ausrichten und alles hineinpassen: 4 für u32 , 5 für V und 7 für die Zeichenfolgenbytes. Dies funktioniert, weil das letzte Mitglied von V die Ausrichtung 1 hat und die Zeichenfolgenbytes auch die Ausrichtung 1 haben.

Beachten Sie, dass dies in C / C ++ nicht zulässig ist, wenn die Typen wie oben beschrieben zusammengesetzt sind, da das Schreiben in einen gepackten Speicher ein undefiniertes Verhalten ist. Ich denke, dies ist eine Lücke im C / C ++ - Standard, die leider nicht behoben werden kann. Ich kann erläutern, warum dies nicht funktioniert, aber wir konzentrieren uns stattdessen auf Rust. Kann das funktionieren? :-)

In Bezug auf die Größe und Ausrichtung der Node<V> -Struktur selbst sind Sie ziemlich nach Lust und Laune des Rust-Compilers. Es ist UB (undefiniertes Verhalten), eine Größe oder Ausrichtung zuzuweisen, die kleiner ist als die von Rust geforderten, da Rust Optimierungen vornehmen kann, die auf der Annahme basieren, dass ein beliebiges Node<V> -Objekt - auf dem Stapel, auf dem Heap, hinter einer Referenz usw. - hat eine Größe und Ausrichtung, die mit denen übereinstimmen, die zur Kompilierungszeit erwartet werden.

In der Praxis sieht es so aus, als ob die Antwort leider Nein lautet: Ich habe dieses Programm ausgeführt und festgestellt, dass Node<V> zumindest auf dem Rust Playground eine Größe von 12 und eine Ausrichtung von 4 hat, was bedeutet, dass alle Objekte danach sind Die Node<V> müssen um mindestens 12 Bytes versetzt sein. Es sieht so aus, als ob der Versatz des data.b -Felds innerhalb des Node<V> 8 Bytes beträgt, was bedeutet, dass die Bytes 9-11 nach dem Auffüllen sind. Obwohl diese Füllbytes in gewissem Sinne "nicht verwendet" werden, behandelt der Compiler sie leider immer noch als Teil des Node<V> und behält sich das Recht vor, mit ihnen alles zu tun, was ihm gefällt (vor allem einschließlich des Schreibens) für sie, wenn Sie einem Node<V> zuweisen, was bedeutet, dass wenn Sie versuchen, zusätzliche Daten dort zu entfernen, diese möglicherweise überschrieben werden).

(Übrigens: Sie können einen Typ nicht als gepackt behandeln, den der Rust-Compiler nicht für gepackt hält. Sie können dem Rust-Compiler jedoch mitteilen, dass etwas gepackt ist, wodurch sich das Layout des Typs ändert (Auffüllen entfernen) repr(packed) )

In Bezug auf das Auslegen eines Objekts nach dem anderen, ohne dass beide Teil desselben Rust-Typs sind, bin ich mir jedoch fast zu 100% sicher, dass dies gültig ist - schließlich ist es das, was Vec tut. Mit den Methoden vom Typ Layout können Sie dynamisch berechnen, wie viel Speicherplatz für die Gesamtzuweisung benötigt wird:

let node_layout = Layout::new::<Node<V>>();
// NOTE: This is only valid if the node_layout.align() is at least as large as mem::align_of_val("a")!
// NOTE: I'm assuming that the alignment of all strings is the same (since str is unsized, you can't do mem::align_of::<str>())
let padding = node_layout.padding_needed_for(mem::align_of_val("a"));
let total_size = node_layout.size() + padding + 7;
let total_layout = Layout::from_size_align(total_size, node_layout.align()).unwrap();

Würde so etwas funktionieren?

#[repr(C)]
struct Node<T> {
   size: u32,
   data: T,
   bytes: [u8; 0],
}

… Dann mit einer größeren Größe zuweisen und slice::from_raw_parts_mut(node.bytes.as_mut_ptr(), size) ?

Danke @joshlf für die ausführliche Antwort! Die TLDR für meinen Anwendungsfall ist, dass ich ein Node<V> der Größe 16 erhalten kann, aber nur, wenn V repr(packed) . Ansonsten ist das Beste, was ich tun kann, Größe 19 (12 + 7).

@ SimonSapin nicht sicher; Ich werde es versuchen.

Ich habe diesen Thread noch nicht wirklich eingeholt, aber ich bin absolut dagegen, noch etwas zu stabilisieren. Wir haben noch keine Fortschritte bei der Umsetzung der schwierigen Probleme erzielt:

  1. Allokator-polymorphe Sammlungen

    • nicht einmal nicht aufgeblähte Box!

  2. Fehlbare Sammlungen

Ich denke, das Design der grundlegenden Merkmale wird sich auf die Lösungen dieser auswirken: Ich hatte in den letzten Monaten wenig Zeit für Rust, habe mich aber manchmal darüber gestritten. Ich bezweifle, dass ich auch hier Zeit haben werde, meinen Fall vollständig darzulegen, daher kann ich nur hoffen, dass wir zunächst zumindest eine vollständige Lösung für all diese Fragen aufstellen: Jemand beweist mir das Gegenteil, dass es unmöglich ist, rigoros (korrekte Verwendung erzwingen) und flexibel zu sein und ergonomisch mit den aktuellen Merkmalen. Oder beenden Sie einfach das Kontrollkästchen oben.

Betreff :

Ich denke, dass eine relevante Frage im Zusammenhang mit dem Konflikt zwischen dieser Perspektive und @alexcrichtons Wunsch, etwas zu stabilisieren, lautet: Wie viel Nutzen Alloc Methoden direkt aufrufen (selbst die meisten Sammlungen werden wahrscheinlich Box oder einen ähnlichen Container verwenden), sodass die eigentliche Frage lautet: Was bedeutet Stabilisierung für Benutzer, die dies tun werden? Nicht Alloc Methoden direkt aufrufen? Ehrlich gesagt ist der einzige ernsthafte Anwendungsfall, den ich mir vorstellen kann, dass er einen Weg für allokatorpolymorphe Sammlungen ebnet (die wahrscheinlich von einer viel breiteren Gruppe von Benutzern verwendet werden), aber es scheint, dass dies auf # 27336 blockiert ist, was weit davon entfernt ist gelöst werden. Vielleicht fehlen mir noch andere große Anwendungsfälle, aber aufgrund dieser schnellen Analyse neige ich dazu, mich von der Stabilisierung abzuwenden, da sie nur marginale Vorteile hat, wenn wir uns an ein Design binden, das wir später möglicherweise als suboptimal empfinden .

@joshlf ermöglicht es den Allokatoren zu definieren und zu verwenden.

Hmmm guter Punkt. Wäre es möglich, die Angabe des globalen Allokators zu stabilisieren, ohne Alloc zu stabilisieren? Das heißt, der Code, der Alloc implementiert, müsste instabil sein, aber das würde wahrscheinlich in seiner eigenen Kiste eingekapselt sein, und der Mechanismus, um diesen Allokator als globalen Allokator zu markieren, wäre selbst stabil. Oder verstehe ich falsch, wie stabil / instabil und der stabile Compiler / nächtliche Compiler interagieren?

Ah @joshlf, denken https://github.com/rust-lang/rust/issues/42774#issuecomment -317279035 angegeben. Ich bin mir ziemlich sicher, dass wir auf andere Probleme stoßen werden - Probleme mit den Merkmalen, wie sie existieren, weshalb ich arbeiten möchte, um jetzt mit der Arbeit daran zu beginnen. Es ist viel einfacher, diese Probleme zu diskutieren, sobald sie für alle sichtbar sind, als die prognostizierten Zukünfte nach # 27336 zu diskutieren.

@joshlf Aber Sie können die Kiste, die den globalen

@sfackler Ah ja, es gibt dieses Missverständnis, vor dem ich Angst hatte: P.

Ich finde den Namen Excess(ptr, usize) etwas verwirrend, weil das usize nicht das excess in der Größe der angeforderten Zuordnung ist (wie in der zusätzlichen zugewiesenen Größe), sondern das total Größe der Zuordnung.

IMO Total , Real , Usable oder jeder Name, der angibt, dass die Größe die Gesamtgröße oder die tatsächliche Größe der Zuordnung ist, ist besser als "Überschuss", den ich finde irreführend. Gleiches gilt für die Methoden _excess .

Ich stimme @gnzlbg oben zu, ich denke, ein einfaches (ptr, usize) Tupel wäre in Ordnung.

Beachten Sie, dass Excess jedoch nicht im ersten Durchgang stabilisiert werden soll

Hat diesen Thread zur Diskussion über reddit gepostet, bei dem einige Leute Bedenken haben: https://www.reddit.com/r/rust/comments/78dabn/custom_allocators_are_on_the_verge_of_being/

Nach einer weiteren Diskussion mit @ rust-lang / libs heute möchte ich einige Änderungen am Stabilisierungsvorschlag vornehmen, die zusammengefasst werden können mit:

  • Fügen Sie alloc_zeroed zu den stabilisierten Methoden hinzu, andernfalls haben Sie dieselbe Signatur wie alloc .
  • Ändern Sie *mut u8 in *mut void in der API mithilfe der Unterstützung von extern { type void; } , um das Problem von c_void gesamten Ökosystem bereitzustellen .
  • Ändern Sie den Rückgabetyp von alloc in *mut void , und entfernen Sie die Result und die Error

Das vielleicht umstrittenste ist der letzte Punkt, deshalb möchte ich auch darauf näher eingehen. Dies kam aus der heutigen Diskussion mit dem libs-Team und drehte sich speziell darum, wie (a) die auf Result basierende Schnittstelle einen weniger effizienten ABI hat als eine Zeiger-Rückgabe und (b) heute fast keine "Produktions" -Zuweiser bieten die Möglichkeit, mehr als "dies nur OOM'd" zu lernen. Für die Leistung können wir meistens mit Inlining und dergleichen darüber Papier machen, aber es bleibt, dass Error eine zusätzliche Nutzlast ist, die auf den untersten Ebenen schwer zu entfernen ist.

Für die Rückgabe von Nutznutzungen von Fehlern wird davon ausgegangen, dass Allokatoren eine implementierungsspezifische Selbstbeobachtung durchführen können, um zu erfahren, warum eine Allokation fehlgeschlagen ist, und ansonsten sollten fast alle Verbraucher nur wissen müssen, ob die Allokation erfolgreich war oder fehlgeschlagen ist. Darüber hinaus soll dies eine API auf sehr niedriger Ebene sein, die eigentlich nicht so oft aufgerufen wird (stattdessen sollten stattdessen die typisierten APIs aufgerufen werden, die die Dinge gut zusammenfassen). In diesem Sinne ist es nicht von größter Bedeutung, dass wir die benutzerfreundlichste und ergonomischste API für diesen Standort haben, sondern es ist wichtiger, Anwendungsfälle zu aktivieren, ohne die Leistung zu beeinträchtigen.

Der Hauptvorteil von *mut u8 ist, dass es sehr praktisch ist, .offset mit Byte-Offsets zu verwenden.

In der libs-Besprechung haben wir auch impl *mut void { fn offset } was nicht im Widerspruch zu den vorhandenen offset die für T: Sized . Könnte auch byte_offset .

+1 für die Verwendung von *mut void und byte_offset . Wird es ein Problem mit der Stabilisierung der Funktion für externe Typen geben, oder können wir dieses Problem umgehen, da nur die Definition instabil ist (und liballoc intern instabile Dinge ausführen kann) und nicht die Verwendung (z. B. let a: *mut void = ... isn) nicht instabil)?

Ja, wir müssen die externe Typstabilisierung nicht blockieren. Selbst wenn die Unterstützung für externe Typen gelöscht wird, kann das void uns definierte

Gab es in der libs-Sitzung weitere Diskussionen darüber, ob Alloc und Dealloc getrennte Merkmale sein sollten oder nicht?

Wir haben das nicht speziell angesprochen, aber wir waren im Allgemeinen der Meinung, dass wir nicht vom Stand der Technik abweichen sollten, es sei denn, wir haben einen besonders zwingenden Grund dafür. Insbesondere das Allocator-Konzept von C ++ weist keine ähnliche Aufteilung auf.

Ich bin mir nicht sicher, ob das in diesem Fall ein passender Vergleich ist. In C ++ wird alles explizit freigegeben, sodass es kein Äquivalent zu Box , das eine Kopie (oder einen Verweis auf) seines eigenen Allokators speichern muss. Das ist es, was die große Explosion für uns verursacht.

@joshlf unique_ptr entspricht Box , vector entspricht Vec , unordered_map entspricht HashMap usw.

@cramertj Ah, interessant, ich habe nur Sammlungsarten betrachtet. Es scheint, dass dies dann eine Sache sein könnte. Wir können es später jederzeit über Blanket-Impls hinzufügen, aber es wäre wahrscheinlich sauberer, dies zu vermeiden.

Der Blanket-Impl-Ansatz könnte tatsächlich sauberer sein:

pub trait Dealloc {
    fn dealloc(&self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

Ein Merkmal weniger, über das Sie sich in den meisten Anwendungsfällen Sorgen machen müssen.

  • Ändern Sie den Rückgabetyp der Zuweisung in * mut void, und entfernen Sie das Ergebnis und den Fehler

Das vielleicht umstrittenste ist der letzte Punkt, deshalb möchte ich auch darauf näher eingehen. Dies kam aus der heutigen Diskussion mit dem libs-Team und drehte sich speziell darum, wie (a) die ergebnisbasierte Schnittstelle einen weniger effizienten ABI aufweist als ein Zeiger, der Zeiger zurückgibt, und (b) heute fast keine "Produktions" -Zuweiser die Fähigkeit zum Lernen bieten alles andere als "das nur OOM'd". Für die Leistung können wir meistens mit Inlining und dergleichen darüber Papier, aber es bleibt, dass der Fehler eine zusätzliche Nutzlast ist, die auf den untersten Ebenen schwer zu entfernen ist.

Ich mache mir Sorgen, dass dies die Verwendung des zurückgegebenen Zeigers sehr einfach machen würde, ohne nach Null zu suchen. Es scheint, dass der Overhead auch entfernt werden könnte, ohne dieses Risiko hinzuzufügen, indem Result<NonZeroPtr<void>, AllocErr> und AllocErr Null gesetzt wird.

( NonZeroPtr ist ein zusammengeführtes ptr::Shared und ptr::Unique wie in https://github.com/rust-lang/rust/issues/27730#issuecomment-316236397 vorgeschlagen.)

@SimonSapin so etwas wie Result<NonZeroPtr<void>, AllocErr> erfordert die Stabilisierung von drei Typen, die alle brandneu sind und von denen einige in der Vergangenheit einiges an Stabilisierung verloren haben. So etwas wie void ist nicht einmal erforderlich und (meiner Meinung nach) eine nette Sache.

Ich bin damit einverstanden, dass es "einfach ist, es zu verwenden, ohne auf Null zu prüfen", aber dies ist wiederum eine sehr einfache API, die nicht für eine starke Nutzung vorgesehen ist. Daher denke ich nicht, dass wir die Ergonomie der Anrufer optimieren sollten.

Leute können auch übergeordnete Abstraktionen wie alloc_one über die niedrigen alloc erstellen, die komplexere Rückgabetypen wie Result<NonZeroPtr<void>, AllocErr> haben könnten.

Ich bin damit einverstanden, dass AllocErr in der Praxis nicht nützlich wäre, aber wie wäre es mit nur Option<NonZeroPtr<void>> ? APIs, die ohne Overhead nicht versehentlich missbraucht werden können, sind eines der Dinge, die Rust von C unterscheiden, und die Rückkehr zu Nullzeigern im C-Stil ist für mich ein Rückschritt. Zu sagen, dass es sich um eine „API auf sehr niedriger Ebene handelt, die nicht stark genutzt werden soll“, ist wie zu sagen, dass wir uns bei ungewöhnlichen Mikrocontroller-Architekturen nicht um die Speichersicherheit kümmern sollten, da sie sehr niedrig sind und nicht stark genutzt werden.

Jede Interaktion mit dem Allokator beinhaltet unsicheren Code, unabhängig vom Rückgabetyp dieser Funktion. Niedrige Zuordnungs-APIs können missbrauchen, ob der Rückgabetyp Option<NonZeroPtr<void>> oder *mut void .

Alloc::alloc insbesondere die API, die auf niedriger Ebene ist und nicht für eine starke Nutzung vorgesehen ist. Methoden wie Alloc::alloc_one<T> oder Alloc::alloc_array<T> sind die Alternativen, die häufiger verwendet werden und einen "schöneren" Rückgabetyp haben.

Ein Stateful AllocError ist es nicht wert, aber ein Typ mit der Größe Null, der Error implementiert und einen Display von allocation failure ist schön zu haben. Wenn wir den Weg von NonZeroPtr<void> , sehe ich Result<NonZeroPtr<void>, AllocError> gegenüber Option<NonZeroPtr<void>> vorzuziehen.

Warum der Ansturm zur Stabilisierung :( !! Result<NonZeroPtr<void>, AllocErr> für Kunden unbestreitbar angenehmer ist. Zu sagen, dass dies eine "API auf sehr niedriger Ebene" ist, die nicht nett sein muss, ist nur bedrückend ehrgeizig. Code auf allen Ebenen sollte es sein so sicher und wartbar wie möglich, undurchsichtiger Code, der nicht ständig bearbeitet wird (und daher in den Kurzzeiterinnerungen der Menschen angezeigt wird), umso mehr!

Wenn wir außerdem von Benutzern geschriebene allokationspolymorphe Sammlungen haben sollen, auf die ich sicherlich hoffe, ist dies eine offene Menge ziemlich komplexen Codes, der Allokatoren direkt verwendet.

Operativ möchten wir den Alloactor operativ nur einmal pro baumbasierter Sammlung referenzieren / klonen. Das bedeutet, dass der Allokator an jede benutzerdefinierte Allokatorbox übergeben wird, die zerstört wird. Es ist jedoch ein offenes Problem, wie dies in Rust ohne lineare Typen am besten funktioniert. Im Gegensatz zu meinem vorherigen Kommentar wäre ich mit etwas unsicherem Code in Sammlungsimplementierungen in Ordnung, da der ideale Verwendungsfall die Implementierung von Box ändert, nicht die Implementierung von Split Allocator- und Deallocator-Merkmalen. Das heißt, wir können stabilisierbare Fortschritte erzielen, ohne die Linearität zu blockieren.

@sfackler Ich denke, wir brauchen einige zugehörige Typen, die den Deallocator mit dem Allocator verbinden. eine Nachrüstung ist möglicherweise nicht möglich.

@ Ericson2314 Es gibt einen "Ansturm" zur Stabilisierung, weil die Leute

Wofür würde dieser zugehörige Typ verwendet?

@sfackler Leute können immer noch eine nächtliche / und Art von Leuten für immer , es sei denn, wir möchten das Ökosystem mit einem neuen 2.0-Standard aufteilen.

Die zugehörigen Typen würden den Deallocator mit dem Allokator in Beziehung setzen. Jeder muss über den anderen Bescheid wissen, damit dies funktioniert. [Es gibt immer noch das Problem, den falschen (De-) Allokator des richtigen Typs zu verwenden, aber ich akzeptiere, dass niemand aus der Ferne eine Lösung dafür vorgeschlagen hat.]

Wenn die Leute sich nur an eine Nacht halten können, warum haben wir dann überhaupt stabile Builds? Die Gruppe von Personen, die direkt mit Allokator-APIs interagieren, ist viel kleiner als die Personen, die diese APIs nutzen möchten, indem sie beispielsweise den globalen Allokator ersetzen.

Können Sie einen Code schreiben, der zeigt, warum ein Deallocator den Typ seines zugeordneten Allokators kennen muss? Warum benötigt die Allokator-API von C ++ keine ähnliche Zuordnung?

Wenn die Leute sich nur an eine Nacht halten können, warum haben wir dann überhaupt stabile Builds?

Sprachstabilität anzeigen. Code, den Sie gegen diese Version von Dingen schreiben, wird niemals kaputt gehen. Auf einem neueren Compiler. Sie stecken eine Nacht fest, wenn Sie etwas so Schlimmes brauchen, dass es sich nicht lohnt, auf die endgültige Iteration der Funktion zu warten, deren Qualität dieser Garantie würdig ist.

Die Gruppe von Personen, die direkt mit Allokator-APIs interagieren, ist viel kleiner als die Personen, die diese APIs nutzen möchten, indem sie beispielsweise den globalen Allokator ersetzen.

Aha! Dies wäre für das Verschieben von Jemalloc aus dem Baum usw.? Niemand hat vorgeschlagen, die schrecklichen Hacks zu stabilisieren, die die Auswahl des globalen Allokators ermöglichen, nur die statische Haufenmenge selbst? Oder habe ich den Vorschlag falsch gelesen?

Es wird vorgeschlagen, die schrecklichen Hacks, die die Auswahl des globalen Allokators ermöglichen, zu stabilisieren. Dies ist die Hälfte dessen, was es uns ermöglicht, Jemalloc aus dem Baum zu entfernen. Dieses Problem ist die andere Hälfte.

#[global_allocator] Attributstabilisierung: https://github.com/rust-lang/rust/issues/27389#issuecomment -336955367

Huch

@ Ericson2314 Was denkst du wäre eine nicht schreckliche Möglichkeit, den globalen

(Beantwortet in https://github.com/rust-lang/rust/issues/27389#issuecomment-342285805)

Der Vorschlag wurde geändert, um * mut void zu verwenden.

@rfcbot gelöst * mut u8

@rfcbot überprüft

Nach einigen Diskussionen über IRC stimme ich dem mit dem Verständnis zu, dass wir nicht beabsichtigen, ein Box Generikum auf Alloc zu stabilisieren, sondern auf einem Dealloc Merkmal mit einem entsprechende Decke impl, wie von @sfackler hier vorgeschlagen . Bitte lassen Sie mich wissen, wenn ich die Absicht missverstanden habe.

@cramertj Nur um zu verdeutlichen, ist es möglich, dieses pauschale Impl nachträglich hinzuzufügen und nicht die Alloc Definition zu brechen, die wir hier stabilisieren?

@joshlf yep, es würde so aussehen: https://github.com/rust-lang/rust/issues/32838#issuecomment -340959804

Wie geben wir die Dealloc für eine bestimmte Alloc ? Ich würde mir so etwas vorstellen?

pub unsafe trait Alloc {
    type Dealloc: Dealloc = Self;
    ...
}

Ich denke, das bringt uns in ein heikles Gebiet. WRT https://github.com/rust-lang/rust/issues/29661.

Ja, ich glaube nicht, dass es eine Möglichkeit gibt, das Hinzufügen von Dealloc abwärtskompatibel mit vorhandenen Definitionen von Alloc (denen dieser Typ nicht zugeordnet ist) zu haben, ohne einen Standard zu haben.

Wenn Sie automatisch den einem Allokator entsprechenden Deallocator abrufen möchten, benötigen Sie mehr als nur einen zugeordneten Typ, sondern eine Funktion zum Erzeugen eines Deallocator-Werts.

Aber dies kann in Zukunft gehandhabt werden, wenn das Zeug an einen separaten Subtrait von Alloc angehängt wird, denke ich.

@sfackler Ich bin nicht sicher, ob ich verstehe. Können Sie die Signatur von Box::new unter Ihrem Design ausschreiben?

Dies ignoriert die Platzierungssyntax und all das, aber eine Möglichkeit, dies zu tun, wäre

pub struct Box<T, D>(NonZeroPtr<T>, D);

impl<T, D> Box<T, D>
where
    D: Dealloc
{
    fn new<A>(alloc: A, value: T) -> Box<T, D>
    where
        A: Alloc<Dealloc = D>
    {
        let ptr = alloc.alloc_one().unwrap_or_else(|_| alloc.oom());
        ptr::write(&value, ptr);
        let deallocator = alloc.deallocator();
        Box(ptr, deallocator)
    }
}

Insbesondere müssen wir tatsächlich in der Lage sein, eine Instanz des Deallocators zu erstellen und nicht nur dessen Typ zu kennen. Sie können auch Box über Alloc parametrisieren und stattdessen A::Dealloc speichern, was bei der Typinferenz hilfreich sein kann. Wir können dies nach dieser Stabilisierung zum Laufen bringen, indem wir Dealloc und deallocator in ein separates Merkmal verschieben:

pub trait SplitAlloc: Alloc {
    type Dealloc;

    fn deallocator(&self) -> Self::Dealloc;
}

Aber wie würde der Impl von Drop aussehen?

impl<T, D> Drop for Box<T, D>
where
    D: Dealloc
{
    fn drop(&mut self) {
        unsafe {
            ptr::drop_in_place(self.0);
            self.1.dealloc_one(self.0);
        }
    }
}

Aber wenn wir zuerst Alloc stabilisieren, dann implementieren nicht alle Alloc s Dealloc , oder? Und ich dachte, die Spezialisierung auf Impl ist noch weit entfernt? Mit anderen Worten, theoretisch möchten Sie so etwas wie das Folgende tun, aber ich denke, es funktioniert noch nicht?

impl<T, D> Drop for Box<T, D> where D: Dealloc { ... }
impl<T, A> Drop for Box<T, A> where A: Alloc { ... }

Wenn überhaupt, hätten wir eine

default impl<T> SplitAlloc for T
where
    T: Alloc { ... }

Aber ich denke nicht, dass das wirklich notwendig wäre. Die Anwendungsfälle für benutzerdefinierte Allokatoren und globale Allokatoren sind so unterschiedlich, dass ich nicht davon ausgehen würde, dass es eine Menge Überlappungen zwischen ihnen geben würde.

Ich nehme an, das könnte funktionieren. Es scheint mir jedoch viel sauberer zu sein, sofort Dealloc haben, damit wir die einfachere Oberfläche haben können. Ich stelle mir vor, wir könnten eine ziemlich einfache, unumstrittene Oberfläche haben, die keine Änderung des vorhandenen Codes erfordert, der bereits Alloc implementiert:

unsafe trait Dealloc {
    fn dealloc(&mut self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

unsafe trait Alloc {
    type Dealloc: Dealloc = &mut Self;
    fn deallocator(&mut self) -> Self::Dealloc { self }
    ...
}

Ich dachte, zugehörige Typvorgaben waren problematisch?

Ein Dealloc , das ein veränderbarer Verweis auf den Allokator ist, scheint nicht allzu nützlich zu sein - Sie können immer nur eine Sache gleichzeitig zuweisen, oder?

Ich dachte, zugehörige Typvorgaben waren problematisch?

Oh, ich denke, die zugehörigen Typvorgaben sind weit genug entfernt, dass wir uns nicht auf sie verlassen können.

Trotzdem könnten wir das einfachere haben:

unsafe trait Dealloc {
    fn dealloc(&mut self, ptr: *mut void, layout: Layout);
}

impl<T> Dealloc for T
where
    T: Alloc
{
    fn dealloc(&self, ptr: *mut void, layout: Layout) {
        <T as Alloc>::dealloc(self, ptr, layout)
    }
}

unsafe trait Alloc {
    type Dealloc: Dealloc;
    fn deallocator(&mut self) -> Self::Dealloc;
    ...
}

und fordern Sie einfach den Implementierer auf, ein bisschen Boilerplate zu schreiben.

Ein Dealloc , das ein veränderlicher Verweis auf den Allokator ist, scheint nicht allzu nützlich zu sein - Sie können immer nur eine Sache gleichzeitig zuweisen, oder?

Ja, guter Punkt. Wahrscheinlich ein strittiger Punkt angesichts Ihres anderen Kommentars.

Sollte deallocator self , &self oder &mut self ?

Wahrscheinlich &mut self , um mit den anderen Methoden übereinzustimmen.

Gibt es Allokatoren, die es vorziehen würden, sich selbst nach Wert zu nehmen, damit sie nicht z. B. den Status klonen müssen?

Das Problem bei der Verwendung von self nach Wert besteht darin, dass es ausgeschlossen ist, Dealloc und dann weiter zuzuweisen.

Ich denke an einen hypothetischen "One-Shot" -Zuweiser, obwohl ich nicht weiß, wie real das ist.

Ein solcher Allokator könnte existieren, aber wenn Sie self nach Wert nehmen, müssen alle _ Allokatoren auf diese Weise funktionieren, und alle Allokatoren, die eine Allokation nach dem Aufruf von deallocator zulassen, werden ausgeschlossen.

Ich würde immer noch gerne sehen, dass einige davon implementiert und in Sammlungen verwendet werden, bevor wir darüber nachdenken, sie zu stabilisieren.

Denken Sie, dass https://github.com/rust-lang/rust/issues/27336 oder die in https://github.com/rust-lang/rust/issues/32838#issuecomment -339066870 erörterten Punkte dies ermöglichen Sammlungen vorantreiben?

Ich bin besorgt über die Auswirkungen des Typ-Alias-Ansatzes auf die Lesbarkeit der Dokumentation. Ein (sehr ausführlicher) Weg, um Fortschritt zu ermöglichen, wäre das Umschließen von Typen:

pub struct Vec<T>(alloc::Vec<T, Heap>);

impl<T> Vec<T> {
    // forwarding impls for everything
}

Ich weiß, dass es ein Schmerz ist, aber es scheint, dass die Änderungen, die wir hier diskutieren, groß genug sind, dass wir sie, wenn wir uns entscheiden, mit geteilten Allokations- / Dealloc-Merkmalen fortzufahren, zuerst im Standard ausprobieren und dann erneut FCP.

Was ist der Zeitplan, um darauf zu warten, dass dieses Zeug implementiert wird?

Die grow_in_place -Methode gibt keine überschüssige Kapazität zurück. Derzeit wird usable_size mit einem Layout aufgerufen, die Zuordnung wird so erweitert, dass sie mindestens zu diesem Layout passt. Wenn die Zuordnung jedoch über dieses Layout hinaus erweitert wird, können Benutzer dies nicht wissen.

Es fällt mir schwer, den Vorteil der Methoden alloc und realloc gegenüber alloc_excess und realloc_excess verstehen.

Ein Allokator muss einen geeigneten Speicherblock finden, um eine Zuordnung durchzuführen: Dies erfordert die Kenntnis der Größe des Speicherblocks. Ob der Allokator dann einen Zeiger zurückgibt oder das Tupel "Zeiger und Größe des Speicherblocks" keine messbaren Leistungsunterschiede macht.

alloc und realloc erhöhen also nur die API-Oberfläche und scheinen das Schreiben von weniger performantem Code zu fördern. Warum haben wir sie überhaupt in der API? Was ist ihr Vorteil?


BEARBEITEN: oder mit anderen Worten: Alle potenziell zuweisenden Funktionen in der API sollten Excess , wodurch im Grunde alle _excess -Methoden

Überschuss ist nur für Anwendungsfälle interessant, bei denen Arrays wachsen können. Es ist beispielsweise für Box oder BTreeMap nicht nützlich oder relevant. Die Berechnung des Überschusses kann einige Kosten verursachen, und es gibt sicherlich eine komplexere API. Daher scheint es mir nicht so, als ob Code, der sich nicht um Überkapazitäten kümmert, gezwungen werden sollte, dafür zu zahlen.

Die Berechnung des Überschusses kann einige Kosten verursachen

Kannst du ein Beispiel geben? Ich kenne und kann mir keinen Allokator vorstellen, der Speicher zuordnen kann, aber nicht weiß, wie viel Speicher er tatsächlich zuweist (was Excess ist: tatsächliche Menge des zugewiesenen Speichers; wir sollten benenne es um).

Der einzige häufig verwendete Alloc ator, bei dem dies leicht umstritten sein könnte, ist POSIX malloc , der, obwohl er die Excess intern immer berechnet, sie nicht als Teil seines C verfügbar macht API. Die Rückgabe der angeforderten Größe als Excess ist jedoch in Ordnung, tragbar, einfach, verursacht keinerlei Kosten und wird von jedem, der POSIX malloc ohnehin bereits angenommen.

jemalloc und im Grunde alle anderen Alloc ator da draußen bieten APIs, die die Excess ohne dass Kosten anfallen. Für diese Allokatoren ist die Rückgabe der Excess also Null kosten auch.

Die Berechnung des Überschusses kann einige Kosten verursachen, und es gibt sicherlich eine komplexere API. Daher scheint es mir nicht so, als ob Code, der sich nicht um Überkapazitäten kümmert, gezwungen werden sollte, dafür zu zahlen.

Im Moment zahlt jeder bereits den Preis für das Allokatormerkmal mit zwei APIs für die Zuweisung von Speicher. Und während man eine Excess -lose API auf einer Excess -vollsten API erstellen kann, ist das Gegenteil nicht der Fall. Ich frage mich also, warum das nicht so gemacht wird:

  • Alloc Trait-Methoden geben immer Excess
  • Fügen Sie ein ExcessLessAlloc Merkmal hinzu, das nur die Excess von Alloc Methoden für alle Benutzer entfernt, die 1) sich genug um die Verwendung von Alloc kümmern, aber 2) dies nicht tun kümmern Sie sich um die tatsächliche Menge an Speicher, die derzeit zugewiesen wird (sieht für mich wie eine Nische aus, aber ich denke immer noch, dass eine solche API schön zu haben ist)
  • Wenn eines Tages jemand einen Weg findet, Alloc Atoren mit schnellen Pfaden für Excess -lose Methoden zu implementieren, können wir immer eine benutzerdefinierte Implementierung von ExcessLessAlloc dafür bereitstellen.

FWIW Ich bin gerade wieder in diesem Thread gelandet, weil ich nicht implementieren kann, was ich will, zusätzlich zu Alloc . Ich erwähnte, dass es vorher grow_in_place_excess fehlt, aber ich bin gerade wieder festgefahren, weil es auch alloc_zeroed_excess fehlt (und wer weiß was noch).

Ich würde mich wohler fühlen, wenn sich die Stabilisierung hier zuerst auf die Stabilisierung einer Excess -vollsten API konzentrieren würde. Selbst wenn die API nicht für alle Anwendungen die ergonomischste ist, würde eine solche API zumindest alle Anwendungen zulassen, was eine notwendige Bedingung ist, um zu zeigen, dass das Design nicht fehlerhaft ist.

Kannst du ein Beispiel geben? Ich kenne und kann mir keinen Allokator vorstellen, der Speicher zuordnen kann, aber nicht weiß, wie viel Speicher er tatsächlich zuweist (was Excess ist: tatsächliche Menge des zugewiesenen Speichers; wir sollten benenne es um).

Die meisten Allokatoren verwenden heutzutage Größenklassen, wobei jede Größenklasse nur Objekte einer bestimmten festen Größe zuweist und Zuordnungsanforderungen, die nicht zu einer bestimmten Größenklasse passen, auf die kleinste Größenklasse aufgerundet werden, in die sie passen. In diesem Schema ist es üblich, Dinge wie ein Array von Größenklassenobjekten zu haben und dann classes[size / SIZE_QUANTUM].alloc() tun. In dieser Welt erfordert das Herausfinden, welche Größenklasse verwendet wird, zusätzliche Anweisungen: z. B. let excess = classes[size / SIZE_QUANTUM].size . Es mag nicht viel sein, aber die Leistung von Hochleistungszuweisern (wie Jemalloc) wird in einzelnen Taktzyklen gemessen, sodass dies einen erheblichen Overhead darstellen kann, insbesondere wenn diese Größe durch eine Kette von Funktionsrückgaben geleitet wird.

Kannst du ein Beispiel geben?

Zumindest wenn Sie Ihre PR an alloc_jemalloc senden, führt alloc_excess eindeutig mehr Code aus als alloc : https://github.com/rust-lang/rust/pull/45514/files.

In diesem Schema ist es üblich, Dinge wie ein Array von Größenklassenobjekten zu haben und dann Klassen [size / SIZE_QUANTUM] .alloc () auszuführen. In dieser Welt erfordert das Herausfinden, welche Größenklasse verwendet wird, zusätzliche Anweisungen: Lassen Sie z. B. überschüssige = Klassen [Größe / SIZE_QUANTUM] .size

Lassen Sie mich sehen, ob ich richtig folge:

// This happens in both cases:
let size_class = classes[size / SIZE_QUANTUM];
let ptr = size_class.alloc(); 
// This would happen only if you need to return the size:
let size = size_class.size;
return (ptr, size);

Ist es das?


Zumindest wenn Sie Ihre PR an alloc_jemalloc senden, führt alloc_excess eindeutig mehr Code als alloc aus

Diese PR war ein Bugfix (kein Perf-Fix), es gibt viele Dinge, die mit dem aktuellen Status unserer Jemalloc-Schicht in Bezug auf die Perfektion falsch sind, aber da diese PR zumindest das zurückgibt, was sie sollte:

  • nallocx ist eine const -Funktion im GCC-Sinne, dh eine echte reine Funktion. Dies bedeutet, dass es keine Nebenwirkungen hat, seine Ergebnisse nur von seinen Argumenten abhängen, dass es nicht auf einen globalen Status zugreift, dass seine Argumente keine Zeiger sind (die Funktion kann also nicht auf den globalen Status zugreifen, um sie zu werfen), und dass LLVM dies für C / C ++ - Programme verwenden kann Informationen, um den Anruf zu beenden, wenn das Ergebnis nicht verwendet wird. AFAIK Rust kann FFI C-Funktionen derzeit nicht als const fn oder ähnliches markieren. Dies ist also das erste, was behoben werden könnte, und dies würde realloc_excess Null für diejenigen bedeuten, die den Überschuss nicht verwenden, solange Inlining und Optimierungen ordnungsgemäß funktionieren.
  • nallocx wird immer für ausgerichtete Zuordnungen innerhalb von mallocx berechnet, das heißt, der gesamte Code verarbeitet es bereits, aber mallocx wirft das Ergebnis weg, sodass wir es hier tatsächlich zweimal berechnen , und in einigen Fällen ist nallocx fast so teuer wie mallocx ... Ich habe eine Gabel von Jemallocator, die einige Benchmarks für solche Dinge in ihren Filialen hat, aber dies muss stromaufwärts von behoben werden jemalloc durch Bereitstellung einer API, die dies nicht wegwirft. Dieser Fix betrifft jedoch nur diejenigen, die derzeit Excess .
  • und dann ist das Problem, dass wir die Align-Flags zweimal berechnen, aber das ist etwas, das LLVM auf unserer Seite optimieren kann (und das trivial zu beheben ist).

Also ja, es sieht nach mehr Code aus, aber dieser zusätzliche Code ist Code, den wir tatsächlich zweimal aufrufen, weil wir beim ersten Aufrufen die Ergebnisse weggeworfen haben. Es ist nicht unmöglich zu beheben, aber ich habe noch keine Zeit dafür gefunden.


EDIT: @sfackler Ich habe es heute geschafft, etwas Zeit dafür alloc_excess "frei" in Bezug auf alloc in jemallocs langsamem Pfad machen und habe nur einen Overhead von ~ 1ns in Jemallocs 'schneller Weg. Ich habe den schnellen Weg nicht wirklich detailliert betrachtet, aber es könnte möglich sein, ihn weiter zu verbessern. Die Details finden Sie hier: https://github.com/jemalloc/jemalloc/issues/1074#issuecomment -345040339

Ist es das?

Ja.

Dies ist also das erste, was behoben werden könnte, und dies würde realloc_excess für diejenigen, die den Überschuss nicht verwenden, zu Nullkosten machen, solange Inlining und Optimierungen ordnungsgemäß funktionieren.

Bei Verwendung als globaler Allokator kann nichts davon eingefügt werden.

Selbst wenn die API nicht für alle Anwendungen die ergonomischste ist, würde eine solche API zumindest alle Anwendungen zulassen, was eine notwendige Bedingung ist, um zu zeigen, dass das Design nicht fehlerhaft ist.

Es gibt buchstäblich keinen Code auf Github, der alloc_excess aufruft. Wenn dies ein so wichtiges Merkmal ist, warum hat es noch niemand benutzt? Die Zuordnungs-APIs von C ++ bieten keinen Zugriff auf überschüssige Kapazität. Es scheint unglaublich einfach zu sein, diese Funktionen in Zukunft abwärtskompatibel hinzuzufügen / zu stabilisieren, wenn es konkrete Beweise dafür gibt, dass sie die Leistung verbessern, und jeder sich tatsächlich genug darum kümmert, sie zu verwenden.

Bei Verwendung als globaler Allokator kann nichts davon eingefügt werden.

Dann ist dies ein Problem, das wir zumindest für LTO-Builds lösen sollten, da globale Allokatoren wie jemalloc angewiesen sind: nallocx ist die Art und Weise, wie es _by design_ ist, und die erste Empfehlung Die Entwickler von jemalloc haben uns bezüglich der Leistung von alloc_excess dass wir diese Aufrufe inline haben und C-Attribute ordnungsgemäß weitergeben sollten, damit der Compiler die Aufrufe von nallocx von den Aufrufseiten entfernt, die dies nicht tun Verwenden Sie die Excess , wie es C- und C ++ - Compiler tun.

Selbst wenn wir das nicht können, kann die Excess API durch Patchen der jemalloc API kostengünstig gemacht werden (ich habe eine erste Implementierung eines solchen Patches in meinem rust-lang / jemalloc Gabel). Wir könnten diese API entweder selbst pflegen oder versuchen, sie stromaufwärts zu landen, aber damit sie stromaufwärts landet, müssen wir gut begründen, warum diese anderen Sprachen diese Optimierungen durchführen können und Rust nicht. Oder wir müssen ein anderes Argument haben, da diese neue API für Benutzer, die Excess benötigen, deutlich schneller als mallocx + nallocx Excess .

Wenn dies ein so wichtiges Merkmal ist, warum hat es noch niemand benutzt?

Das ist eine gute Frage. std::Vec ist das Aushängeschild für die Verwendung der API Excess , wird jedoch derzeit nicht verwendet, und alle meine vorherigen Kommentare besagen, dass "dies und das in den Excess fehlen API "habe ich versucht, Vec dazu zu bringen, es zu benutzen. Die Excess API:

Ich kann nicht wissen, warum niemand diese API verwendet. Aber da nicht einmal die std -Bibliothek sie für die Datenstruktur verwenden kann, für die sie am besten geeignet ist ( Vec ), würde ich sagen, dass der Hauptgrund das ist Diese API ist derzeit defekt.

Wenn ich noch weiter raten müsste, würde ich sagen, dass nicht einmal diejenigen, die diese API entworfen haben, sie verwendet haben, hauptsächlich, weil keine einzelne std -Sammlung sie verwendet (wo ich erwarte, dass diese API zuerst getestet wird). und auch, weil die Verwendung von _excess und Excess überall, um usable_size / allocation_size zu bedeuten, äußerst verwirrend / ärgerlich ist, mit zu programmieren.

Dies liegt wahrscheinlich daran, dass mehr Arbeit in die Excess -losen APIs gesteckt wurde. Wenn Sie zwei APIs haben, ist es schwierig, sie synchron zu halten. Es ist für Benutzer schwierig, beide zu erkennen und zu wissen, welche zu verwenden sind. und schließlich fällt es den Benutzern schwer, die Bequemlichkeit dem richtigen vorzuziehen.

Mit anderen Worten, wenn ich zwei konkurrierende APIs habe und 100% der Arbeit in die Verbesserung einer und 0% der Arbeit in die Verbesserung der anderen stecke, ist es nicht überraschend, zu dem Schluss zu kommen, dass eine in der Praxis signifikant ist besser als der andere.

Soweit ich das beurteilen kann, sind dies die einzigen zwei Aufrufe von nallocx außerhalb der Jemalloc-Tests auf Github:

https://github.com/facebook/folly/blob/f2925b23df8d85ebca72d62a69f1282528c086de/folly/detail/ThreadLocalDetail.cpp#L182
https://github.com/louishust/mysql5.6.14_tokudb/blob/4897660dee3e8e340a1e6c8c597f3b2b7420654a/storage/tokudb/ft-index/ftcxx/malloc_utils.hpp#L91

Keiner von beiden ähnelt der aktuellen alloc_excess API, sondern wird eigenständig verwendet, um eine Zuordnungsgröße zu berechnen, bevor sie erstellt wird.

Apache Arrow untersuchte die Verwendung von nallocx in ihrer Implementierung, stellte jedoch fest, dass die Dinge nicht gut funktionierten:

https://issues.apache.org/jira/browse/ARROW-464

Dies sind im Grunde die einzigen Verweise auf nallocx ich finden kann. Warum ist es wichtig, dass die anfängliche Implementierung von Allokator-APIs eine so undurchsichtige Funktion unterstützt?

Soweit ich das beurteilen kann, sind dies die einzigen zwei Aufrufe von nallocx außerhalb der jemalloc-Tests auf Github:

Von der Spitze aus meinem Kopf Ich weiß , dass Vektortyp zumindest Facebook nutzt es über malloc - Implementierung facebook ( malloc und fbvector Wachstumspolitik , die ein großes Stück von C ++ 's Vektoren bei facebook Anwendung ist diesen) und auch die Kapelle früher die zur Verbesserung des Leistung ihres Typs String ( hier und das Tracking-Problem ). Vielleicht war heute nicht Githubs bester Tag?

Warum ist es wichtig, dass die anfängliche Implementierung von Allokator-APIs eine so undurchsichtige Funktion unterstützt?

Die anfängliche Implementierung einer Allokator-API muss diese Funktion nicht unterstützen.

Eine gute Unterstützung dieser Funktion sollte jedoch die Stabilisierung einer solchen API blockieren.

Warum sollte es die Stabilisierung blockieren, wenn es später abwärtskompatibel hinzugefügt werden kann?

Warum sollte es die Stabilisierung blockieren, wenn es später abwärtskompatibel hinzugefügt werden kann?

Denn für mich bedeutet das zumindest, dass nur die Hälfte des Designraums ausreichend erforscht wurde.

Erwarten Sie, dass die nicht überzähligen Teile der API durch das Design der übermäßigen Funktionen beeinflusst werden? Ich gebe zu, dass ich diese Diskussion nur halbherzig verfolgt habe, aber es scheint mir unwahrscheinlich.

Wenn wir diese API nicht erstellen können:

fn alloc(...) -> (*mut u8, usize) { 
   // worst case system API:
   let ptr = malloc(...);
   let excess = malloc_excess(...);
   (ptr, excess)
}
let (ptr, _) = alloc(...); // drop the excess

so effizient wie dieser:

fn alloc(...) -> *mut u8 { 
   // worst case system API:
   malloc(...)
}
let ptr = alloc(...);

dann haben wir größere Probleme.

Erwarten Sie, dass die nicht überzähligen Teile der API durch das Design der übermäßigen Funktionen beeinflusst werden?

Also ja, ich erwarte, dass eine gute Exzess-API einen großen Einfluss auf das Design der nicht überschüssigen Funktionalität hat: Sie würde sie vollständig entfernen.

Dies würde die aktuelle Situation verhindern, dass zwei APIs nicht synchron sind und die Excess-API weniger Funktionalität hat als die Excess-Less-API. Während man eine API ohne Überschuss auf eine API mit Überschuss aufbauen kann, ist das Gegenteil nicht der Fall.

Diejenigen, die die Excess fallen lassen möchten, sollten sie einfach fallen lassen.

Um zu verdeutlichen, ob es eine Möglichkeit gibt, eine alloc_excess -Methode nachträglich abwärtskompatibel hinzuzufügen, dann wären Sie damit einverstanden? (Aber natürlich bedeutet eine Stabilisierung ohne alloc_excess , dass das spätere Hinzufügen eine bahnbrechende Änderung wäre. Ich frage nur, damit ich Ihre Argumentation verstehe.)

@ Joshlf Das ist sehr einfach.

: bell: Dies tritt nun in die endgültige Kommentierungsphase ein , wie oben beschrieben . :Glocke:

Diejenigen, die den Überschuss fallen lassen wollen, sollten ihn einfach fallen lassen.

Alternativ können 0,01% der Menschen, die sich für Überkapazitäten interessieren, eine andere Methode anwenden.

@sfackler Dies ist, was ich für eine zweiwöchige Pause von Rost bekomme - ich vergesse die Standardmethode impls :)

Alternativ können 0,01% der Menschen, die sich für Überkapazitäten interessieren, eine andere Methode anwenden.

Woher bekommen Sie diese Nummer?

Alle meine Rust-Datenstrukturen sind flach im Speicher. Die Fähigkeit dazu ist der einzige Grund, warum ich Rust benutze. Wenn ich einfach alles boxen könnte, würde ich eine andere Sprache verwenden. Ich kümmere mich also nicht um die Excess die 0.01% der Zeit, ich kümmere mich die ganze Zeit darum.

Ich verstehe, dass dies domänenspezifisch ist und dass sich in anderen Domänen die Leute niemals für die Excess interessieren würden, aber ich bezweifle, dass sich nur 0,01% der Rust-Benutzer dafür interessieren (ich meine, viele Leute verwenden Vec und String , die Aushängeschilddatenstrukturen für Excess ).

Ich erhalte diese Zahl aus der Tatsache, dass es insgesamt ca. 4 Dinge gibt, die Nallocx verwenden, verglichen mit den Dingen, die Malloc verwenden.

@gnzlbg

Schlagen Sie vor, dass wir, wenn wir es von Anfang an "richtig" machen würden, nur fn alloc(layout) -> (ptr, excess) und überhaupt kein fn alloc(layout) -> ptr ? Das scheint mir alles andere als offensichtlich. Selbst wenn Überschuss verfügbar ist, scheint es naheliegend, die letztere API für Anwendungsfälle zu haben, in denen Überschuss keine Rolle spielt (z. B. die meisten Baumstrukturen), selbst wenn er als alloc_excess(layout).0 implementiert ist.

@rkruppe

Das scheint mir alles andere als offensichtlich. Selbst wenn Überschuss verfügbar ist, scheint es natürlich, die letztere API für Anwendungsfälle zu haben, in denen Überschuss keine Rolle spielt (z. B. die meisten Baumstrukturen), selbst wenn er als alloc_excess (Layout) .0 implementiert ist.

Derzeit ist die API mit übermäßigem Überschuss zusätzlich zu der API ohne Überschuss implementiert. Um Alloc für einen Allokator ohne Überschuss zu implementieren, muss der Benutzer die Methoden alloc und dealloc angeben.

Wenn ich jedoch Alloc für einen übermäßig vollen Allokator implementieren möchte, muss ich mehr Methoden bereitstellen (mindestens alloc_excess , aber dies wächst, wenn wir in realloc_excess . alloc_zeroed_excess , grow_in_place_excess , ...).

Wenn wir es umgekehrt machen würden, das heißt, die API ohne Überschuss als Nizza zusätzlich zu der API mit Überschuss voll implementieren, dann reicht die Implementierung von alloc_excess und dealloc für die Unterstützung aus beide Arten von Allokatoren.

Die Benutzer, die sich nicht darum kümmern oder den Überschuss nicht zurückgeben oder abfragen können, können nur die Eingabegröße oder das Layout zurückgeben (was eine winzige Unannehmlichkeit darstellt), aber die Benutzer, die den Überschuss verarbeiten können und möchten, müssen ihn nicht implementieren weitere Methoden.


@sfackler

Ich erhalte diese Zahl aus der Tatsache, dass es insgesamt ca. 4 Dinge gibt, die Nallocx verwenden, verglichen mit den Dingen, die Malloc verwenden.

Angesichts dieser Fakten über die Verwendung von _excess im Rust-Ökosystem:

  • Insgesamt 0 Dinge verbrauchen _excess im Rost-Ökosystem
  • Insgesamt 0 Dinge verwenden _excess in der Rost-Standardbibliothek
  • Nicht einmal Vec und String können die _excess API ordnungsgemäß in der Rost std Bibliothek verwenden
  • Die _excess API ist instabil, nicht synchron mit der API ohne Überschuss, bis vor kurzem fehlerhaft (hat die excess überhaupt nicht zurückgegeben), ...

    und angesichts dieser Fakten über die Verwendung von _excess in anderen Sprachen:

  • Die API von jemalloc wird aufgrund der Abwärtskompatibilität von C- oder C ++ - Programmen nicht nativ unterstützt

  • C- und C ++ - Programme, die die überschüssige API von jemalloc verwenden möchten, müssen sich alle Mühe geben, um sie zu verwenden:

    • Deaktivieren des Systemzuordners und von jemalloc (oder tcmalloc)

    • Implementieren Sie die Standardbibliothek ihrer Sprache erneut (implementieren Sie im Fall von C ++ eine inkompatible Standardbibliothek).

    • Schreiben Sie ihren gesamten Stapel auf diese inkompatible Standardbibliothek

  • Einige Communitys (Firefox verwendet es, Facebook implementiert die Sammlungen in der C ++ - Standardbibliothek neu, um es verwenden zu können, ...) geben sich immer noch alle Mühe, es zu verwenden.

Diese beiden Argumente erscheinen mir plausibel:

  • Die excess API in std kann nicht verwendet werden, daher kann die Bibliothek std sie nicht verwenden, daher kann sie niemand verwenden, weshalb sie im Rust-Ökosystem nicht einmal verwendet wird .
  • Obwohl C und C ++ es nahezu unmöglich machen, diese API zu verwenden, sind große Projekte mit Arbeitskräften sehr bemüht, sie zu verwenden, weshalb sich zumindest eine potenziell winzige Community von Menschen sehr darum kümmert.

Ihr Argument:

  • Niemand verwendet die _excess API, daher interessieren sich nur 0,01% der Leute dafür.

nicht.

@alexcrichton Die Entscheidung, von -> Result<*mut u8, AllocErr> auf -> *mut void zu wechseln, kann für Leute, die die ursprüngliche Entwicklung der Allokator-RFCs verfolgt haben, eine bedeutende Überraschung sein.

Ich bin nicht anderer Meinung als die Punkte, die Sie machen , aber es schien dennoch, als wäre eine ganze Reihe von Menschen bereit gewesen, mit der "Schwergewichtigkeit" von Result über die erhöhte Wahrscheinlichkeit zu leben, eine Null zu verpassen Überprüfen Sie den zurückgegebenen Wert.

  • Ich ignoriere die vom ABI auferlegten Probleme mit der Laufzeiteffizienz, da ich wie @alexcrichton davon lösen könnten.

Gibt es eine Möglichkeit, die Sichtbarkeit dieser späten Änderung allein zu verbessern?

Ein Weg (aus dem Kopf): Ändern Sie die Signatur jetzt in einer PR für sich selbst in der Hauptniederlassung, während Allocator immer noch instabil ist. Und dann sehen Sie, wer sich über die PR beschwert (und wer feiert!).

  • Ist das zu hartnäckig? Es scheint per Definition weniger hartnäckig zu sein, als eine solche Änderung mit einer Stabilisierung zu verbinden ...

Zum Thema, ob *mut void oder Result<*mut void, AllocErr> : Es ist möglich, dass wir die Idee der getrennten Allokatormerkmale "High-Level" und "Low-Level", wie bereits erwähnt, überdenken in Take II des Allocator RFC .

(Wenn ich ernsthafte Einwände gegen den Rückgabewert von *mut void hätte, würde ich ihn natürlich über den fcpbot als Anliegen einreichen. Aber an dieser Stelle vertraue ich dem Urteil des libs-Teams ziemlich, vielleicht in ein Teil aufgrund von Müdigkeit über diese Allokator-Saga.)

@pnkfelix

Die Entscheidung, von -> Result<*mut u8, AllocErr> auf -> *mut void zu wechseln, kann für Menschen, die die ursprüngliche Entwicklung der Allokator-RFCs verfolgt haben, eine erhebliche Überraschung sein.

Letzteres impliziert, dass, wie bereits erwähnt, der einzige Fehler, den wir ausdrücken möchten, OOM ist. Ein etwas leichteres Zwischenprodukt, das immer noch den Vorteil hat, dass es nicht versehentlich auf Fehler überprüft wird, ist -> Option<*mut void> .

@gnzlbg

Die überschüssige API in std ist nicht verwendbar, daher kann die std-Bibliothek sie nicht verwenden, daher kann sie niemand verwenden, weshalb sie im Rust-Ökosystem nicht einmal verwendet wird.

Dann reparieren Sie es.

@pnkfelix

Zum Thema, ob mut void oder result

Das waren im Grunde unsere Gedanken, außer dass die High-Level-API in Alloc selbst als alloc_one , alloc_array usw. sein würde. Wir können diese sogar zuerst als Erweiterung im Ökosystem entwickeln lassen Merkmale, um zu sehen, auf welchen APIs Menschen konvergieren.

@pnkfelix

Der Grund, warum Layout nur Klonen und nicht Kopieren implementiert hat, ist, dass ich die Möglichkeit offen lassen wollte, dem Layouttyp mehr Struktur hinzuzufügen. Insbesondere bin ich immer noch daran interessiert, dass das Layout versucht, eine Typstruktur zu verfolgen, die zum Erstellen verwendet wurde (z. B. 16-Array von struct {x: u8, y: [char; 215]}), so dass Allokatoren dies tun würden Die Option, Instrumentierungsroutinen verfügbar zu machen, die angeben, aus welchen Typen ihr aktueller Inhalt besteht.

Wurde irgendwo damit experimentiert?

@sfackler Ich habe das meiste bereits gemacht und alles kann mit der duplizierten API gemacht werden (keine überschüssigen + _excess Methoden). Ich würde gut damit umgehen können, zwei APIs zu haben und momentan keine vollständige _excess API zu haben.

Das einzige, was mich noch ein bisschen beunruhigt, ist, dass man, um einen Allokator zu implementieren, alloc + dealloc implementieren muss, aber alloc_excess + dealloc sollte auch funktionieren. Wäre es möglich, alloc eine Standardimplementierung in Bezug auf alloc_excess oder ist dies eine nicht mögliche oder brechende Änderung? In der Praxis werden die meisten Allokatoren die meisten Methoden sowieso implementieren, daher ist dies keine große Sache, sondern eher ein Wunsch.


jemallocator implementiert Alloc zweimal (für Jemalloc und &Jemalloc ), wobei die Implementierung von Jemalloc für einige method gerecht ist Ein (&*self).method(...) , der den Methodenaufruf an die Implementierung von &Jemalloc weiterleitet. Dies bedeutet, dass beide Implementierungen von Alloc für Jemalloc manuell synchronisiert werden müssen. Ob es tragisch sein kann, unterschiedliche Verhaltensweisen für die &/_ -Implementierungen zu erhalten, weiß ich nicht.


Ich fand es sehr schwierig herauszufinden, was die Leute in der Praxis tatsächlich mit dem Merkmal Alloc . Die einzigen Projekte, die ich gefunden habe, die es verwenden, werden sowieso weiterhin jede Nacht verwendet (Servo, Redox) und verwenden es nur, um den globalen Allokator zu ändern. Es macht mir große Sorgen, dass ich kein Projekt finden konnte, das es als Parameter für den Sammlungstyp verwendet (vielleicht hatte ich einfach Pech und es gibt einige?). Ich suchte besonders nach Beispielen für die Implementierung von SmallVec und ArrayVec zu einem Vec -ähnlichen Typ (da std::Vec kein Alloc type Parameter noch) und fragte sich auch, wie das Klonen zwischen diesen Typen ( Vec s mit einem anderen Alloc ator) funktionieren würde (das gleiche gilt wahrscheinlich für das Klonen von Box es mit verschiedenen Alloc s). Gibt es Beispiele dafür, wie diese Implementierungen irgendwo aussehen würden?

Die einzigen Projekte, die ich gefunden habe und die es verwenden, werden sowieso weiterhin jede Nacht verwendet (Servo, Redox).

Für das, was es wert ist, versucht Servo, instabile Funktionen nach Möglichkeit zu entfernen: https://github.com/servo/servo/issues/5286

Dies ist auch ein Henne-Ei-Problem. Viele Projekte verwenden Alloc noch nicht, da es immer noch instabil ist.

Mir ist nicht wirklich klar, warum wir überhaupt eine vollständige Ergänzung der _excess-APIs haben sollten. Sie existierten ursprünglich, um die experimentelle * allocm-API von jemalloc widerzuspiegeln, aber diese wurden vor einigen Jahren in 4.0 entfernt, um nicht ihre gesamte API-Oberfläche zu duplizieren. Es scheint, als könnten wir ihrem Beispiel folgen?

Wäre es möglich, alloc später eine Standardimplementierung in Bezug auf alloc_excess zu geben, oder ist dies eine nicht mögliche oder brechende Änderung?

Wir können eine Standardimplementierung von alloc in Bezug auf alloc_excess hinzufügen, aber alloc_excess muss eine Standardimplementierung in Bezug auf alloc . Alles funktioniert gut, wenn Sie eines oder beide implementieren, aber wenn Sie keines von beiden implementieren, wird Ihr Code kompiliert, aber unendlich rekursiv. Dies ist schon früher aufgetaucht (vielleicht für Rand ?), Und wir könnten sagen, dass Sie mindestens eine dieser Funktionen implementieren müssen, aber es ist uns egal, welche.

Es macht mir große Sorgen, dass ich kein Projekt finden konnte, das es als Parameter für den Sammlungstyp verwendet (vielleicht hatte ich einfach Pech und es gibt einige?).

Ich kenne niemanden, der das tut.

Die einzigen Projekte, die ich gefunden habe und die es verwenden, werden sowieso weiterhin jede Nacht verwendet (Servo, Redox).

Eine große Sache, die dies verhindert, ist, dass stdlib-Sammlungen noch keine parametrischen Allokatoren unterstützen. Dies schließt auch die meisten anderen Kisten so gut wie aus, da die meisten externen Sammlungen interne unter der Haube verwenden ( Box , Vec usw.).

Die einzigen Projekte, die ich gefunden habe und die es verwenden, werden sowieso weiterhin jede Nacht verwendet (Servo, Redox).

Eine große Sache, die dies verhindert, ist, dass stdlib-Sammlungen noch keine parametrischen Allokatoren unterstützen. Dies schließt auch die meisten anderen Kisten so gut wie aus, da die meisten externen Sammlungen interne unter der Haube verwenden (Box, Vec usw.).

Dies gilt für mich - ich habe einen Spielzeugkern, und wenn ich könnte, würde ich Vec<T, A> , aber stattdessen muss ich eine innen veränderbare globale Allokatorfassade haben, die grob ist.

@remexre Wie wird durch die Parametrisierung Ihrer Datenstrukturen ein globaler Zustand mit innerer Veränderlichkeit

Ich nehme an, es wird immer noch einen innerlich veränderlichen globalen Status geben, aber es fühlt sich viel sicherer an, ein Setup zu haben, in dem der globale Allokator unbrauchbar ist, bis der Speicher vollständig zugeordnet ist, als eine globale set_allocator -Funktion.


EDIT : Ich habe gerade festgestellt, dass ich die Frage nicht beantwortet habe. Im Moment habe ich so etwas wie:

struct BumpAllocator{ ... }
struct RealAllocator{ ... }
struct LinkedAllocator<A: 'static + AreaAllocator> {
    head: Mutex<Option<Cons<A>>>,
}
#[global_allocator]
static KERNEL_ALLOCATOR: LinkedAllocator<&'static mut (AreaAllocator + Send + Sync)> =
    LinkedAllocator::new();

Dabei ist AreaAllocator ein Merkmal, mit dem ich (zur Laufzeit) überprüfen kann, ob sich die Allokatoren nicht versehentlich "überlappen" (in Bezug auf die Adressbereiche, denen sie zuordnen). BumpAllocator wird nur sehr früh verwendet, um Speicherplatz zu schaffen, wenn der Rest des Speichers zugeordnet wird, um die RealAllocator zu erstellen.

Idealerweise möchte ich, dass ein Mutex<Option<RealAllocator>> (oder ein Wrapper, der es "nur einfügen" macht) der einzige Allokator ist und dass alles, was früh zugewiesen wird, durch den frühen Start BumpAllocator parametrisiert wird

@sfackler

Mir ist nicht wirklich klar, warum wir überhaupt eine vollständige Ergänzung der _excess-APIs haben sollten. Sie existierten ursprünglich, um die experimentelle * allocm-API von jemalloc widerzuspiegeln, aber diese wurden vor einigen Jahren in 4.0 entfernt, um nicht ihre gesamte API-Oberfläche zu duplizieren. Es scheint, als könnten wir ihrem Beispiel folgen?

Derzeit ruft shrink_in_place xallocx wodurch die tatsächliche Zuordnungsgröße zurückgegeben wird. Da shrink_in_place_excess nicht vorhanden ist, wird diese Größe weggeworfen, und Benutzer müssen nallocx aufrufen, um sie neu zu berechnen. Die Kosten hängen wirklich davon ab, wie groß die Zuordnung ist.

Zumindest einige Jemalloc-Zuweisungsfunktionen, die wir bereits verwenden, geben uns die verwendbare Größe zurück, aber die aktuelle API erlaubt uns nicht, sie zu verwenden.

@remexre

Als ich an meinem Spielzeugkern arbeitete, war es auch mein Ziel, den globalen Allokator zu meiden, um sicherzustellen, dass keine Zuordnung erfolgt, bis ein Allokator eingerichtet wurde. Freut mich zu hören, dass ich nicht derjenige bin!

Ich mag das Wort Heap für den globalen Standardzuweiser nicht. Warum nicht Default ?

Ein weiterer Punkt zur Klarstellung: RFC 1974 bringt all dieses Zeug in std::alloc aber es ist derzeit in std::heap . Welcher Standort wird zur Stabilisierung vorgeschlagen?

@jethrogb "Heap" ist ein ziemlich kanonischer Begriff für "das Ding, auf das Malloc Ihnen

@sfackler

"Das Ding Malloc gibt dir Hinweise auf"

Außer in meinen Augen ist das System .

Ah sicher. Global ist dann vielleicht ein anderer Name? Da Sie #[global_allocator] , um es auszuwählen.

Es kann mehrere Heap-Allokatoren geben (z. B. libc und jemalloc mit Präfix). Wie wäre es, wenn Sie std::heap::Heap in std::heap::Default und #[global_allocator] in #[default_allocator] umbenennen?

Die Tatsache, dass es das ist, was Sie erhalten, wenn Sie nichts anderes angeben (vermutlich, wenn beispielsweise Vec einen zusätzlichen Typparameter / ein zusätzliches Feld für den Allokator erhält), ist wichtiger als die Tatsache, dass es kein "per" hat -instances "state (oder Instanzen wirklich).

Die letzte Kommentierungsfrist ist nun abgeschlossen.

In Bezug auf FCP denke ich, dass die zur Stabilisierung vorgeschlagene API-Teilmenge von sehr begrenztem Nutzen ist. Beispielsweise wird die Kiste jemallocator nicht unterstützt.

Inwiefern? jemallocator muss möglicherweise einige der Implikationen instabiler Methoden hinter einem Feature-Flag markieren, aber das war's.

Wenn jemallocator auf Stable Rust beispielsweise Alloc::realloc durch Aufrufen von je_rallocx implementieren kann, sondern sich auf das Standard-Impl für Zuweisung + Kopie + Dealloc verlassen muss, ist dies kein akzeptabler Ersatz für das alloc_jemalloc Kiste IMO der Standardbibliothek.

Sicher, Sie könnten etwas zum Kompilieren bekommen, aber es ist keine besonders nützliche Sache.

Warum? C ++ hat überhaupt kein Konzept von Realloc in seiner Allocator-API und das scheint die Sprache nicht verkrüppelt zu haben. Es ist offensichtlich nicht ideal, aber ich verstehe nicht, warum es inakzeptabel wäre.

C ++ - Sammlungen verwenden im Allgemeinen kein Realloc, da C ++ - Verschiebungskonstruktoren beliebigen Code ausführen können, da Realloc nicht nützlich ist.

Der Vergleich erfolgt nicht mit C ++, sondern mit der aktuellen Rust-Standardbibliothek mit integrierter Jemalloc-Unterstützung. Das Wechseln zu und aus dem Standardzuweiser mit nur dieser Teilmenge der Alloc API wäre eine Regression.

Und realloc ist ein Beispiel. jemallocator implementiert derzeit auch alloc_zeroed , alloc_excess , usable_size , grow_in_place usw.

alloc_zeroed soll stabilisiert werden. Soweit ich das beurteilen kann (siehe oben), gibt es buchstäblich keine Verwendungen von alloc_excess . Könnten Sie einen Code anzeigen, der sich zurückbildet, wenn dieser auf eine Standardimplementierung zurückgreift?

Generell verstehe ich jedoch nicht, warum dies ein Argument gegen die Stabilisierung eines Teils dieser APIs ist. Wenn Sie jemallocator nicht verwenden möchten, können Sie es weiterhin nicht verwenden.

Könnte Layout::array<T>() zu einer Konstante gemacht werden?

Es kann in Panik geraten, also im Moment nicht.

Es kann in Panik geraten, also im Moment nicht.

Ich verstehe ... Ich würde mich mit const fn Layout::array_elem<T>() zufrieden geben, was einem nicht in Panik geratenen Äquivalent von Layout::<T>::repeat(1).0 .

@mzabaluev Ich denke, was Sie beschreiben, entspricht Layout::new<T>() . Es kann derzeit in Panik geraten, aber das liegt nur daran, dass es mit Layout::from_size_align und dann .unwrap() implementiert wird, und ich gehe davon aus, dass es anders gemacht werden könnte.

@joshlf Ich denke, diese Struktur hat die Größe 5, während diese als Elemente eines Arrays aufgrund der Ausrichtung alle 8 Bytes platziert werden:

struct Foo {
    bar: u32,
    baz: u8
}

Ich bin nicht sicher, ob ein Array von Foo das Auffüllen des letzten Elements für seine Größenberechnung enthalten würde, aber das ist meine starke Erwartung.

In Rust ist die Größe eines Objekts immer ein Vielfaches seiner Ausrichtung, sodass die Adresse des n -ten Elements eines Arrays immer array_base_pointer + n * size_of<T>() . Die Größe eines Objekts in einem Array entspricht also immer der Größe dieses Objekts. Weitere Informationen finden Sie auf der

OK, es stellt sich heraus, dass eine Struktur bis zu ihrer Ausrichtung aufgefüllt ist, aber AFAIK dies ist keine stabile Garantie, außer in #[repr(C)] .
Wie auch immer, es wäre auch willkommen, Layout::new einer Konstante zu machen.

Dies ist das dokumentierte (und damit garantierte) Verhalten einer stabilen Funktion:

https://doc.rust-lang.org/std/mem/fn.size_of.html

Gibt die Größe eines Typs in Bytes zurück.

Insbesondere ist dies der Versatz in Bytes zwischen aufeinanderfolgenden Elementen in einem Array mit diesem Elementtyp einschließlich Ausrichtungsauffüllung. Damit für jede Art T und Länge n , [T; n] hat eine Größe von n * size_of::<T>() .

Vielen Dank. Ich habe gerade festgestellt, dass jede Konstante, die das Ergebnis von Layout::new multipliziert, von Natur aus in Panik geraten würde (es sei denn, es wird mit saturating_mul oder ähnlichem gemacht), also bin ich wieder auf dem ersten Platz. Fahren Sie mit einer Frage zu Panik im Problem der Konstantenverfolgung fort.

Das Makro panic!() wird derzeit in konstanten Ausdrücken nicht unterstützt, aber Paniken aus aktivierter Arithmetik werden vom Compiler generiert und sind von dieser Einschränkung nicht betroffen:

error[E0080]: constant evaluation error
 --> a.rs:1:16
  |
1 | const A: i32 = i32::max_value() * 2;
  |                ^^^^^^^^^^^^^^^^^^^^ attempt to multiply with overflow

error: aborting due to previous error

Dies hängt mit Alloc::realloc aber nicht mit der Stabilisierung der minimalen Schnittstelle ( realloc ist nicht Teil davon):

Derzeit kopiert das Standardimplement von Alloc::realloc tote Vektorelemente (im Bereich [len(), capacity()) ), da Vec::reserve/double RawVec::reserve/double aufruft, die Alloc::realloc aufrufen. . Im absurden Fall eines riesigen leeren Vektors, der capacity() + 1 Elemente einfügen und somit neu zuweisen möchte, sind die Kosten für das Berühren des gesamten Speichers nicht unerheblich.

Wenn die Standardimplementierung von Alloc::realloc theoretisch auch einen Bereich "bytes_used" annehmen würde, könnte sie theoretisch nur den relevanten Teil bei der Neuzuweisung kopieren. In der Praxis überschreibt mindestens jemalloc Alloc::realloc default impl mit einem Aufruf von rallocx . Ob ein alloc / dealloc Tanz, der nur den relevanten Speicher kopiert, schneller oder langsamer ist als ein rallocx -Anruf, hängt wahrscheinlich von vielen Dingen ab (verwaltet rallocx um den Block an Ort und Stelle zu erweitern? Wie viel unnötiger Speicher wird rallocx kopieren? etc.).

https://github.com/QuiltOS/rust/tree/allocator-error Ich habe begonnen zu demonstrieren, wie der zugehörige Fehlertyp meiner Meinung nach unsere Sammlungen und Fehlerbehandlungsprobleme löst, indem ich die Generalisierung selbst durchführe. Beachten Sie insbesondere, wie ich in den Modulen das ändere

  • Verwenden Sie die Implementierung von Result<T, A::Err> für die Implementierung von T
  • Niemals unwrap oder irgendetwas anderes teilweise
  • Nein oom(e) außerhalb von AbortAdapter .

Dies bedeutet, dass die Änderungen, die ich vornehme, ziemlich sicher und auch ziemlich sinnlos sind! Das Arbeiten sowohl mit der Fehlerrückgabe als auch mit der Fehlerbehebung sollte keinen zusätzlichen Aufwand erfordern, um mentale Invarianten aufrechtzuerhalten - die Typprüfung erledigt die gesamte Arbeit.

Ich erinnere mich --- Ich denke in @Gankros RFC? oder der Pre-RFC-Thread --- Gecko / Servo-Leute sagen, es sei schön, dass die Fehlbarkeit von Sammlungen nicht Teil ihres Typs ist. Nun, ich kann #[repr(transparent)] zu AbortAdapter hinzufügen, damit Sammlungen sicher zwischen Foo<T, A> und Foo<T, AbortAdapter<A>> (in sicheren Wrappern) umgewandelt werden können, so dass man sie frei verwenden kann Wechseln Sie hin und her, ohne jede Methode zu duplizieren. [Für die Rückkompatibilität müssen die Standardbibliothekssammlungen auf jeden Fall dupliziert werden, aber die Benutzermethoden müssen nicht so sein, wie es heutzutage recht einfach ist, mit Result<T, !> zu arbeiten.]

Leider wird der Code die Typprüfung nicht vollständig durchführen, da das Ändern der Typparameter eines lang-Elements (Feld) den Compiler verwirrt (Überraschung!). Das ICE-verursachende Box-Commit ist das letzte - alles, bevor es gut ist. @eddyb Rustc in # 47043 behoben!

edit @joshlf Ich wurde über Ihre https://github.com/rust-lang/rust/pull/45272 informiert und habe diese hier aufgenommen. Vielen Dank!

Permanenter Speicher (z. B. http://pmem.io ) ist die nächste große Sache, und Rust muss positioniert werden, um gut damit arbeiten zu können.

Ich habe kürzlich an einem Rust-Wrapper für einen dauerhaften Speicherzuweiser (insbesondere libpmemcto) gearbeitet. Unabhängig davon, welche Entscheidungen bezüglich der Stabilisierung dieser API getroffen werden, muss sie: -

  • Sie können einen performanten Wrapper um einen dauerhaften Speicherzuweiser wie libpmemcto unterstützen.
  • Sie können Sammlungstypen nach Allokator angeben (parametrisieren) (im Moment müssen Sie Box, Rc, Arc usw. duplizieren).
  • Sie können Daten über Allokatoren hinweg klonen
  • Sie können unterstützen, dass persistente Speicherstrukturen mit Feldern gespeichert werden, die bei der Instanziierung eines persistenten Speicherpools neu initialisiert werden. Einige persistente Speicherstrukturen müssen also Felder enthalten, die nur vorübergehend auf dem Heap gespeichert werden. Meine aktuellen Anwendungsfälle beziehen sich auf den persistenten Speicherpool, der für die Zuordnung verwendet wird, und auf vorübergehende Daten, die für Sperren verwendet werden.

Abgesehen davon verwendet die pmem.io-Entwicklung (Intels PMDK) einen modifizierten Jemalloc-Allokator unter dem Deckmantel. Daher erscheint es ratsam, Jemalloc als Beispiel für einen API-Konsumenten zu verwenden.

Wäre es möglich, den Umfang zu reduzieren, um zuerst nur GlobalAllocator abzudecken, bis wir mehr Erfahrung mit der Verwendung von Alloc Atoren in Sammlungen sammeln?

IIUC dies würde bereits die Bedürfnisse von servo erfüllen und es uns ermöglichen, parallel mit der Parametrisierung von Containern zu experimentieren. In Zukunft können wir entweder Sammlungen verschieben, um stattdessen GlobalAllocator verwenden, oder einfach ein pauschales Impl von Alloc für GlobalAllocator hinzufügen, damit diese für alle Sammlungen verwendet werden können.

Gedanken?

@gnzlbg Damit das Attribut #[global_allocator] nützlich ist (über die Auswahl von heap::System ), muss das Merkmal Alloc auch stabil sein, damit es von Kisten wie https: / implementiert werden kann. GlobalAllocator Schlagen Sie eine neue API vor?

Im Moment gibt es keinen Typ oder Merkmal namens GlobalAllocator. Schlagen Sie eine neue API vor?

Was ich vorgeschlagen habe, ist die Umbenennung der "minimalen" API, die @alexcrichton vorgeschlagen hat, um sich hier von Alloc auf GlobalAllocator zu stabilisieren, um nur globale Allokatoren darzustellen, und die Tür offen zu lassen, damit Sammlungen von einem anderen parametrisiert werden können Allokator-Merkmal in der Zukunft (was nicht bedeutet, dass wir sie nicht durch das Merkmal GlobalAllocator parametrisieren können).

IIUC servo derzeit nur den globalen Allokator wechseln können (im Gegensatz dazu, dass einige Sammlungen auch von einem Allokator parametrisiert werden können). Anstatt zu versuchen, eine Lösung zu stabilisieren, die für beide Anwendungsfälle zukunftssicher sein sollte, können wir jetzt möglicherweise nur das globale Allokatorproblem behandeln und später herausfinden, wie Sammlungen von Allokatoren parametrisiert werden können.

Ich weiß nicht, ob das Sinn macht.

Das IIUC-Servo muss derzeit nur den globalen Allokator wechseln können (im Gegensatz dazu, dass einige Sammlungen auch von einem Allokator parametrisiert werden können).

Das ist richtig, aber:

  • Wenn ein Merkmal und seine Methode stabil sind, damit es implementiert werden kann, kann es auch direkt aufgerufen werden, ohne std::heap::Heap durchlaufen. Es ist also nicht nur ein Merkmal globaler Allokator, sondern auch ein Merkmal für Allokatoren (selbst wenn wir am Ende ein anderes Merkmal für generische Sammlungen gegenüber Allokatoren erstellen), und GlobalAllocator ist kein besonders guter Name.
  • Die jemallocator-Kiste implementiert derzeit alloc_excess , realloc , realloc_excess , usable_size , grow_in_place und shrink_in_place die dies nicht sind Teil der vorgeschlagenen minimalen API. Diese können effizienter sein als die Standardimplikation, daher wäre das Entfernen eine Leistungsregression.

Beide Punkte sind sinnvoll. Ich dachte nur, dass die einzige Möglichkeit, die Stabilisierung dieser Funktion erheblich zu beschleunigen, darin besteht, eine Abhängigkeit davon auszuschließen, die auch ein gutes Merkmal für die Parametrisierung von Sammlungen darüber ist.

[Es wäre schön, wenn Servo so wäre (stabile | offizielle Mozilla-Kiste), und Fracht könnte dies erzwingen, um hier ein wenig Druck abzubauen.]

@ Ericson2314 Servo ist nicht das einzige Projekt, das diese APIs verwenden möchte.

@ Ericson2314 Ich verstehe nicht was das bedeutet, könntest du es umformulieren?

Zum Kontext: Servo verwendet derzeit eine Reihe instabiler Funktionen (einschließlich #[global_allocator] ), aber wir versuchen, uns langsam davon zu entfernen (entweder durch Aktualisierung auf einen Compiler, der einige Funktionen stabilisiert hat, oder durch Suche nach stabilen Alternativen. ) Dies wird unter https://github.com/servo/servo/issues/5286 verfolgt. Es wäre also schön, #[global_allocator] zu stabilisieren, aber es blockiert keine Servo-Arbeit.

Firefox stützt sich auf die Tatsache, dass Rust std beim Kompilieren eines cdylib standardmäßig den Systemzuweiser verwendet und dass Mozjemalloc, das schließlich mit derselben Binärdatei verknüpft wird, Symbole wie malloc und free that "shadow" (ich kenne die richtige Linker-Terminologie nicht) die von libc. Zuweisungen aus Rust-Code in Firefox verwenden also Mozjemalloc. (Dies ist unter Unix, ich weiß nicht, wie es unter Windows funktioniert.) Das funktioniert, aber es fühlt sich für mich zerbrechlich an. Firefox verwendet stabiles Rust, und ich möchte, dass #[global_allocator] , um Mozjemalloc explizit auszuwählen, damit das gesamte Setup robuster wird.

@SimonSapin Je mehr ich mit Allokatoren und Sammlungen spiele, desto eher denke ich, dass wir die Sammlungen noch nicht um Alloc parametrisieren möchten, da eine Sammlung je nach Allokator möglicherweise eine anbieten möchte Unterschiedliche APIs, die Komplexität einiger Vorgänge ändern sich, einige Erfassungsdetails hängen tatsächlich vom Allokator ab usw.

Daher möchte ich einen Weg vorschlagen, wie wir hier Fortschritte erzielen können.

Schritt 1: Heap-Allokator

Wir könnten uns zunächst darauf beschränken, dass Benutzer den Allokator für den Heap (oder den Allokator system / platform / global / free-store, oder wie auch immer Sie ihn bevorzugen) in stabilem Rust auswählen.

Das einzige, was wir anfänglich damit parametrisieren, ist Box , das nur Speicher zuweisen ( new ) und freigeben ( drop ) muss.

Dieses Allokator-Merkmal könnte anfänglich die von @alexcrichton vorgeschlagene (oder etwas erweiterte) API haben, und dieses Allokator-Merkmal könnte nachts immer noch eine leicht erweiterte API haben, um die std:: -Sammlungen zu unterstützen.

Sobald wir dort sind, können Benutzer, die auf Stable migrieren möchten, dies tun, können jedoch aufgrund der instabilen API einen Leistungseinbruch erleiden.

Schritt 2: Heap-Allokator ohne Leistungseinbußen

Zu diesem Zeitpunkt können wir die Benutzer neu bewerten, die aufgrund eines Leistungseinbruchs nicht zu Stable wechseln können, und entscheiden, wie diese API erweitert und stabilisiert werden soll.

Schritte 3 bis N: Unterstützung von benutzerdefinierten Allokatoren in std -Sammlungen.

Erstens ist das schwierig, also könnte es niemals passieren, und ich denke, dass es niemals passiert, ist keine schlechte Sache.

Wenn ich eine Sammlung mit einem benutzerdefinierten Allokator parametrisieren möchte, liegt entweder ein Leistungsproblem oder ein Usability-Problem vor.

Wenn ich ein Usability-Problem habe, möchte ich normalerweise eine andere Sammlungs-API, die Funktionen meines benutzerdefinierten Allokators ausnutzt, wie zum Beispiel meine SliceDeque -Kiste. Das Parametrieren einer Sammlung durch einen benutzerdefinierten Allokator hilft mir hier nicht weiter.

Wenn ich ein Leistungsproblem habe, ist es für einen benutzerdefinierten Allokator immer noch sehr schwierig, mir zu helfen. Ich werde Vec in den nächsten Abschnitten betrachten, da es sich um die Sammlung handelt, die ich am häufigsten neu implementiert habe.

Reduzieren Sie die Anzahl der Systemzuordnungsaufrufe (Small Vector Optimization).

Wenn ich einige Elemente innerhalb des Vec -Objekts zuweisen möchte, um die Anzahl der Aufrufe des Systemzuordners zu verringern, verwende ich heute nur SmallVec<[T; M]> . Ein SmallVec ist jedoch kein Vec :

  • Das Verschieben eines Vec ist O (1) in der Anzahl der Elemente, aber das Verschieben eines SmallVec<[T; M]> ist O (N) für N <M und O (1) danach.

  • Zeiger auf die Vec -Elemente werden beim Verschieben ungültig, wenn len() <= M aber nicht anders, dh wenn len() <= M Operationen wie into_iter die Elemente in das verschieben müssen Iterator-Objekt selbst, anstatt nur Zeiger zu nehmen.

Könnten wir Vec über einen Allokator generisch machen, um dies zu unterstützen? Alles ist möglich, aber ich denke, dass die wichtigsten Kosten sind:

  • Dadurch wird die Implementierung von Vec komplexer, was sich auf Benutzer auswirken kann, die diese Funktion nicht verwenden
  • Die Dokumentation von Vec würde komplexer werden, da das Verhalten einiger Operationen vom Allokator abhängen würde.

Ich denke, diese Kosten sind nicht zu vernachlässigen.

Verwenden Sie Zuordnungsmuster

Der Wachstumsfaktor eines Vec ist auf einen bestimmten Allokator zugeschnitten. In std wir es auf die gängigen jemalloc / malloc / ... zuschneiden. Wenn Sie jedoch einen benutzerdefinierten Allokator verwenden, ist der Wachstumsfaktor wahrscheinlich der von uns gewählte Standardmäßig ist dies nicht das Beste für Ihren Anwendungsfall. Sollte jeder Allokator in der Lage sein, einen Wachstumsfaktor für vec-ähnliche Allokationsmuster anzugeben? Ich weiß es nicht, aber mein Bauchgefühl sagt mir: wahrscheinlich nicht.

Nutzen Sie die zusätzlichen Funktionen Ihres Systemzuordners

Beispielsweise ist in den meisten Tier 1- und Tier 2-Zielen ein übermäßig festgeschriebener Allokator verfügbar. In Linux-ähnlichen und Macos-Systemen wird der Heap-Allokator standardmäßig überlastet, während die Windows-API VirtualAlloc verfügbar macht, mit dem Speicher reserviert werden kann (z. B. auf Vec::reserve/with_capacity ) und Speicher auf push .

Derzeit bietet das Merkmal Alloc keine Möglichkeit, einen solchen Allokator unter Windows zu implementieren, da es die Konzepte des Festschreibens und Reservierens von Speicher nicht voneinander trennt (unter Linux kann ein nicht zu festschreibender Allokator gehackt werden durch einmaliges Berühren jeder Seite). Es gibt auch keine Möglichkeit für einen Allokator, anzugeben, ob er standardmäßig zu viel Commits für alloc festlegt oder nicht.

Das heißt, wir müssten die Alloc API erweitern, um dies für Vec , und das wäre IMO für wenig Gewinn. Denn wenn Sie einen solchen Allokator haben, ändert sich die Semantik von Vec erneut:

  • Vec muss nie wieder wachsen, daher machen Operationen wie reserve wenig Sinn
  • push ist O(1) anstelle von amortisierten O(1) .
  • Iteratoren für lebende Objekte werden niemals ungültig, solange das Objekt aktiv ist, was einige Optimierungen ermöglicht

Nutzen Sie weitere zusätzliche Funktionen Ihres Systemzuordners

Einige System-Alloktoren wie cudaMalloc / cudaMemcpy / ... unterscheiden zwischen fixiertem und nicht fixiertem Speicher. Sie können Speicher in disjunkten Adressräumen zuweisen (daher benötigen wir einen zugeordneten Zeigertyp in der Zuordnungsmerkmal), ...

Die Verwendung dieser für Sammlungen wie Vec ändert jedoch erneut die Semantik einiger Operationen auf subtile Weise, z. B. ob die Indizierung eines Vektors plötzlich undefiniertes Verhalten hervorruft oder nicht, je nachdem, ob Sie dies von einem GPU-Kernel oder vom Host aus tun.

Einpacken

Ich denke, dass es schwierig, wahrscheinlich zu schwierig ist, eine Alloc API zu entwickeln, mit der alle Sammlungen (oder nur Vec ) parametrisiert werden können.

Vielleicht können wir die Sammlungen überdenken, nachdem wir die globalen / system / platform / heap / free-store-Allokatoren richtig eingestellt haben und Box . Vielleicht können wir Alloc wiederverwenden, vielleicht brauchen wir ein VecAlloc, VecDequeAlloc , HashMapAlloc`, ... oder vielleicht sagen wir einfach: "Weißt du was, wenn du das wirklich brauchst Kopieren Sie einfach die Standardsammlung in eine Kiste und formen Sie sie an Ihren Allokator. " Vielleicht ist die beste Lösung, dies einfach zu vereinfachen, indem Sie Standard-Sammlungen in einer eigenen Kiste (oder Kisten) im Kinderzimmer haben und nur stabile Funktionen verwenden, die möglicherweise als eine Reihe von Bausteinen implementiert sind.

Wie auch immer, ich denke, es ist zu schwierig, all diese Probleme hier auf einmal anzugehen und ein Merkmal von Alloc , das für alles gut ist. Wir sind bei Schritt 0. Ich denke, der beste Weg, um schnell zu Schritt 1 und Schritt 2 zu gelangen, besteht darin, Sammlungen aus dem Bild zu lassen, bis wir dort sind.

Sobald wir dort sind, können Benutzer, die auf Stable migrieren möchten, dies tun, können jedoch aufgrund der instabilen API einen Leistungseinbruch erleiden.

Bei der Auswahl eines benutzerdefinierten Allokators geht es normalerweise darum, die Leistung zu verbessern. Daher weiß ich nicht, wem diese anfängliche Stabilisierung dienen würde.

Bei der Auswahl eines benutzerdefinierten Allokators geht es normalerweise darum, die Leistung zu verbessern. Daher weiß ich nicht, wem diese anfängliche Stabilisierung dienen würde.

Jeder? Zumindest jetzt. Die meisten der von Ihnen beanstandeten Methoden fehlen im ursprünglichen Stabilisierungsvorschlag (z. B. alloc_excess ). AFAIK wird von nichts in der Standardbibliothek verwendet. Oder hat sich das kürzlich geändert?

Vec (und andere Benutzer von RawVec ) verwenden realloc in push

@ SimonSapin

Die jemallocator-Kiste implementiert derzeit alloc_excess, realloc, realloc_excess, usable_size, grow_in_place und shrink_in_place

Von diesen Methoden werden AFAIK realloc , grow_in_place und shrink_in_place verwendet, aber grow_in_place ist nur ein naiver Wrapper über shrink_in_place für jemalloc bei Zumindest, wenn wir das instabile Standardimplement von grow_in_place in Bezug auf shrink_in_place in der Eigenschaft Alloc implementiert haben, reduziert sich dies auf zwei Methoden: realloc und shrink_in_place .

Bei der Auswahl eines benutzerdefinierten Allokators geht es normalerweise darum, die Leistung zu verbessern.

Dies ist zwar richtig, aber Sie können möglicherweise mehr Leistung erzielen, wenn Sie einen geeigneteren Allokator ohne diese Methoden verwenden als einen schlechten Allokator, der über diese Methoden verfügt.

IIUC Der Hauptanwendungsfall für Servo war die Verwendung von Firefox jemalloc anstelle eines zweiten jemalloc. War das richtig?

Selbst wenn wir bei einer anfänglichen Stabilisierung realloc und shrink_in_place zu dem Merkmal Alloc hinzufügen, würde dies die Leistungsbeschwerden nur verzögern.

In dem Moment, in dem wir dem Merkmal Alloc eine instabile API hinzufügen, die letztendlich von den Sammlungen std , können Sie auf Stable nicht die gleiche Leistung erzielen wie auf Stable in der Lage sein, jeden Abend einzusteigen. Das heißt, wenn wir realloc_excess und shrink_in_place_excess zum Zuweisungsmerkmal hinzufügen und Vec / String / ... verwenden, haben wir realloc stabilisiert. shrink_in_place hätten dir kein bisschen geholfen.

IIUC Der Hauptanwendungsfall für Servo war die Verwendung von Firefox jemalloc anstelle eines zweiten jemalloc. War das richtig?

Obwohl sie Code gemeinsam nutzen, sind Firefox und Servo zwei separate Projekte / Anwendungen.

Firefox verwendet Mozjemalloc, eine Abzweigung einer alten Version von Jemalloc, mit einer Reihe von zusätzlichen Funktionen. Ich denke, dass ein unsafe FFI-Code für die Richtigkeit und Solidität von Mozjemalloc abhängt, das von Rust std verwendet wird.

Servo verwendet jemalloc, das derzeit Rusts Standard für ausführbare Dateien ist. Es ist jedoch geplant, diesen Standard auf den Allokator des Systems zu ändern. Servo verfügt außerdem über einen Berichtscode für die Speicherauslastung von unsafe , der sich auf die Verwendung von Jemalloc stützt. (Übergeben von Vec::as_ptr() an je_malloc_usable_size .)

Servo verwendet jemalloc, das derzeit Rusts Standard für ausführbare Dateien ist. Es ist jedoch geplant, diesen Standard auf den Allokator des Systems zu ändern.

Es wäre gut zu wissen, ob die Systemzuordnungen in den Systemen, für die Servo-Ziele optimierte APIs für realloc und shrink_to_fit bereitstellen, wie dies bei jemalloc der Fall ist. realloc (und calloc ) sind sehr häufig, aber shrink_to_fit ( xallocx ) ist AFAIK-spezifisch für jemalloc . Vielleicht wäre die beste Lösung, realloc und alloc_zeroed ( calloc ) in der ersten Implementierung zu stabilisieren und shrink_to_fit für später zu belassen. Dies sollte es Servo ermöglichen, auf den meisten Plattformen ohne Leistungsprobleme mit Systemzuordnungen zu arbeiten.

Servo verfügt auch über einen unsicheren Berichtscode für die Speichernutzung, der sich auf die Verwendung von jemalloc stützt. (Übergabe von Vec :: as_ptr () an je_malloc_usable_size.)

Wie Sie wissen, verfügt die jemallocator -Kiste über APIs dafür. Ich gehe davon aus, dass Kisten, die der jemallocator -Kiste ähneln, für andere Allokatoren auftauchen, die ähnliche APIs anbieten, wenn sich die globale Allokator-Story zu stabilisieren beginnt. Ich habe nicht darüber nachgedacht, ob diese APIs überhaupt zum Merkmal Alloc .

Ich denke nicht, dass malloc_usable_size im Merkmal Alloc . Die Verwendung von #[global_allocator] , um sicher zu sein, welcher Allokator von Vec<T> und die separate Verwendung einer Funktion aus der jemallocator -Kiste ist in Ordnung.

@SimonSapin Sobald das Merkmal Alloc stabil ist, haben wir wahrscheinlich eine Kiste wie jemallocator für Linux Malloc und Windows. Diese Kisten könnten eine zusätzliche Funktion haben, um die Teile der instabilen Alloc API (wie z. B. usable_size zu malloc_usable_size ) und einige andere Dinge zu implementieren sind nicht Teil der Alloc API, wie Speicherberichte zusätzlich zu mallinfo . Sobald es verwendbare Kisten für die Systeme gibt, auf die Servos abzielen, ist es einfacher zu wissen, welche Teile des Merkmals Alloc für die Stabilisierung priorisiert werden müssen, und wir werden wahrscheinlich neuere APIs finden, mit denen zumindest für einige experimentiert werden sollte Allokatoren.

@gnzlbg Ich bin etwas skeptisch gegenüber den Dingen in https://github.com/rust-lang/rust/issues/32838#issuecomment -358267292. Wenn man all diese systemspezifischen Dinge weglässt, ist es nicht schwer, Sammlungen für die Zuordnung zu verallgemeinern - ich habe es geschafft. Der Versuch, dies zu berücksichtigen, scheint eine separate Herausforderung zu sein.

@SimonSapin Hat Firefox keine instabile Rust-Richtlinie? Ich glaube, ich war verwirrt: Firefox und Servo wollen das, aber wenn ja, würde der Anwendungsfall von Firefox den Druck zur Stabilisierung erhöhen.

@sfackler Siehe das ^. Ich habe versucht, zwischen Projekten zu unterscheiden, die diesen Stall brauchen oder wollen, aber Servo ist auf der anderen Seite dieser Kluft.

Ich habe ein Projekt, das dies will und erfordert, dass es stabil ist. Servo oder Firefox als Konsumenten von Rust haben nichts besonders Magisches.

@ Ericson2314 Richtig, Firefox verwendet Stable: https://wiki.mozilla.org/Rust_Update_Policy_for_Firefox. Wie ich bereits erklärt habe, gibt es heute eine funktionierende Lösung. Dies ist also kein wirklicher Blocker für irgendetwas. Es wäre schöner / robuster, #[global_allocator] , das ist alles.

Servo verwendet einige instabile Funktionen, aber wie bereits erwähnt, versuchen wir dies zu ändern.

FWIW, parametrische Allokatoren sind sehr nützlich, um Allokatoren zu implementieren. Ein Großteil der weniger leistungsempfindlichen Buchhaltung wird viel einfacher, wenn Sie verschiedene Datenstrukturen intern verwenden und durch einen einfacheren Allokator (wie

@ Ericson2314

Wenn man all diese systemspezifischen Dinge weglässt, ist es nicht schwer, Sammlungen für die Zuordnung zu verallgemeinern - ich habe es geschafft. Der Versuch, dies zu berücksichtigen, scheint eine separate Herausforderung zu sein.

Haben Sie eine Implementierung von ArrayVec oder SmallVec zu Vec + benutzerdefinierten Allokatoren, die ich mir ansehen könnte? Das war der erste Punkt, den ich erwähnte, und das ist überhaupt nicht systemspezifisch. Dies wären wahrscheinlich die einfachsten zwei Allokatoren, die man sich vorstellen kann. Einer ist nur ein unformatiertes Array als Speicher, und der andere kann auf dem ersten aufgebaut werden, indem dem Heap ein Fallback hinzugefügt wird, sobald das Array keine Kapazität mehr hat. Der Hauptunterschied besteht darin, dass diese Allokatoren nicht "global" sind, aber jeder der Vec einen eigenen Allokator hat, der von allen anderen unabhängig ist, und diese Allokatoren sind zustandsbehaftet.

Ich argumentiere auch nicht, dies niemals zu tun. Ich behaupte nur, dass dies sehr schwierig ist: C ++ versucht es seit 30 Jahren mit nur teilweisem Erfolg: GPU-Allokatoren und GC-Allokatoren funktionieren aufgrund der generischen Zeigertypen, implementieren jedoch ArrayVec und SmallVec zu Vec führt nicht zu einer kostengünstigen Abstraktion in C ++ - Land ( P0843r1 erläutert einige der Probleme für ArrayVec im Detail).

Ich würde es nur vorziehen, wenn wir dies weiterverfolgen würden, nachdem wir die Teile stabilisiert haben, die etwas Nützliches liefern, solange dies nicht dazu führt, dass in Zukunft benutzerdefinierte Sammlungszuordnungen verfolgt werden.


Ich habe ein wenig mit @SimonSapin im IRC gesprochen und wenn wir den anfänglichen Stabilisierungsvorschlag mit realloc und alloc_zeroed , könnte Rust in Firefox (das nur stabiles Rust verwendet) verwendet werden mozjemalloc als globaler Allokator in stabilem Rust ohne zusätzliche Hacks. Wie von @SimonSapin erwähnt , hat Firefox heute eine praktikable Lösung dafür.

Trotzdem könnten wir dort anfangen und, sobald wir dort sind, servo auf stabile #[global_allocator] ohne Leistungsverlust verschieben.


@ Joshlf

FWIW, parametrische Allokatoren sind sehr nützlich, um Allokatoren zu implementieren.

Könnten Sie etwas näher darauf eingehen, was Sie meinen? Gibt es einen Grund, warum Sie Ihre benutzerdefinierten Allokatoren nicht mit dem Merkmal Alloc parametrisieren können? Oder Ihr eigenes benutzerdefiniertes Allokatormerkmal und implementieren Sie einfach das Merkmal Alloc auf den endgültigen Allokatoren (diese beiden Merkmale müssen nicht unbedingt gleich sein)?

Ich verstehe nicht, woher der Anwendungsfall von "SmallVec = Vec + Spezialzuweiser" kommt. Es ist nicht etwas, was ich schon oft erwähnt habe (weder in Rust noch in anderen Kontexten), gerade weil es viele ernsthafte Probleme hat. Wenn ich daran denke, "die Leistung mit einem spezialisierten Allokator zu verbessern", denke ich überhaupt nicht daran.

Beim Blick auf die Layout API habe ich mich über die Unterschiede in der Fehlerbehandlung zwischen from_size_align und align_to , wobei die erstere im Fehlerfall None zurückgibt , während letztere in Panik geraten (!).

Wäre es nicht hilfreicher und konsistenter, eine entsprechend definierte und informative LayoutErr -Aufzählung hinzuzufügen und in beiden Fällen eine Result<Layout, LayoutErr> (und sie möglicherweise für die anderen Funktionen zu verwenden, die derzeit eine Option auch)?

@rkruppe

Ich verstehe nicht, woher der Anwendungsfall von "SmallVec = Vec + Spezialzuweiser" kommt. Es ist nicht etwas, was ich schon oft erwähnt habe (weder in Rust noch in anderen Kontexten), gerade weil es viele ernsthafte Probleme hat. Wenn ich daran denke, "die Leistung mit einem spezialisierten Allokator zu verbessern", denke ich überhaupt nicht daran.

Es gibt zwei unabhängige Möglichkeiten, Allokatoren in Rust und C ++ zu verwenden: den Systemallokator, der standardmäßig von allen Allokationen verwendet wird, und als Typargument für eine Sammlung, die durch ein Allokatormerkmal parametrisiert wird, um ein Objekt dieser bestimmten Sammlung zu erstellen verwendet einen bestimmten Allokator (der der Allokator des Systems sein kann oder nicht).

Das Folgende konzentriert sich nur auf diesen zweiten Anwendungsfall: Verwenden einer Sammlung und eines Zuordnungstyps zum Erstellen eines Objekts dieser Sammlung, das einen bestimmten Zuweiser verwendet.

Nach meiner Erfahrung mit C ++ dient die Parametrisierung einer Sammlung mit einem Allokator zwei Anwendungsfällen:

  • Verbessern Sie die Leistung eines Sammlungsobjekts, indem Sie dafür sorgen, dass die Sammlung einen benutzerdefinierten Zuweiser verwendet, der auf ein bestimmtes Zuordnungsmuster ausgerichtet ist, und / oder
  • Fügen Sie einer Sammlung eine neue Funktion hinzu, die es ihr ermöglicht, etwas zu tun, was sie vorher nicht konnte.

Hinzufügen neuer Funktionen zu Sammlungen

Dies ist der Anwendungsfall von Allokatoren, die ich in 99% der Fälle in C ++ - Codebasen sehe. Die Tatsache, dass das Hinzufügen einer neuen Funktion zu einer Sammlung die Leistung verbessert, ist meiner Meinung nach zufällig. Insbesondere verbessert keiner der folgenden Allokatoren die Leistung, indem er auf ein Allokationsmuster abzielt. Dazu fügen sie Funktionen hinzu, die in einigen Fällen, wie @ Ericson2314 erwähnt, als "systemspezifisch" angesehen werden können. Dies sind einige Beispiele:

  • Stapelzuweiser für kleine Pufferoptimierungen (siehe Howard Hinnants Papier " std::vector oder flat_{map,set,multimap,...} und indem Sie ihm einen benutzerdefinierten Allokator übergeben, fügen Sie eine kleine Pufferoptimierung mit ( SmallVec ) oder ohne ( ArrayVec ) hinzu. Haufen fallen zurück. Dies ermöglicht beispielsweise das Ablegen einer Sammlung mit ihren Elementen auf dem Stapel oder im statischen Speicher (wo sie sonst den Heap verwendet hätte).

  • segmentierte Speicherarchitekturen (wie 16-Bit-Zeiger-x86-Ziele und GPGPUs). Beispielsweise war C ++ 17 Parallel STL während C ++ 14 die parallele technische Spezifikation. Die Vorläuferbibliothek desselben Autors ist die Thrust-Bibliothek von NVIDIA, die Allokatoren enthält, mit denen Containerklassen den GPGPU-Speicher (z. B. push :: device_malloc_allocator ) oder den push :: pinned_allocator ) verwenden können. Der angeheftete Speicher ermöglicht eine schnellere Übertragung zwischen Host und Gerät manche Fälle).

  • Allokatoren zur Lösung von Parallelitätsproblemen wie falschem Teilen (z. B. Intel Thread Building Blocks cache_aligned_allocator ) oder Überausrichtungsanforderungen von SIMD-Typen (z. B. Eigen3s aligned_allocator ).

  • Gemeinsamer Interprozess-Speicher: Boost.Interprocess verfügt über

  • Speicherbereinigung: Die Bibliothek für die

  • instrumentierte Allokatoren: Mit dem blsma_testallocator der Bloomberg Software Library können Sie Speicherzuweisungs- / Freigabemuster (und C ++ - spezifische Objektkonstruktionen / -zerstörungen) der Objekte protokollieren, in denen Sie sie verwenden. Sie wissen nicht, ob ein Vec nach reserve zugewiesen wird? Schließen Sie einen solchen Allokator an, und er wird Ihnen sagen, ob es passiert. Bei einigen dieser Allokatoren können Sie sie benennen, sodass Sie sie für mehrere Objekte verwenden und Protokolle abrufen können, in denen angegeben ist, welches Objekt was tut.

Dies sind die Arten von Allokatoren, die ich in C ++ am häufigsten in freier Wildbahn sehe. Wie ich bereits erwähnt habe, ist die Tatsache, dass sie in einigen Fällen die Leistung verbessern, meiner Meinung nach zufällig. Der wichtige Teil ist, dass keiner von ihnen versucht, auf ein bestimmtes Zuordnungsmuster abzuzielen.

Targeting von Zuordnungsmustern zur Verbesserung der Leistung.

AFAIK, es gibt keine weit verbreiteten C ++ - Allokatoren, die dies tun, und ich werde gleich erklären, warum ich denke, dass dies der Fall ist. Die folgenden Bibliotheken zielen auf diesen Anwendungsfall ab:

Diese Bibliotheken bieten jedoch nicht wirklich einen einzigen Allokator für einen bestimmten Anwendungsfall. Stattdessen stellen sie Allokatorbausteine ​​bereit, mit denen Sie benutzerdefinierte Allokatoren erstellen können, die auf das bestimmte Zuordnungsmuster in einem bestimmten Teil einer Anwendung ausgerichtet sind.

Der allgemeine Rat, an den ich mich aus meinen C ++ - Tagen erinnere, war, sie einfach nicht zu verwenden (sie sind das allerletzte Mittel), weil:

  • Es ist sehr schwer, die Leistung des Systemzuordners zu erreichen.
  • Die Wahrscheinlichkeit, dass das Zuordnungsmuster des Anwendungsspeichers einer anderen Person mit Ihrem übereinstimmt, ist gering. Sie müssen also wirklich Ihr Zuordnungsmuster kennen und wissen, welche Zuordnungsbausteine ​​Sie benötigen, um es abzugleichen
  • Sie sind nicht portierbar, da verschiedene Anbieter unterschiedliche Implementierungen von C ++ - Standardbibliotheken haben, die unterschiedliche Zuordnungsmuster verwenden. Anbieter richten ihre Implementierung in der Regel an ihre Systemzuordnungen. Das heißt, eine auf einen Anbieter zugeschnittene Lösung kann bei einem anderen Anbieter eine schreckliche Leistung erbringen (schlechter als der Systemzuweiser).
  • Es gibt viele Alternativen, die man ausschöpfen kann, bevor man versucht, diese zu verwenden: Verwenden einer anderen Sammlung, Reservieren von Speicher, ... Die meisten Alternativen sind weniger aufwändig und können größere Gewinne erzielen.

Dies bedeutet nicht, dass Bibliotheken für diesen Anwendungsfall nicht nützlich sind. Sie sind es, weshalb Bibliotheken wie Foonathan / Memory blühen. Aber zumindest meiner Erfahrung nach werden sie in der Natur viel weniger verwendet als Allokatoren, die "zusätzliche Funktionen hinzufügen", denn um einen Gewinn zu erzielen, müssen Sie den System-Allokator schlagen, was mehr Zeit erfordert, als die meisten Benutzer bereit sind zu investieren (Stackoverflow ist voll) von Fragen vom Typ "Ich habe Boost.Pool verwendet und meine Leistung wurde schlechter. Was kann ich tun? Boost.Pool nicht verwenden.").

Einpacken

IMO Ich finde es großartig, dass das C ++ - Allokatormodell, obwohl alles andere als perfekt, beide Anwendungsfälle unterstützt, und ich denke, wenn Rusts Standardkollektionen von Allokatoren parametrisiert werden sollen, sollten sie auch beide Anwendungsfälle unterstützen, zumindest in C ++ - Allokatoren für beide Fälle haben sich als nützlich erwiesen.

Ich denke nur, dass dieses Problem etwas orthogonal zu der Möglichkeit ist, den globalen / system / platform / default / heap / free-store-Allokator einer bestimmten Anwendung anzupassen, und dass der Versuch, beide Probleme gleichzeitig zu lösen, die Lösung für eines verzögern könnte von ihnen unnötig.

Was einige Benutzer mit einer von einem Allokator parametrisierten Sammlung tun möchten, unterscheidet sich möglicherweise erheblich von dem, was andere Benutzer tun möchten. Wenn @rkruppe mit "übereinstimmenden Zuordnungsmustern" beginnt und ich mit "Verhindern von falschem Teilen" oder "Verwenden einer kleinen Pufferoptimierung mit Heap-Fallback" beginne, wird es nur schwierig sein, erstens die gegenseitigen Anforderungen zu verstehen und zweitens anzukommen bei einer Lösung, die für beide funktioniert.

@gnzlbg Danke für das umfassende Schreiben. Das meiste davon geht nicht auf meine ursprüngliche Frage ein und ich bin mit einigen nicht einverstanden, aber es ist gut, wenn es buchstabiert wird, damit wir nicht aneinander vorbei reden.

Meine Frage betraf speziell diese Anwendung:

Stapelzuweiser für kleine Pufferoptimierungen (siehe Howard Hinnants Papier "stack_alloc"). Mit ihnen können Sie std :: vector oder flat_ {map, set, multimap, ...} verwenden und durch Übergeben eines benutzerdefinierten Allokators, den Sie in einer kleinen Pufferoptimierung mit (SmallVec) oder ohne (ArrayVec) Heap-Fallback hinzufügen, zurückgreifen. Dies ermöglicht beispielsweise das Ablegen einer Sammlung mit ihren Elementen auf dem Stapel oder im statischen Speicher (wo sie sonst den Heap verwendet hätte).

Wenn ich über stack_alloc lese, wird mir jetzt klar, wie das funktionieren kann. Es ist nicht das, was die Leute normalerweise mit SmallVec meinen (wo der Puffer inline in der Sammlung gespeichert ist), weshalb ich diese Option verpasst habe, aber es umgeht das Problem, Zeiger aktualisieren zu müssen, wenn die Sammlung verschoben wird (und macht diese Bewegungen auch billiger ). Beachten Sie auch, dass mit short_alloc mehrere Sammlungen ein arena , was es noch ähnlicher macht als typische SmallVec-Typen. Es ist eher wie ein linearer / Bump-Pointer-Allokator mit einem eleganten Fallback auf die Heap-Allokation, wenn nicht genügend Speicherplatz zur Verfügung steht.

Ich bin nicht der Meinung, dass diese Art von Allokator und cache_aligned_allocator grundsätzlich neue Funktionen hinzufügen. Sie sind unterschiedlich und abhängig von Ihrer Definition von „Zuordnungsmustern“ können sie nicht optimize für ein bestimmtes Zuordnungsmuster verwendet. Sie sind jedoch sicherlich für bestimmte Anwendungsfälle optimiert und weisen keine signifikanten Verhaltensunterschiede zu Allzweck-Heap-Allokatoren auf.

Ich stimme jedoch zu, dass Anwendungsfälle wie Sutters verzögerte Speicherzuweisung, die die Bedeutung eines "Zeigers" erheblich ändern, eine separate Anwendung sind, die möglicherweise ein separates Design benötigt, wenn wir sie überhaupt unterstützen möchten.

Wenn ich über stack_alloc lese, wird mir jetzt klar, wie das funktionieren kann. Es ist nicht das, was die Leute normalerweise mit SmallVec meinen (wo der Puffer inline in der Sammlung gespeichert ist), weshalb ich diese Option verpasst habe, aber es umgeht das Problem, Zeiger aktualisieren zu müssen, wenn die Sammlung verschoben wird (und macht diese Bewegungen auch billiger ).

Ich habe stack_alloc weil es der einzige solche Allokator mit "einem Papier" ist, aber es wurde 2009 veröffentlicht und geht C ++ 11 voraus (C ++ 03 unterstützte keine Stateful Allocators in Sammlungen).

Die Funktionsweise in C ++ 11 (das Stateful Allocators unterstützt) ist auf den Punkt gebracht:

  • std :: vector speichert ein Allocator Objekt im Inneren wie Rust RawVec tut .
  • Die Allocator-Schnittstelle verfügt über eine obskure Eigenschaft namens Allocator :: propagate_on_container_move_assignment (ab sofort POCMA), die von benutzerdefinierten Allokatoren angepasst werden kann. Diese Eigenschaft ist standardmäßig true . Wenn diese Eigenschaft false ist, kann der Allokator bei der Verschiebungszuweisung nicht weitergegeben werden, sodass der Standard eine Sammlung benötigt, um jedes seiner Elemente manuell in den neuen Speicher zu verschieben.

Wenn also ein Vektor mit dem Systemzuweiser verschoben wird, wird zuerst der Speicher für den neuen Vektor auf dem Stapel zugewiesen, dann wird der Zuweiser verschoben (der die Größe Null hat), und dann werden die 3 Zeiger verschoben, die noch gültig sind. Solche Bewegungen sind O(1) .

OTOHO, wenn ein Vektor mit einem POCMA == true Allokator verschoben wird, wird zuerst der Speicher für den neuen Vektor auf dem Stapel zugewiesen und mit einem leeren Vektor initialisiert, dann wird die alte Sammlung drain in die neue, so dass die alte leer und die neue voll ist. Dadurch wird jedes Element der Sammlung mithilfe seiner Verschiebungszuweisungsoperatoren einzeln verschoben. Dieser Schritt ist O(N) und korrigiert interne Zeiger der Elemente. Schließlich wird die ursprüngliche, jetzt leere Sammlung gelöscht. Beachten Sie, dass dies wie ein Klon aussieht, jedoch nicht, weil die Elemente selbst nicht geklont, sondern in C ++ verschoben wurden.

Ist das sinnvoll?

Das Hauptproblem bei diesem Ansatz in C ++ sind:

  • Die Vektorwachstumspolitik ist implementierungsdefiniert
  • Die Allokator-API verfügt nicht über _excess -Methoden
  • Die Kombination der beiden oben genannten Probleme bedeutet, dass Sie, wenn Sie wissen, dass Ihr Vektor höchstens 9 Elemente enthalten kann, keinen Stapelzuweiser haben können, der 9 Elemente enthalten kann, da Ihr Vektor möglicherweise versuchen kann, zu wachsen, wenn er 8 mit einem Wachstumsfaktor enthält von 1,5, also müssen Sie pessimieren und Platz für 18 Elemente zuweisen.
  • Die Komplexität der Vektoroperation ändert sich in Abhängigkeit von den Allokatoreigenschaften (POCMA ist nur eine von vielen Eigenschaften, über die die C ++ - Allokator-API verfügt; das Schreiben von C ++ - Allokatoren ist nicht trivial). Dies macht das Festlegen der API des Vektors zu einem Problem, da das Kopieren oder Verschieben von Elementen zwischen verschiedenen Allokatoren desselben Typs manchmal zusätzliche Kosten verursacht, die die Komplexität der Operationen ändern. Es macht auch das Lesen der Spezifikation zu einem großen Schmerz. Viele Online-Dokumentationsquellen wie cppreference stellen den allgemeinen Fall in den Vordergrund und die obskuren Details darüber, was sich ändert, wenn eine Allokator-Eigenschaft in winzigen Kleinbuchstaben wahr oder falsch ist, um 99% der Benutzer nicht damit zu belästigen.

Es gibt viele Leute, die daran arbeiten, die Allokator-API von C ++ zu verbessern, um diese Probleme zu beheben, indem sie beispielsweise _excess -Methoden hinzufügen und sicherstellen, dass standardkonforme Sammlungen diese verwenden.

Ich bin nicht der Meinung, dass diese Art von Allokator und cache_aligned_allocator grundlegend neue Funktionen hinzufügen.

Vielleicht habe ich damit gemeint, dass Sie damit Standard-Sammlungen in Situationen oder für Typen verwenden können, für die Sie sie vorher nicht verwenden konnten. In C ++ können Sie beispielsweise die Elemente eines Vektors nicht ohne einen Stapelzuweiser in das statische Speichersegment Ihrer Binärdatei einfügen (Sie können jedoch eine eigene Sammlung schreiben, die dies tut). OTOH, der C ++ - Standard unterstützt keine überausgerichteten Typen wie SIMD-Typen. Wenn Sie versuchen, einen mit new zuzuordnen, rufen Sie undefiniertes Verhalten auf (Sie müssen posix_memalign oder ähnliches verwenden). . Die Verwendung des Objekts manifestiert das undefinierte Verhalten normalerweise über einen Segfault (*). Mit Dingen wie aligned_allocator können Sie diese Typen häufen zuweisen und sie sogar in Standardauflistungen einfügen, ohne undefiniertes Verhalten aufzurufen, indem Sie einen anderen Zuweiser verwenden. Sicher, der neue Allokator hat unterschiedliche Zuordnungsmuster (diese Allokatoren richten im Grunde genommen den gesamten Speicher über ...), aber die Leute verwenden sie, um etwas tun zu können, was sie vorher nicht konnten.

Offensichtlich ist Rust nicht C ++. Und C ++ hat Probleme, die Rust nicht hat (und umgekehrt). Ein Allokator, der eine neue Funktion in C ++ hinzufügt, ist in Rust möglicherweise nicht erforderlich, da beispielsweise bei SIMD-Typen keine Probleme auftreten.

(*) Benutzer von Eigen3 leiden stark darunter, denn um undefiniertes Verhalten bei der Verwendung von C ++ - und STL-Containern zu vermeiden, müssen Sie die Container gegen SIMD-Typen oder Typen mit SIMD-Typen ( Eigen3-Dokumente ) schützen und sich selbst new für Ihre Typen, indem Sie den Operator new für sie überladen ( mehr Eigen3-Dokumente ).

@gnzlbg danke, ich war auch durch das smallvec exmaple verwirrt. Das würde unbewegliche Typen und eine Art Allokation in Rust erfordern - zwei RFCs im Rückblick und dann weitere Folgearbeiten -, also habe ich vorerst keine Bedenken, darüber nachzudenken. Die bestehende Strategie von smallvec, immer den gesamten benötigten Stapelspeicherplatz zu nutzen, scheint vorerst in Ordnung zu sein.

Ich stimme auch @rkruppe zu, dass in Ihrer überarbeiteten Liste die neuen Funktionen des Allokator verwendet , nicht bekannt sein müssen. Manchmal hat das vollständige Collection<Allocator> neue Eigenschaften (die beispielsweise vollständig im angehefteten Speicher vorhanden sind), aber das ist nur eine natürliche Folge der Verwendung des Allokators.

Die einzige Ausnahme, die ich hier sehe, sind Zuweiser, die nur eine einzige Größe / einen einzigen Typ zuweisen (die NVidia-Zuweiser tun dies ebenso wie die Plattenzuweiser). Wir könnten ein separates Merkmal ObjAlloc<T> , das für normale Allokatoren pauschal implementiert ist: impl<A: Alloc, T> ObjAlloc<T> for A . Dann würden Sammlungen ObjAlloc-Grenzen verwenden, wenn sie nur einige Elemente zuweisen müssten. Aber ich fühle mich etwas albern, wenn ich das anspreche, da es später rückwärts kompatibel sein sollte.

Ist das sinnvoll?

Sicher, aber es ist für Rust nicht wirklich relevant, da wir keine Verschiebungskonstruktoren haben. Ein (beweglicher) Allokator, der direkt den Speicher enthält, auf den Zeiger verteilt werden, ist also nicht möglich.

In C ++ können Sie beispielsweise die Elemente eines Vektors nicht ohne einen Stapelzuweiser in das statische Speichersegment Ihrer Binärdatei einfügen (Sie können jedoch eine eigene Sammlung schreiben, die dies tut).

Dies ist keine Verhaltensänderung. Es gibt viele triftige Gründe, um zu steuern, woher Sammlungen ihren Speicher beziehen, aber alle beziehen sich auf "externe Effekte" wie Leistung, Linkerskripte, Kontrolle über das Speicherlayout des gesamten Programms usw.

Dinge wie align_allocator ermöglichen es Ihnen, diese Typen zu häufen und sie sogar in Standardauflistungen zu speichern, ohne undefiniertes Verhalten aufzurufen, indem Sie einen anderen Allokator verwenden.

Aus diesem Grund habe ich speziell den cache_aligned_allocator von TBB und nicht den align_allocator von Eigen erwähnt. cache_aligned_allocator scheint keine spezifische Ausrichtung in seiner Dokumentation zu garantieren (es heißt nur, dass es "normalerweise" 128 Byte ist), und selbst wenn dies der Fall wäre, würde es normalerweise nicht für diesen Zweck verwendet werden (da seine Ausrichtung wahrscheinlich zu groß für allgemeine Zwecke ist SIMD-Typen und zu klein für Dinge wie seitenausgerichtetes DMA). Wie Sie sagen, dient es dazu, falsches Teilen zu vermeiden.

@gnzlbg

FWIW, parametrische Allokatoren sind sehr nützlich, um Allokatoren zu implementieren.

Könnten Sie etwas näher darauf eingehen, was Sie meinen? Gibt es einen Grund, warum Sie Ihre benutzerdefinierten Allokatoren nicht mit dem Merkmal Alloc parametrisieren können? Oder Ihr eigenes benutzerdefiniertes Allokatormerkmal und implementieren Sie das Allokationsmerkmal einfach auf den endgültigen Allokatoren (diese beiden Merkmale müssen nicht unbedingt gleich sein)?

Ich glaube ich war nicht klar; Lass mich versuchen, es besser zu erklären. Angenommen, ich implementiere einen Allokator, den ich voraussichtlich verwenden werde:

  • Als globaler Allokator
  • In einer No-Standard-Umgebung

Nehmen wir an, ich möchte Vec unter der Haube verwenden, um diesen Allokator zu implementieren. Ich kann Vec nicht einfach direkt verwenden, wie es heute existiert, weil

  • Wenn ich der globale Allokator bin, führt die Verwendung nur zu einer rekursiven Abhängigkeit von mir
  • Wenn ich mich in einer No-Standard-Umgebung befinde, gibt es kein Vec wie es heute existiert

Daher muss ich in der Lage sein, ein Vec , das auf einem anderen Allokator parametrisiert ist, den ich intern für die einfache interne Buchhaltung verwende. Dies ist das Ziel von bsalloc (und die Quelle des Namens - es wird verwendet, um andere

In elfmalloc können wir immer noch ein globaler Allokator sein, indem wir:

  • Wenn Sie uns kompilieren, kompilieren Sie statisch jemalloc als globalen Allokator
  • Erstellen Sie eine gemeinsam genutzte Objektdatei, die von anderen Programmen dynamisch geladen werden kann

Beachten Sie, dass in diesem Fall ist es wichtig, dass wir uns nicht selbst kompilieren das System allocator als globale allocator verwenden , weil dann, einmal geladen, würden wir die rekursive Abhängigkeit , weil an diesem Punkt wieder einzuführen, wir das System allocator sind.

Aber es funktioniert nicht, wenn:

  • Jemand möchte uns auf "offizielle" Weise als globalen Allokator in Rust verwenden (im Gegensatz dazu, dass zuerst eine gemeinsame Objektdatei erstellt wird).
  • Wir sind in einer normalen Umgebung

OTOH, der C ++ - Standard unterstützt keine überausgerichteten Typen wie SIMD-Typen. Wenn Sie versuchen, einen mit einem neuen Heap zuzuweisen, rufen Sie ein undefiniertes Verhalten auf (Sie müssen posix_memalign oder ähnliches verwenden).

Da unser aktuelles Merkmal Alloc die Ausrichtung als Parameter verwendet, gehe ich davon aus, dass diese Problemklasse (das Problem "Ich kann ohne eine andere Ausrichtung nicht arbeiten") für uns verschwindet.

@gnzlbg - eine umfassende Beschreibung (danke), aber keiner der Anwendungsfälle deckt den dauerhaften Speicher ab *.

Dieser Anwendungsfall muss berücksichtigt werden. Insbesondere beeinflusst es stark, was das Richtige ist:

  • Dass mehr als ein Allokator verwendet wird, und insbesondere wenn dieser Allokator für dauerhaften Speicher verwendet wird, wäre er niemals der Systemzuweiser . (In der Tat kann es mehrere persistente Speicherzuordnungen geben.)
  • Die Kosten für die Neuimplementierung der Standardsammlungen sind hoch und führen zu inkompatiblem Code mit Bibliotheken von Drittanbietern.
  • Die Lebensdauer des Allokators beträgt nicht unbedingt 'static .
  • Objekte, die im persistenten Speicher gespeichert sind, benötigen einen zusätzlichen Status, der aus dem Heap ausgefüllt werden muss, dh sie müssen neu initialisiert werden. Dies gilt insbesondere für Mutexe und dergleichen. Was früher wegwerfbar war, wird nicht mehr entsorgt.

Rust hat eine hervorragende Gelegenheit, die Initiative hier zu ergreifen und sie zu einer erstklassigen Plattform für HDs, SSDs und sogar PCI-angeschlossenen Speicher zu machen.

* Eigentlich keine Überraschung, denn bis vor kurzem war es etwas Besonderes. Es wird jetzt in Linux, FreeBSD und Windows weitgehend unterstützt.

@raphaelcohn

Dies ist wirklich nicht der richtige Ort, um ein dauerhaftes Gedächtnis zu erarbeiten. Ihre ist nicht die einzige Denkrichtung in Bezug auf die Schnittstelle zum persistenten Speicher. Beispielsweise kann sich herausstellen, dass der vorherrschende Ansatz darin besteht, sie aus Gründen der Datenintegrität einfach wie eine schnellere Festplatte zu behandeln.

Wenn Sie einen Anwendungsfall für die Verwendung von persistentem Speicher auf diese Weise haben, ist es wahrscheinlich besser, diesen Fall zuerst an anderer Stelle zu erstellen. Erstellen Sie einen Prototyp, überlegen Sie sich einige konkretere Änderungen an der Allokatorschnittstelle und stellen Sie im Idealfall fest, dass diese Änderungen die Auswirkungen wert sind, die sie im Durchschnitt haben würden.

@rpjohnst

Ich stimme dir nicht zu. Dies ist genau die Art von Ort, zu dem es gehört. Ich möchte vermeiden, dass eine Entscheidung getroffen wird, die ein Design schafft, das das Ergebnis eines zu engen Fokus und der Suche nach Beweisen ist.

Das aktuelle Intel PMDK, bei dem ein großer Aufwand für die Unterstützung des Benutzerbereichs auf niedriger Ebene im Vordergrund steht, nähert sich dem weitaus mehr als zugewiesenen regulären Speicher mit Zeigern an - Speicher, der dem über mmap ähnlich ist. In der Tat, wenn man unter Linux mit beständigem Speicher arbeiten möchte, dann ist dies meiner Meinung nach im Moment so ziemlich Ihre einzige Anlaufstelle. Im Wesentlichen behandelt eines der fortschrittlichsten Toolkits für die Verwendung - das vorherrschende, wenn Sie so wollen - es als zugewiesenen Speicher.

Was das Prototyping angeht - genau das habe ich gesagt : -

Ich habe kürzlich an einem Rust-Wrapper für einen dauerhaften Speicherzuweiser (insbesondere libpmemcto) gearbeitet.

(Sie können eine frühe Version meiner Kiste unter https://crates.io/crates/nvml verwenden . Im Modul cto_pool gibt es viel mehr Experimente zur Quellcodeverwaltung.)

Mein Prototyp ist darauf ausgelegt, was erforderlich ist, um eine Datenspeicher-Engine in einem realen Großsystem zu ersetzen. Eine ähnliche Denkweise steckt hinter vielen meiner Open Source-Projekte. Ich habe über viele Jahre die besten Bibliotheken gefunden, ebenso wie die besten Standards, die sich aus der tatsächlichen Nutzung ableiten.

Nichts geht über den Versuch, einen realen Allokator an die aktuelle Schnittstelle anzupassen. Ehrlich gesagt war die Erfahrung, die Alloc -Schnittstelle zu verwenden, dann die gesamten Vec kopieren und dann zu optimieren, schmerzhaft. Viele Orte gehen davon aus, dass Allokatoren nicht übergeben werden, z. B. Vec::new() .

Dabei habe ich in meinem ursprünglichen Kommentar einige Bemerkungen dazu gemacht, was von einem Allokator und was von einem Benutzer eines solchen Allokators verlangt würde. Ich denke, diese sind in einem Diskussionsthread über eine Allokator-Schnittstelle sehr gültig.

Die gute Nachricht ist, dass Ihre ersten 3 Aufzählungspunkte von https://github.com/rust-lang/rust/issues/32838#issuecomment -358940992 von anderen Anwendungsfällen geteilt werden.

Ich wollte nur hinzufügen, dass ich der Liste keinen nichtflüchtigen Speicher hinzugefügt habe
weil in der Liste Anwendungsfälle von Allokatoren aufgeführt sind, die Container in parametrisieren
die C ++ - Welt, die "weit verbreitet" ist, zumindest nach meiner Erfahrung (diese
Die von mir erwähnten Alloktoren stammen größtenteils aus sehr populären Bibliotheken, die von vielen benutzt werden.
Ich kenne zwar die Bemühungen des Intel SDK (einige ihrer Bibliotheken)
Ziel C ++) Ich persönlich kenne keine Projekte, die sie verwenden (haben sie
ein Allokator, der mit std :: vector verwendet werden kann? Ich weiß es nicht). Das geht nicht
bedeuten, dass sie weder verwendet noch wichtig sind. Ich würde gerne wissen
über diese, aber der Hauptpunkt meines Beitrags war die Parametrisierung
Allokatoren nach Containern sind sehr komplex, und wir sollten versuchen, zu machen
Fortschritte bei den Systemzuordnungen, ohne dass Türen für Container geschlossen werden müssen
(aber wir sollten das später angehen).

Am Sonntag, den 21. Januar 2018 um 17:36 Uhr schrieb John Ericson [email protected] :

Die gute Nachricht sind Ihre ersten 3 Aufzählungspunkte von # 32838 (Kommentar)
https://github.com/rust-lang/rust/issues/32838#issuecomment-358940992
werden von anderen Anwendungsfällen geteilt.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/32838#issuecomment-359261305 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AA3Npk95PZBZcm7tknNp_Cqrs_3T1UkEks5tM2ekgaJpZM4IDYUN
.

Ich habe versucht, das meiste zu lesen, was bereits geschrieben wurde, so dass dies möglicherweise bereits hier ist. In diesem Fall tut es mir leid, wenn ich es verpasst habe, aber hier ist:

Etwas, das für Spiele (in C / C ++) ziemlich häufig ist, ist die Verwendung der "Scratch-Zuweisung pro Frame". Dies bedeutet, dass es einen linearen / Bump-Allokator gibt, der für Allokationen verwendet wird, die für einen bestimmten Zeitraum (in) aktiv sind ein Spielrahmen) und dann "zerstört".

In diesem Fall zerstört, was bedeutet, dass Sie den Allokator wieder in seine Ausgangsposition zurücksetzen. Es gibt überhaupt keine "Zerstörung" von Objekten, da diese Objekte vom Typ POD sein müssen (daher werden keine Destruktoren ausgeführt).

Ich frage mich, ob so etwas zum aktuellen Allokator-Design in Rust passt.

(edit: Es sollte KEINE Zerstörung von Objekten geben)

@emoon

Etwas, das für Spiele (in C / C ++) ziemlich häufig ist, ist die Verwendung der "Scratch-Zuweisung pro Frame". Dies bedeutet, dass es einen linearen / Bump-Allokator gibt, der für Allokationen verwendet wird, die für einen bestimmten Zeitraum (in) aktiv sind ein Spielrahmen) und dann "zerstört".

In diesem Fall zerstört, was bedeutet, dass Sie den Allokator wieder in seine Ausgangsposition zurücksetzen. Es gibt überhaupt eine "Zerstörung" von Objekten, da diese Objekte vom POD-Typ sein müssen (daher werden keine Destruktoren ausgeführt).

Sollte machbar sein. Auf den ersten Blick benötigen Sie ein Objekt für die Arena selbst und ein anderes Objekt, bei dem es sich um einen Frame pro Frame für die Arena handelt. Dann könnten Sie Alloc für dieses Handle implementieren und davon ausgehen, dass Sie für die Zuweisung sichere Wrapper auf hoher Ebene verwenden (stellen Sie sich beispielsweise vor, dass Box für Alloc parametrisch wird) Die Lebensdauer würde sicherstellen, dass alle zugewiesenen Objekte gelöscht wurden, bevor das Handle pro Frame gelöscht wurde. Beachten Sie, dass dealloc weiterhin für jedes Objekt aufgerufen wird. Wenn jedoch dealloc ein No-Op wäre, könnte die gesamte Drop-and-Deallocate-Logik vollständig oder größtenteils wegoptimiert werden.

Sie können auch einen benutzerdefinierten intelligenten Zeigertyp verwenden, der Drop nicht implementiert, was an anderer Stelle viele Dinge einfacher machen würde.

Vielen Dank! Ich habe in meinem ursprünglichen Beitrag einen Tippfehler gemacht. Es ist zu sagen, dass es keine Zerstörung von Objekten gibt.

Was ist der aktuelle Konsens für Personen, die keine Experten für Allokatoren sind und diesem Thread nicht folgen können: Planen wir, benutzerdefinierte Allokatoren für die stdlib-Sammlungstypen zu unterstützen?

@alexreg Ich bin mir nicht sicher, was der endgültige Plan ist, aber es wurden 0 technische Schwierigkeiten dabei bestätigt. OTOH, wir haben keine gute Möglichkeit, dies in std aufzudecken, da Standardtypvariablen verdächtig sind, aber ich habe kein Problem damit, es vorerst nur zu einer alloc -Nur zu machen, also wir kann auf der lib-Seite ungehindert Fortschritte machen.

@ Ericson2314 Okay, gut zu hören. Sind Standardtypvariablen bereits implementiert? Oder vielleicht in der RFC-Phase? Wie Sie sagen, wenn sie nur auf Dinge beschränkt sind, die mit alloc / std::heap , sollte alles in Ordnung sein.

Ich denke wirklich, dass AllocErr ein Fehler sein sollte. Es wäre konsistenter mit anderen Modulen (zB io).

impl Error for AllocError macht wahrscheinlich Sinn und tut nicht weh, aber ich persönlich habe festgestellt, dass das Merkmal Error nutzlos ist.

Ich habe mir heute die Funktion Layout :: from_size_align angesehen, und die Einschränkung " align darf 2 ^ 31 (dh 1 << 31 ) nicht überschreiten" ergab für mich keinen Sinn. Und git Schuld zeigte auf # 30170.

Ich muss sagen, dass dies dort eine ziemlich trügerische Commit-Nachricht war, die davon sprach, dass align in ein U32 passt, was nur zufällig ist, wenn das eigentliche Problem, das "behoben" wird (mehr umgangen wird), ein schlechtes Verhalten des Systemzuweisers ist.

Was mich zu diesem Hinweis führt: Das Element "OSX / alloc_system ist bei großen Ausrichtungen fehlerhaft" sollte hier nicht aktiviert werden. Obwohl das direkte Problem behoben wurde, denke ich, dass das Update auf lange Sicht nicht richtig ist: Da sich ein System-Allokator schlecht verhält, sollte die Implementierung eines Allokators, der sich verhält, nicht verhindert werden. Und die willkürliche Einschränkung von Layout :: from_size_align macht das.

@glandium Ist es nützlich, die Ausrichtung auf ein Vielfaches von 4 Gigbyte oder mehr anzufordern?

Ich kann mir Fälle vorstellen, in denen eine Zuordnung von 4GiB auf 4GiB ausgerichtet sein soll, was derzeit nicht möglich ist, aber kaum mehr. Aber ich denke nicht, dass willkürliche Einschränkungen hinzugefügt werden sollten, nur weil wir jetzt nicht an solche Gründe denken.

Ich kann mir Fälle vorstellen, in denen eine Zuordnung von 4GiB auf 4GiB ausgerichtet sein soll

Was sind das für Fälle?

Ich kann mir Fälle vorstellen, in denen eine Zuordnung von 4GiB auf 4GiB ausgerichtet sein soll

Was sind das für Fälle?

Konkret habe ich gerade die Unterstützung für beliebig große Ausrichtungen in mmap-alloc hinzugefügt, um die Zuweisung großer, ausgerichteter Speicherplatten zur Verwendung in elfmalloc . Die Idee ist, die Speicherplatte auf ihre Größe auszurichten, sodass Sie bei einem Zeiger auf ein von dieser Platte zugewiesenes Objekt lediglich die niedrigen Bits maskieren müssen, um die enthaltende Platte zu finden. Wir verwenden derzeit keine 4 GB großen Platten (für so große Objekte gehen wir direkt zu mmap), aber es gibt keinen Grund, warum wir dies nicht könnten, und ich könnte mir eine Anwendung mit großen RAM-Anforderungen vorstellen, die dies tun wollte das (das heißt, wenn es Objekte mit mehreren GB häufig genug zugewiesen hat, dass es den Overhead von mmap nicht akzeptieren wollte).

Hier ist ein möglicher Anwendungsfall für die Ausrichtung> 4 GB: Ausrichtung an einer großen Seitengrenze. Es gibt bereits Plattformen, die> 4 GiB-Seiten unterstützen. In diesem IBM Dokument heißt es: "Der POWER5 + -Prozessor unterstützt vier Seitengrößen für den virtuellen Speicher: 4 KB, 64 KB, 16 MB und 16 GB." Auch ist x86-64 nicht weit weg: „huge Seiten“ in der Regel 2 MiB sind, aber es ist auch unterstützt 1 GiB.

Alle nicht typisierten Funktionen im Alloc-Merkmal befassen sich mit *mut u8 . Das heißt, sie könnten Nullzeiger nehmen oder zurückgeben, und die Hölle würde losbrechen. Sollten sie stattdessen NonNull verwenden?

Es gibt viele Hinweise, von denen sie zurückkehren könnten, von denen die Hölle kommen würde
ausbrechen.
Am Sonntag, den 4. März 2018 um 03:56 Uhr schrieb Mike Hommey [email protected] :

Alle nicht typisierten Funktionen im Alloc-Merkmal befassen sich mit * mut u8.
Das heißt, sie könnten Nullzeiger nehmen oder zurückgeben, und die Hölle würde es tun
ausbrechen. Sollten sie stattdessen NonNull verwenden?

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/32838#issuecomment-370223269 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/ABY2UR2dRxDtdACeRUh_djM-DExRuLxiks5ta9aFgaJpZM4IDYUN
.

Ein zwingender Grund für die Verwendung NonNull ist , dass es erlauben würde , dem Result s zur Zeit von zurück Alloc Methoden (oder Options , wenn wir zu diesem in - Schalter die Zukunft) kleiner sein.

Ein zwingenderer Grund für die Verwendung von NonNull ist, dass die derzeit von Alloc-Methoden (oder Optionen, wenn wir in Zukunft darauf umsteigen) zurückgegebenen Ergebnisse kleiner werden.

Ich glaube nicht, dass es so wäre, weil AllocErr zwei Varianten hat.

Es gibt viele Hinweise, von denen sie zurückkehren könnten, von denen die Hölle losbrechen würde.

Ein Nullzeiger ist jedoch eindeutig falscher als jeder andere Zeiger.

Ich denke gerne, dass das Rostsystem bei Fußgewehren hilft und zum Codieren von Invarianten verwendet wird. In der Dokumentation zu alloc heißt es eindeutig: "Wenn diese Methode ein Ok(addr) zurückgibt, ist die zurückgegebene Adresse eine Adresse ungleich Null", der Rückgabetyp jedoch nicht. Aus Ok(malloc(layout.size())) wäre

Beachten Sie, dass es auch Hinweise gibt, dass die Größe von Layout nicht Null sein muss, daher würde ich auch argumentieren, dass dies als NonZero codiert werden sollte.

Nicht weil all diese Funktionen von Natur aus unsicher sind, sollten wir keine Fußwaffenprävention haben.

Von allen möglichen Fehlern bei der Verwendung (Bearbeiten: und Implementieren) von Allokatoren ist das Übergeben eines Nullzeigers einer der am einfachsten zu findenden Fehler (bei der Dereferenzierung wird immer ein sauberer Segfault angezeigt, zumindest wenn Sie eine MMU haben und dies nicht getan haben) sehr seltsame Dinge damit), und normalerweise auch eine der trivialsten, die es zu beheben gilt. Es ist wahr, dass unsichere Schnittstellen versuchen können, Fußgewehre zu verhindern, aber diese Fußgewehre scheinen unverhältnismäßig klein zu sein (im Vergleich zu den anderen möglichen Fehlern und zur Ausführlichkeit der Codierung dieser Invariante im Typsystem).

Außerdem ist es wahrscheinlich, dass Allokator-Implementierungen nur den ungeprüften Konstruktor von NonNull "für die Leistung" verwenden: Da ein korrekter Allokator ohnehin niemals null zurückgeben würde, würde er die NonNell::new(...).unwrap() überspringen wollen. In diesem Fall erhalten Sie keine konkrete Verhinderung von Fußgewehren, nur mehr Boilerplate. (Die Größenvorteile von Result können, wenn sie real sind, immer noch ein zwingender Grund dafür sein.)

Allokator-Implementierungen würden nur den ungeprüften Konstruktor von NonNull verwenden

Es geht weniger darum, die Implementierung des Allokators zu unterstützen, als vielmehr darum, den Benutzern zu helfen. Wenn MyVec ein NonNull<T> und Heap.alloc() bereits ein NonNull zurückgibt, muss dieser weniger geprüfte oder unsichere nicht aktivierte Anruf getätigt werden.

Beachten Sie, dass Zeiger nicht nur Rückgabetypen sind, sondern auch Eingabetypen für z. B. dealloc und realloc . Sollen diese Funktionen gegen eine mögliche Eingabe von Null härten oder nicht? Die Dokumentation würde dazu neigen, Nein zu sagen, aber das Typensystem würde dazu neigen, Ja zu sagen.

Ganz ähnlich mit layout.size (). Sollen Zuordnungsfunktionen irgendwie damit umgehen, dass die angeforderte Größe 0 ist, oder nicht?

(Die Vorteile der Ergebnisgröße können, falls sie real sind, immer noch ein zwingender Grund dafür sein.)

Ich bezweifle, dass es Größenvorteile gibt, aber mit so etwas wie # 48741 würde es Codegenvorteile geben.

Wenn wir dieses Prinzip der Flexibilität für Benutzer der API fortsetzen, sollten Zeiger in Rückgabetypen NonNull , nicht jedoch in Argumenten. (Dies bedeutet nicht, dass diese Argumente zur Laufzeit auf Null gesetzt werden sollten.)

Ich denke, der Ansatz eines Postel-Gesetzes ist der falsche. Gibt es irgendwelche
Fall, in dem die Übergabe eines Nullzeigers an eine Alloc-Methode gültig ist? Wenn nicht,
dann gibt diese Flexibilität der Fußwaffe im Grunde nur etwas mehr
empfindlicher Auslöser.

Am 5. März 2018, 8:00 Uhr, schrieb "Simon Sapin" [email protected] :

Wenn wir dieses Prinzip der Flexibilität für Benutzer der API fortsetzen,
Zeiger sollten in Rückgabetypen NonNull sein, jedoch nicht in Argumenten. (Diese
bedeutet nicht, dass diese Argumente zur Laufzeit auf Null gesetzt werden sollten.)

- -
Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/32838#issuecomment-370327018 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AA_2L8zrOLyUv5mUc_kiiXOAn1f60k9Uks5tbOJ0gaJpZM4IDYUN
.

Es geht weniger darum, die Implementierung des Allokators zu unterstützen, als vielmehr darum, den Benutzern zu helfen. Wenn MyVec ein NonNull enthältund Heap.alloc () gibt bereits einen NonNull zurück, den ich weniger überprüft oder unsicher deaktiviert habe und den ich ausführen muss.

Ah das macht Sinn. Repariert die Fußwaffe nicht, zentralisiert aber die Verantwortung dafür.

Beachten Sie, dass Zeiger nicht nur Rückgabetypen sind, sondern auch Eingabetypen für z. B. Dealloc und Realloc. Sollen diese Funktionen gegen eine mögliche Eingabe von Null härten oder nicht? Die Dokumentation würde dazu neigen, Nein zu sagen, aber das Typensystem würde dazu neigen, Ja zu sagen.

Gibt es einen Fall, in dem die Übergabe eines Nullzeigers an eine Alloc-Methode gültig ist? Wenn nicht, dann gibt diese Flexibilität der Fußwaffe im Grunde nur einen etwas empfindlicheren Abzug.

Der Benutzer hat absolut die Dokumentation zu lesen und die Invarianten im Auge zu behalten. Viele Invarianten können überhaupt nicht über das Typsystem erzwungen werden - wenn sie könnten, wäre die Funktion zunächst nicht unsicher. Dies ist also nur eine Frage, ob das Einfügen von NonNull in eine bestimmte Benutzeroberfläche den Benutzern tatsächlich hilft

  • Erinnern Sie sie daran, die Dokumente zu lesen und über die Invarianten nachzudenken
  • Bequemlichkeit bieten ( @SimonSapins Punkt für den Rückgabewert der Zuweisung)
  • einige materielle Vorteile bieten (z. B. Layoutoptimierungen)

Ich sehe keinen großen Vorteil darin, zB das Argument von dealloc in NonNull . Ich sehe ungefähr zwei Verwendungsklassen dieser API:

  1. Relativ triviale Verwendung: Wenn Sie alloc aufrufen, speichern Sie den zurückgegebenen Zeiger irgendwo und übergeben Sie den gespeicherten Zeiger nach einer Weile an dealloc .
  2. Komplizierte Szenarien mit FFI, viel Zeigerarithmetik usw., in denen eine wichtige Logik erforderlich ist, um sicherzustellen, dass Sie am Ende das Richtige an dealloc .

Das Nehmen von NonNull hier hilft im Grunde nur der ersten Art von Anwendungsfall, da diese die NonNull an einem schönen Ort speichern und sie unverändert an NonNull . Theoretisch könnte es einige Tippfehler verhindern (Übergabe von foo wenn Sie bar gemeint haben), wenn Sie mit mehreren Zeigern jonglieren und nur einer von ihnen NonNull , aber dies scheint nicht zu sein zu häufig oder wichtig. Der Nachteil von dealloc , der einen rohen Zeiger nimmt (unter der Annahme, dass alloc NonNull zurückgibt, NonNull dem @SimonSapin überzeugt ist, dass es passieren sollte), wäre, dass ein as_ptr in erforderlich ist Der Dealloc-Anruf, der möglicherweise ärgerlich ist, aber die Sicherheit in keiner Weise beeinträchtigt.

Die zweite Art von Anwendungsfall wird nicht unterstützt, da sie wahrscheinlich nicht während des gesamten Prozesses NonNull Daher müsste manuell ein NonNull aus dem erhaltenen Rohzeiger neu erstellt werden mit welchen Mitteln auch immer. Wie ich bereits sagte, würde dies wahrscheinlich eher zu einer ungeprüften / unsafe Behauptung als zu einer tatsächlichen Laufzeitprüfung werden, sodass keine Fußgewehre verhindert werden.

Dies bedeutet nicht, dass ich dafür bin, dass dealloc einen rohen Zeiger nimmt. Ich sehe einfach keine der behaupteten Vorteile für Fußgewehre. Die Konsistenz der Typen gewinnt wahrscheinlich nur standardmäßig.

Es tut mir leid, aber ich habe dies als "Viele Invarianten können überhaupt nicht über das Typsystem erzwungen werden ... deshalb versuchen wir es nicht einmal" gelesen. Lass das Perfekte nicht der Feind des Guten sein!

Ich denke, es geht mehr um die Kompromisse zwischen den Garantien von NonNull und der Ergonomie, die durch den Wechsel zwischen NonNull und Rohzeigern verloren geht. Ich habe so oder so keine besonders starke Meinung - keine Seite scheint unvernünftig.

@cramertj Ja, aber ich kaufe die Prämisse dieser Art von Argument nicht wirklich. Die Leute sagen, Alloc ist für obskure, versteckte und weitgehend unsichere Anwendungsfälle. Nun, in obskurem, schwer lesbarem Code möchte ich so viel Sicherheit wie möglich haben - gerade weil sie so selten berührt werden, ist es wahrscheinlich, dass der ursprüngliche Autor nicht da ist. Umgekehrt, wenn der Code Jahre später gelesen wird, schrauben Sie die Egonomie. Wenn überhaupt, ist es kontraproduktiv. Der Code sollte sich bemühen, sehr explizit zu sein, damit ein unbekannter Leser besser herausfinden kann, was in aller Welt vor sich geht. Weniger Rauschen <klarere Invarianten.

Die zweite Art von Anwendungsfall wird nicht unterstützt, da sie wahrscheinlich nicht NonNull während des gesamten Prozesses verwenden kann. Daher müsste manuell ein NonNull aus dem Rohzeiger neu erstellt werden, den es erhalten hat mit welchen Mitteln auch immer.

Dies ist einfach ein Koordinationsfehler, keine technische Unvermeidlichkeit. Sicher, im Moment verwenden viele unsichere APIs möglicherweise Rohzeiger. Es muss also etwas den Weg weisen, mit NonNull oder anderen Wrappern zu einer überlegenen Benutzeroberfläche zu wechseln. Dann kann anderer Code leichter nachziehen. Ich sehe keinen Grund, ständig auf schwer lesbare, nicht informative Rohzeiger in Greenfield, All-Rust, unsicherem Code zurückzugreifen.

Hallo!

Ich möchte nur sagen, dass ich als Autor / Betreuer eines benutzerdefinierten Rust-Allokators für NonNull bin. So ziemlich aus all den Gründen, die bereits in diesem Thread dargelegt wurden.

Ich möchte auch darauf hinweisen, dass @glandium der

Ich könnte viel mehr schreiben, um auf @ Ericson2314 und andere zu antworten, aber es wird schnell zu einer sehr Sicherheitsvorteile von NonNull in dieser Art von API ist (es gibt natürlich noch andere Vorteile). Das heißt nicht, dass es keine Sicherheitsvorteile gibt, aber wie @cramertj sagte, gibt es Kompromisse und ich denke, die "Pro" -Seite ist überbewertet. Unabhängig davon habe ich bereits gesagt, dass ich aus anderen Gründen dazu neige , NonNull an verschiedenen Orten zu verwenden - aus dem Grund, dass @SimonSapin alloc , in dealloc für Konsistenz gegeben hat. Also machen wir das und gehen nicht mehr auf Tangenten los.

Wenn es ein paar NonNull Anwendungsfälle gibt, mit denen jeder an Bord ist, ist das ein guter Anfang.

Wir werden wahrscheinlich Unique und Freunde aktualisieren wollen, um NonNull anstelle von NonZero zu verwenden, um die Reibung mindestens innerhalb von liballoc niedrig zu halten. Aber das sieht so aus, als würden wir sowieso schon auf einer Abstraktionsebene über dem Allokator arbeiten, und ich kann mir keine Gründe vorstellen, dies nicht auch auf der Allokatorebene zu tun. Sieht für mich nach einer vernünftigen Veränderung aus.

(Es gibt bereits zwei Implementierungen des Merkmals From , die sicher zwischen Unique<T> und NonNull<T> konvertieren.)

Da ich etwas sehr Ähnliches wie die Allokator-API für stabilen Rost benötige, habe ich den Code aus dem Rost-Repo extrahiert und in eine separate Kiste gelegt:

Dies / könnte / kann verwendet werden, um experimentelle Änderungen an der API zu wiederholen, aber ab sofort ist es eine einfache Kopie dessen, was sich im Rost-Repo befindet.

[Off Topic] Ja, es wäre schön, wenn std solche Baumkisten mit stabilem Code verwenden könnte, damit wir mit instabilen Schnittstellen in stabilem Code experimentieren können. Dies ist einer der Gründe, warum ich gerne std eine Fassade habe.

std könnte von einer Kopie einer Kiste von crates.io abhängen, aber wenn Ihr Programm auch von derselben Kiste abhängt, würde es sowieso nicht wie die gleichen Kisten / Typen / Eigenschaften aussehen, die ich rosten würde, also ziehe ich an Ich sehe nicht, wie es helfen würde. Unabhängig von der Fassade ist es jedoch eine sehr bewusste Entscheidung, instabile Merkmale auf dem stabilen Kanal nicht verfügbar zu machen, und kein Unfall.

Es scheint, dass wir uns über die Verwendung von NonNull einig sind. Wie kann dies tatsächlich geschehen? Nur eine PR macht das? ein RFC?

Unabhängig davon habe ich mir eine Assembly angesehen, die aus Boxing-Dingen generiert wurde, und die Fehlerpfade sind ziemlich groß. Sollte etwas dagegen unternommen werden?

Beispiele:

pub fn bar() -> Box<[u8]> {
    vec![0; 42].into_boxed_slice()
}

pub fn qux() -> Box<[u8]> {
    Box::new([0; 42])
}

kompiliert zu:

example::bar:
  sub rsp, 56
  lea rdx, [rsp + 8]
  mov edi, 42
  mov esi, 1
  call __rust_alloc_zeroed<strong i="11">@PLT</strong>
  test rax, rax
  je .LBB1_1
  mov edx, 42
  add rsp, 56
  ret
.LBB1_1:
  mov rax, qword ptr [rsp + 8]
  movups xmm0, xmmword ptr [rsp + 16]
  movaps xmmword ptr [rsp + 32], xmm0
  mov qword ptr [rsp + 8], rax
  movaps xmm0, xmmword ptr [rsp + 32]
  movups xmmword ptr [rsp + 16], xmm0
  lea rdi, [rsp + 8]
  call __rust_oom<strong i="12">@PLT</strong>
  ud2

example::qux:
  sub rsp, 104
  xorps xmm0, xmm0
  movups xmmword ptr [rsp + 58], xmm0
  movaps xmmword ptr [rsp + 48], xmm0
  movaps xmmword ptr [rsp + 32], xmm0
  lea rdx, [rsp + 8]
  mov edi, 42
  mov esi, 1
  call __rust_alloc<strong i="13">@PLT</strong>
  test rax, rax
  je .LBB2_1
  movups xmm0, xmmword ptr [rsp + 58]
  movups xmmword ptr [rax + 26], xmm0
  movaps xmm0, xmmword ptr [rsp + 32]
  movaps xmm1, xmmword ptr [rsp + 48]
  movups xmmword ptr [rax + 16], xmm1
  movups xmmword ptr [rax], xmm0
  mov edx, 42
  add rsp, 104
  ret
.LBB2_1:
  movups xmm0, xmmword ptr [rsp + 16]
  movaps xmmword ptr [rsp + 80], xmm0
  movaps xmm0, xmmword ptr [rsp + 80]
  movups xmmword ptr [rsp + 16], xmm0
  lea rdi, [rsp + 8]
  call __rust_oom<strong i="14">@PLT</strong>
  ud2

Das ist eine ziemlich große Menge an Code, die an jedem Ort hinzugefügt werden kann, an dem Boxen erstellt werden. Vergleiche mit 1.19, das keine Allokator-API hatte:

example::bar:
  push rax
  mov edi, 42
  mov esi, 1
  call __rust_allocate_zeroed<strong i="18">@PLT</strong>
  test rax, rax
  je .LBB1_2
  mov edx, 42
  pop rcx
  ret
.LBB1_2:
  call alloc::oom::oom<strong i="19">@PLT</strong>

example::qux:
  sub rsp, 56
  xorps xmm0, xmm0
  movups xmmword ptr [rsp + 26], xmm0
  movaps xmmword ptr [rsp + 16], xmm0
  movaps xmmword ptr [rsp], xmm0
  mov edi, 42
  mov esi, 1
  call __rust_allocate<strong i="20">@PLT</strong>
  test rax, rax
  je .LBB2_2
  movups xmm0, xmmword ptr [rsp + 26]
  movups xmmword ptr [rax + 26], xmm0
  movaps xmm0, xmmword ptr [rsp]
  movaps xmm1, xmmword ptr [rsp + 16]
  movups xmmword ptr [rax + 16], xmm1
  movups xmmword ptr [rax], xmm0
  mov edx, 42
  add rsp, 56
  ret
.LBB2_2:
  call alloc::oom::oom<strong i="21">@PLT</strong>

Wenn dies tatsächlich von Bedeutung ist, ist es in der Tat ärgerlich. Aber vielleicht optimiert LLVM dies für größere Programme?

Es gibt 1439 Anrufe bei __rust_oom im neuesten Firefox pro Nacht. Firefox verwendet jedoch keinen Rost-Allokator, sodass wir direkte Aufrufe an malloc / calloc erhalten, gefolgt von einer Nullprüfung, dass der Sprung zum OOM-Vorbereitungscode, der normalerweise aus zwei Movq und einem Lea besteht, den AllocErr ausfüllt und dessen Adresse abruft um es an __rust__oom weiterzugeben. Das ist im Wesentlichen das beste Szenario, aber das sind immer noch 20 Bytes Maschinencode für die beiden Movq und Lea.

Wenn ich mir ripgrep anschaue, gibt es 85, und sie sind alle in identischen _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn -Funktionen. Alle von ihnen sind 16 Bytes lang. Es gibt 685 Aufrufe dieser Wrapper-Funktionen, denen vor den meisten ein Code vorangestellt ist, der dem ähnelt, den ich in https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485 eingefügt habe.

@nox hat heute mergefunc llvm-Pass zu aktivieren. Ich frage mich, ob das hier einen Unterschied macht.

mergefunc anscheinend die mehreren identischen _ZN61_$LT$alloc..heap..Heap$u20$as$u20$alloc..allocator..Alloc$GT$3oom17h53c76bda5 0c6b65aE.llvm.nnnnnnnnnnnnnnn -Funktionen nicht los (versucht mit -C passes=mergefunc in RUSTFLAGS ).

Aber was einen großen Unterschied macht, ist LTO, was eigentlich dazu führt, dass Firefox Malloc direkt anruft und die Erstellung von AllocErr direkt vor dem Aufruf von __rust_oom belässt. Das macht auch die Erstellung des Layout unnötig, bevor der Allokator aufgerufen wird, und überlässt dies beim Füllen des AllocErr .

Dies lässt mich denken, dass die Zuordnungsfunktionen, außer __rust_oom wahrscheinlich inline markiert werden sollten.

Übrigens, nachdem ich mir den generierten Code für Firefox angesehen habe, denke ich, dass es idealerweise wünschenswert wäre, moz_xmalloc anstelle von malloc . Dies ist ohne eine Kombination der Allocator-Merkmale und die Möglichkeit, den globalen Heap-Allocator zu ersetzen, nicht möglich, erfordert jedoch möglicherweise einen benutzerdefinierten Fehlertyp für das Allocator-Merkmal: moz_xmalloc ist unfehlbar und kehrt im Falle von nie zurück Fehler. IOW, es behandelt OOM selbst, und der Rostcode müsste in diesem Fall nicht __rust_oom aufrufen. Dies würde es für die Allokatorfunktionen wünschenswert machen, optional ! anstelle von AllocErr .

Wir haben darüber gesprochen, AllocErr einer Struktur mit der Größe Null zu machen, was auch hier hilfreich sein könnte. Wenn der Zeiger auch NonNull , kann der gesamte Rückgabewert zeigergroß sein.

https://github.com/rust-lang/rust/pull/49669 nimmt eine Reihe von Änderungen an diesen APIs vor, mit dem Ziel, eine Teilmenge zu stabilisieren, die globale Allokatoren abdeckt. Tracking-Problem für diese Teilmenge: https://github.com/rust-lang/rust/issues/49668. Insbesondere wird ein neues Merkmal GlobalAlloc eingeführt.

Wird diese PR es uns ermöglichen, Dinge wie Vec::new_with_alloc(alloc) zu tun, wo alloc: Alloc bald?

@alexreg nein

@sfackler Hmm , warum nicht? Was brauchen wir, bevor wir das tun können? Ich verstehe den Sinn dieser PR sonst nicht wirklich, es sei denn, sie dient einfach dazu, den globalen Allokator zu ändern.

@alexreg

Ich verstehe den Sinn dieser PR sonst nicht wirklich, es sei denn, sie dient einfach dazu, den globalen Allokator zu ändern.

Ich denke, es ist einfach für die Änderung des globalen Allokators.

@alexreg Wenn Sie auf stabil meinen, gibt es eine Reihe von ungelösten Designfragen, die wir nicht stabilisieren können. Bei Nightly wird dies von RawVec und kann wahrscheinlich als #[unstable] für Vec für jeden hinzugefügt werden, der daran arbeiten möchte.

Und ja, wie in der PR erwähnt, geht es darum, das Ändern des globalen Allokators oder das Zuweisen (z. B. in einem benutzerdefinierten Sammlungstyp) zu ermöglichen, ohne Vec::with_capacity zu verlieren.

FWIW, die in https://github.com/rust-lang/rust/issues/32838#issuecomment -376793369 erwähnte allocator_api -Kiste hat RawVec<T, A> und Box<T, A> auf dem Master Zweig (noch nicht freigegeben). Ich betrachte es als einen Inkubator dafür, wie generische Sammlungen über den Zuordnungstyp aussehen könnten (plus die Tatsache, dass ich einen Box<T, A> -Typ für stabilen Rost benötige). Ich habe noch nicht begonnen, vec.rs zu portieren, um Vec<T, A> hinzuzufügen, aber PRs sind willkommen. vec.rs ist groß.

Ich werde darauf hinweisen, dass die in https://github.com/rust-lang/rust/issues/32838#issuecomment -377097485 erwähnten Codegen- "Probleme" mit den Änderungen in # 49669 verschwunden sein sollten.

Mit einigen weiteren Überlegungen zur Verwendung des Merkmals Alloc zur Implementierung eines Allokators in Ebenen gibt es zwei Dinge, die ich für nützlich halte (zumindest für mich):

  • Wie bereits erwähnt, kann optional ein anderer AllocErr -Typ angegeben werden. Dies kann nützlich sein, um ! , oder, da AllocErr jetzt leer ist, optional mehr Informationen als "fehlgeschlagen" zu übermitteln.
  • Optional kann ein anderer Layout -Typ angegeben werden. Stellen Sie sich vor, Sie haben zwei Ebenen von Zuordnungen: eine für Seitenzuweisungen und eine für größere Regionen. Letztere können sich auf Ersteres verlassen, aber wenn beide denselben Layout -Typ verwenden, müssen beide Ebenen ihre eigene Validierung durchführen: Auf der niedrigsten Ebene ist diese Größe und Ausrichtung ein Vielfaches der Seitengröße. und auf der höheren Ebene entsprechen diese Größe und Ausrichtung den Anforderungen für die größeren Regionen. Diese Überprüfungen sind jedoch überflüssig. Bei speziellen Layout -Typen könnte die Validierung an die Layout -Erstellung anstatt im Allokator selbst delegiert werden, und Konvertierungen zwischen den Layout -Typen würden es ermöglichen, die redundanten Überprüfungen zu überspringen.

@cramertj @SimonSapin @glandium Okay, danke für die Klarstellung. Ich kann nur eine PR für einige der anderen Sammlungen-Prime-Typen einreichen. Ist es am besten, dies gegen Ihr Allocator-API-Repo / Ihre Kiste, @glandium oder Ihren

@alexreg In Anbetracht der Anzahl der Änderungen am Merkmal Alloc in # 49669 ist es wahrscheinlich besser, darauf zu warten, dass es zuerst zusammengeführt wird.

@ England Fair genug. Das scheint nicht zu weit von der Landung entfernt zu sein. Ich habe gerade das https://github.com/pnkfelix/collections-prime Repo bemerkt ... was ist das in Bezug auf deins?

Ich würde noch eine offene Frage hinzufügen:

  • Darf Alloc::oom in Panik geraten? Derzeit sagen die Dokumente, dass diese Methode den Prozess abbrechen muss. Dies hat Auswirkungen auf Code, der Allokatoren verwendet, da diese dann so ausgelegt sein müssen, dass das Abwickeln ordnungsgemäß funktioniert, ohne dass Speicher verloren geht.

Ich denke, wir sollten Panik zulassen, da ein Fehler in einem lokalen Allokator nicht unbedingt bedeutet, dass auch der globale Allokator ausfällt. Im schlimmsten Fall wird das oom des globalen Allokators aufgerufen, wodurch der Prozess abgebrochen wird (andernfalls würde der vorhandene Code beschädigt).

@alexreg Es ist nicht. Es scheint nur eine einfache Kopie dessen zu sein, was sich in std / alloc / collection befindet. Nun, eine zwei Jahre alte Kopie davon. Meine Kiste ist in ihrem Umfang viel eingeschränkter (die veröffentlichte Version hat nur das Merkmal Alloc einigen Wochen, der Hauptzweig hat nur RawVec und Box darüber das), und eines meiner Ziele ist es, es mit stabilem Rost zu bauen.

@glandium Okay, in diesem Fall ist es wahrscheinlich sinnvoll, dass ich warte, bis diese PR landet, dann eine PR gegen den tagge , damit du weißt, wann sie mit dem Master zusammengeführt wird (und sie dann in deine Kiste zusammenführen kann). , Messe?

@alexreg macht Sinn. Sie könnten / könnten jetzt anfangen, daran zu arbeiten, aber das würde wahrscheinlich zu einer gewissen Abwanderung führen, wenn / wenn das Bikeshedding die Dinge in dieser PR ändert.

@glandium Ich habe andere Dinge, die mich darum kümmern , wenn diese PR genehmigt wird. Es wird großartig sein, allokative generische Heap-Zuweisungen / Sammlungen sowohl auf nächtlicher als auch auf stabiler Basis bald zu erhalten. :-)

Darf Alloc :: oom in Panik geraten? Derzeit sagen die Dokumente, dass diese Methode den Prozess abbrechen muss. Dies hat Auswirkungen auf Code, der Allokatoren verwendet, da diese dann so ausgelegt sein müssen, dass das Abwickeln ordnungsgemäß funktioniert, ohne dass Speicher verloren geht.

@Amanieu Dieser RFC wurde zusammengeführt: https://github.com/rust-lang/rfcs/pull/2116 Die Dokumente und die Implementierung wurden möglicherweise noch nicht aktualisiert.

Es gibt eine Änderung an der API, für die ich eine PR einreichen möchte:

Teilen Sie das Merkmal Alloc in zwei Teile: "Implementierung" und "Helfer". Ersteres wären Funktionen wie alloc , dealloc , realloc usw. und letzteres alloc_one , dealloc_one , alloc_array usw. Obwohl es einige hypothetische Vorteile gibt, wenn eine benutzerdefinierte Implementierung für letztere möglich ist, ist dies bei weitem nicht die häufigste Anforderung, und wenn Sie generische Wrapper implementieren müssen (was ich als unglaublich häufig empfunden habe). Bis zu dem Punkt, an dem ich tatsächlich damit begonnen habe, eine benutzerdefinierte Ableitung dafür zu schreiben, müssen Sie noch alle implementieren, da der Wrappee sie möglicherweise anpasst.

OTOH, wenn ein Alloc Trait-Implementierer versucht, ausgefallene Dinge in z. B. alloc_one tun, kann nicht garantiert werden, dass dealloc_one für diese Zuordnung aufgerufen wird. Dafür gibt es mehrere Gründe:

  • Die Helfer werden nicht konsequent eingesetzt. Nur ein Beispiel: raw_vec verwendet eine Mischung aus alloc_array , alloc / alloc_zeroed , verwendet aber nur dealloc .
  • Selbst bei konsequenter Verwendung von zB alloc_array / dealloc_array kann man ein Vec sicher in ein Box umwandeln, das dann dealloc .
  • Dann gibt es einige Teile der API, die einfach nicht existieren (keine auf Null gesetzte Version von alloc_one / alloc_array )

Obwohl es tatsächlich Anwendungsfälle für die Spezialisierung von zB alloc_one (und tatsächlich habe ich einen solchen Bedarf an Mozjemalloc), ist es besser, stattdessen einen speziellen Allokator zu verwenden.

Eigentlich ist es schlimmer als das, im Rost-Repo gibt es genau eine Verwendung von alloc_array und keine Verwendung von alloc_one , dealloc_one , realloc_array , dealloc_array . Nicht einmal die Box-Syntax verwendet alloc_one , sondern exchange_malloc , was size und align . Daher sind diese Funktionen eher als Annehmlichkeit für Kunden als für Implementierer gedacht.

Mit so etwas wie impl<A: Alloc> AllocHelpers for A (oder AllocExt , welcher Name auch immer gewählt wird) hätten wir immer noch die Bequemlichkeit dieser Funktionen für Kunden, ohne dass Implementierer sich selbst in den Fuß schießen könnten, wenn sie dachten Sie würden ausgefallene Dinge tun, indem sie sie überschreiben (und es den Leuten erleichtern, Proxy-Allokatoren zu implementieren).

Es gibt eine Änderung an der API, für die ich eine PR einreichen möchte

Habe dies in # 50436 getan

@ England

(und tatsächlich habe ich ein solches Bedürfnis nach Mozjemalloc),

Könnten Sie diesen Anwendungsfall näher erläutern?

Mozjemalloc hat einen Basis-Allokator, der absichtlich leckt. Mit Ausnahme einer Art von Objekten, bei denen eine freie Liste geführt wird. Ich kann das tun, indem ich Allokatoren überlagere, anstatt Tricks mit alloc_one .

Ist es erforderlich, die Zuordnung mit der genauen Ausrichtung aufzuheben, die Sie zugewiesen haben?

Um zu verdeutlichen, dass die Antwort auf diese Frage JA lautet, habe ich dieses schöne Zitat von Microsoft selbst :

align_alloc () wird wahrscheinlich nie implementiert, da C11 es auf eine Weise spezifiziert hat, die mit unserer Implementierung nicht kompatibel ist (nämlich, dass free () in der Lage sein muss, hoch ausgerichtete Zuordnungen zu verarbeiten).

Für die Verwendung des Systemzuordners unter Windows ist es immer erforderlich, die Ausrichtung beim Freigeben zu kennen, um die Zuordnung hoch ausgerichteter Zuordnungen korrekt aufzuheben. Können wir diese Frage also einfach als gelöst markieren?

Für die Verwendung des Systemzuordners unter Windows ist es immer erforderlich, die Ausrichtung beim Freigeben zu kennen, um die Zuordnung hoch ausgerichteter Zuordnungen korrekt aufzuheben. Können wir diese Frage also einfach als gelöst markieren?

Es ist eine Schande, aber es ist so wie es ist. Lassen Sie uns dann auf überausgerichtete Vektoren verzichten. :verwirrt:

Lassen Sie uns dann auf überausgerichtete Vektoren verzichten

Woher? Sie benötigen nur Vec<T, OverAlignedAlloc<U16>> , das eine Überausrichtung zuweist und die Zuordnung aufhebt.

Woher? Sie benötigen nur Vec<T, OverAlignedAlloc<U16>> , das eine Überausrichtung zuweist und die Zuordnung aufhebt.

Ich hätte genauer sein sollen. Ich meinte, überausgerichtete Vektoren in eine API außerhalb Ihrer Kontrolle zu verschieben, dh eine, die Vec<T> und nicht Vec<T, OverAlignedAlloc<U16>> . (Zum Beispiel CString::new() .)

Du solltest lieber verwenden

#[repr(align(16))]
struct OverAligned16<T>(T);

und dann Vec<OverAligned16<T>> .

Du solltest lieber verwenden

Kommt darauf an. Angenommen, Sie möchten AVX-Intrinsics (256 Bit breit, 32-Byte-Ausrichtungsanforderung) für einen Vektor von f32 s verwenden:

  • Vec<T, OverAlignedAlloc<U32>> löst das Problem, man kann AVX-Intrinsics direkt auf den Vektorelementen verwenden (insbesondere ausgerichtete Speicherlasten), und der Vektor wird immer noch zu einem &[f32] Slice, was die Verwendung ergonomisch macht.
  • Vec<OverAligned32<f32>> löst das Problem nicht wirklich. Jedes f32 benötigt aufgrund der Ausrichtungsanforderung 32 Byte Speicherplatz. Das eingeführte Auffüllen verhindert die direkte Verwendung von AVX-Operationen, da sich die f32 nicht mehr im kontinuierlichen Speicher befinden. Und ich persönlich finde es etwas mühsam, mit dem Deref zu &[OverAligned32<f32>] umzugehen.

Für ein einzelnes Element in einem Box , Box<T, OverAligned<U32>> gegenüber Box<OverAligned32<T>> sind beide Ansätze gleichwertiger, und der zweite Ansatz könnte tatsächlich vorzuziehen sein. In jedem Fall ist es schön, beide Möglichkeiten zu haben.

Diese Änderungen wurden am Alloc-Merkmal veröffentlicht: https://internals.rust-lang.org/t/pre-rfc-changing-the-alloc-trait/7487

Der Tracking-Beitrag oben in dieser Ausgabe ist schrecklich veraltet (wurde zuletzt im Jahr 2016 bearbeitet). Wir brauchen eine aktualisierte Liste aktiver Anliegen, um die Diskussion produktiv fortzusetzen.

Die Diskussion würde auch erheblich von einem aktuellen Entwurfsdokument profitieren, das die aktuellen ungelösten Fragen und die Gründe für die Entwurfsentscheidungen enthält.

Es gibt mehrere Threads von Unterschieden von "Was wird derzeit in der Nacht implementiert?" Bis "Was wurde im ursprünglichen Alloc-RFC vorgeschlagen?", Die Tausende von Kommentaren auf verschiedenen Kanälen hervorbringen (RFC-Repo, Rost-Lang-Tracking-Problem, globaler Alloc-RFC, interne Beiträge, viele) riesige PRs usw.) und was im GlobalAlloc RFC stabilisiert wird, sieht nicht so sehr nach dem aus, was im ursprünglichen RFC vorgeschlagen wurde.

Dies ist etwas, das wir sowieso brauchen, um die Aktualisierung der Dokumente und der Referenz abzuschließen, und das auch in den aktuellen Diskussionen hilfreich wäre.

Ich denke, bevor wir überhaupt über die Stabilisierung des Merkmals Alloc nachdenken, sollten wir zunächst versuchen, die Allokatorunterstützung in allen Standardbibliothekssammlungen zu implementieren. Dies sollte uns einige Erfahrungen damit geben, wie dieses Merkmal in der Praxis verwendet wird.

Ich denke, bevor wir überhaupt über die Stabilisierung des Merkmals Alloc nachdenken, sollten wir zunächst versuchen, die Allokatorunterstützung in allen Standardbibliothekssammlungen zu implementieren. Dies sollte uns einige Erfahrungen damit geben, wie dieses Merkmal in der Praxis verwendet wird.

Ja absolut. Besonders Box , da wir noch nicht wissen, wie wir vermeiden sollen, dass Box<T, A> zwei Wörter aufnimmt.

Ja absolut. Besonders Box, da wir noch nicht wissen, wie wir Box vermeiden sollennimm zwei Wörter auf.

Ich denke nicht, dass wir uns die Größe von Box<T, A> für die anfängliche Implementierung Sorgen machen sollten, aber dies kann später auf abwärtskompatible Weise hinzugefügt werden, indem ein DeAlloc Merkmal hinzugefügt wird, das nur unterstützt Freigabe.

Beispiel:

trait DeAlloc {
    fn dealloc(&mut self, ptr: NonNull<Opaque>, layout: Layout);
}

trait Alloc {
    // In addition to the existing trait items
    type DeAlloc: DeAlloc = Self;
    fn into_dealloc(self) -> Self::DeAlloc {
        self
    }
}

impl<T: Alloc> DeAlloc for T {
    fn dealloc(&mut self, ptr: NonNull<Opaque>, layout: Layout) {
        Alloc::dealloc(self, ptr, layout);
    }
}

Ich denke, bevor wir überhaupt über die Stabilisierung des Alloc-Merkmals nachdenken, sollten wir zunächst versuchen, die Allocator-Unterstützung in allen Standardbibliotheksbeständen zu implementieren. Dies sollte uns einige Erfahrungen damit geben, wie dieses Merkmal in der Praxis verwendet wird.

Ich denke, @ Ericson2314 hat daran gearbeitet, per https://github.com/rust-lang/rust/issues/42774. Wäre schön ein Update von ihm zu bekommen.

Ich denke nicht, dass wir uns die Größe von Box<T, A> für die anfängliche Implementierung Sorgen machen sollten, aber dies kann später auf abwärtskompatible Weise hinzugefügt werden, indem ein DeAlloc Merkmal hinzugefügt wird, das nur unterstützt Freigabe.

Das ist ein Ansatz, aber mir ist überhaupt nicht klar, dass es definitiv der beste ist. Es hat zum Beispiel die entscheidenden Nachteile, dass a) es nur funktioniert, wenn ein Zeiger -> Allokator-Lookup möglich ist (dies gilt beispielsweise nicht für die meisten Arena-Allokatoren) und b) dealloc einen erheblichen Overhead hinzufügt dieser Vorschlag oder dieser Vorschlag ist . Oder vielleicht etwas ganz anderes. Ich denke also nicht, dass wir davon ausgehen sollten, dass dies leicht auf eine Weise zu lösen ist, die abwärtskompatibel mit der aktuellen Inkarnation des Merkmals Alloc .

@joshlf Angesichts der Tatsache, dass Box<T, A> nur dann Zugriff auf sich selbst hat, wenn es gelöscht wird, ist dies das Beste, was wir nur mit sicherem Code tun können. Ein solches Muster kann für Arena-ähnliche Allokatoren nützlich sein, die ein No-Op dealloc und nur freien Speicher haben, wenn der Allokator gelöscht wird.

Für kompliziertere Systeme, bei denen der Allokator einem Container gehört (z. B. LinkedList ) und mehrere Zuordnungen verwaltet, gehe ich davon aus, dass Box nicht intern verwendet wird. Stattdessen verwenden die Interna LinkedList Rohzeiger, die mit der Instanz Alloc zugewiesen und freigegeben werden, die im Objekt LinkedList ist. Dadurch wird vermieden, dass die Größe jedes Zeigers verdoppelt wird.

In Anbetracht der Tatsache, dass Box<T, A> nur dann Zugriff auf sich selbst hat, wenn es gelöscht wird, ist dies das Beste, was wir nur mit sicherem Code tun können. Ein solches Muster kann für Arena-ähnliche Allokatoren nützlich sein, die ein No-Op dealloc und nur freien Speicher haben, wenn der Allokator gelöscht wird.

Richtig, aber Box weiß nicht, dass dealloc kein Op ist.

Bei komplizierteren Systemen, bei denen der Allokator einem Container gehört (z. B. LinkedList ) und mehrere Allokationen verwaltet, gehe ich davon aus, dass Box nicht intern verwendet wird. Stattdessen verwenden die Interna LinkedList Rohzeiger, die mit der Instanz Alloc zugewiesen und freigegeben werden, die im Objekt LinkedList ist. Dadurch wird vermieden, dass die Größe jedes Zeigers verdoppelt wird.

Ich denke, es wäre wirklich eine Schande, von Leuten zu verlangen, dass sie unsicheren Code verwenden, um überhaupt Sammlungen zu schreiben. Wenn das Ziel darin besteht, alle Sammlungen (vermutlich auch solche außerhalb der Standardbibliothek) optional für einen Allokator parametrisch zu machen und Box nicht allokatorparametrisch ist, darf ein Sammlungsautor entweder Box nicht verwenden

Richtig, aber Box weiß nicht, dass Dealloc kein Op ist.

Warum sollte nicht angepasst werden, was C ++ unique_ptr tut?
Das heißt: Zeiger auf Allokator speichern, wenn er "statusbehaftet" ist, und nicht speichern, wenn der Allokator "zustandslos" ist
(zB globaler Wrapper um malloc oder mmap ).
Dies würde erfordern, das aktuelle Alloc -Traint in zwei Merkmale aufzuteilen: StatefulAlloc und StatelessAlloc .
Mir ist klar, dass es sehr unhöflich und unelegant ist (und wahrscheinlich hat es bereits jemand in früheren Diskussionen vorgeschlagen).
Trotz ihrer Uneleganz ist diese Lösung einfach und abwärtskompatibel (ohne Leistungseinbußen).

Ich denke, es wäre wirklich eine Schande, von Leuten zu verlangen, dass sie unsicheren Code verwenden, um überhaupt Sammlungen zu schreiben. Wenn das Ziel darin besteht, alle Sammlungen (vermutlich auch solche außerhalb der Standardbibliothek) für einen Allokator optional parametrisch zu machen und Box nicht allokatorparametrisch ist, darf ein Sammlungsautor Box entweder überhaupt nicht verwenden oder unsicheren Code verwenden (und Denken Sie daran, dass das Erinnern daran, Dinge immer freizugeben, eine der häufigsten Arten von Speichersicherheit in C und C ++ ist. Daher ist es schwierig, unsicheren Code zu finden. Das scheint ein unglücklicher Handel zu sein.

Ich befürchte, dass eine Implementierung eines Effekt- oder Kontextsystems, mit dem knotenbasierte Container wie Listen, Bäume usw. auf sichere Weise geschrieben werden können, zu lange dauern könnte (wenn dies im Prinzip möglich ist).
Ich habe keine Artikel oder akademischen Sprachen gesehen, die sich mit diesem Problem befassen (bitte korrigieren Sie mich, wenn solche Werke tatsächlich existieren).

Der Rückgriff auf unsafe bei der Implementierung knotenbasierter Container könnte daher zumindest kurzfristig ein notwendiges Übel sein.

@eucpp Beachten Sie, dass unique_ptr keinen Allokator speichert - es speichert einen Deleter :

Deleter muss eine FunctionObject- oder lvalue-Referenz auf ein FunctionObject oder eine lvalue-Referenz auf eine Funktion sein, die mit einem Argument vom Typ unique_ptr aufgerufen werden kann:: Zeiger`

Ich sehe dies als ungefähr gleichwertig mit der Bereitstellung von geteilten Alloc und Dealloc Merkmalen.

@cramertj Ja, du hast recht. Dennoch sind zwei Merkmale erforderlich - zustandsbehaftet und zustandslos Dealloc .

Wäre ein ZST Dealloc nicht ausreichend?

Am Dienstag, den 12. Juni 2018 um 15:08 Uhr Evgeniy Moiseenko [email protected]
schrieb:

@cramertj https://github.com/cramertj Ja, Sie haben Recht. Trotzdem zwei
Eigenschaften sind erforderlich - zustandsbehafteter und staatenloser Dealloc.

- -
Sie erhalten dies, weil Sie erwähnt wurden.
Antworte direkt auf diese E-Mail und sieh sie dir auf GitHub an
https://github.com/rust-lang/rust/issues/32838#issuecomment-396716689 ,
oder schalten Sie den Thread stumm
https://github.com/notifications/unsubscribe-auth/AEAJtWkpF0ofVc18NwbfV45G4QY6SCFBks5t8B_AgaJpZM4IDYUN
.

Wäre ein ZST Dealloc nicht ausreichend?

@remexre Ich nehme an, es würde :)

Ich wusste nicht, dass der Rost-Compiler ZST sofort unterstützt.
In C ++ wären zumindest einige Tricks zur Optimierung der leeren Basis erforderlich.
Ich bin ziemlich neu bei Rust und entschuldige mich für einige offensichtliche Fehler.

Ich glaube nicht, dass wir getrennte Eigenschaften für Staats- und Staatenlose brauchen.

Wenn Box mit einem Parameter vom Typ A wird, enthält es direkt einen Wert von A , keinen Verweis oder Zeiger auf A . Dieser Typ kann für einen zustandslosen (De-) Allokator die Größe Null haben. Oder A selbst kann so etwas wie eine Referenz oder ein Handle für einen Stateful Allocator sein, der von mehreren zugewiesenen Objekten gemeinsam genutzt werden kann. Anstelle von impl Alloc for MyAllocator möchten Sie vielleicht etwas wie impl<'r> Alloc for &'r MyAllocator tun

Übrigens würde ein Box , der nur weiß, wie man die Zuordnung aufhebt und nicht wie man sie zuweist, Clone nicht implementiert.

@SimonSapin Ich würde erwarten, dass für Clone ing erneut ein Allokator angegeben werden muss, genauso wie beim Erstellen eines neuen Box (das heißt, dies würde nicht mit Clone Merkmal).

@cramertj Wäre es nicht inkonsistent im Vergleich zu Vec und anderen Containern, die Clone implementieren?
Was sind die Nachteile des Speicherns der Instanz von Alloc in Box anstelle von Dealloc ?
Dann könnte Box Clone als auch clone_with_alloc implementieren.

Ich weiß nicht, dass die geteilten Eigenschaften Clone wirklich stark beeinflussen - der Impl würde einfach wie impl<T, A> Clone for Box<T, A> where A: Alloc + Dealloc + Clone { ... } aussehen.

@sfackler Ich wäre nicht gegen dieses Impl, aber ich würde auch erwarten, ein clone_into oder etwas zu haben, das einen bereitgestellten Allokator verwendet.

Wäre es sinnvoll, eine alloc_copy -Methode für Alloc ? Dies könnte verwendet werden, um schnellere Memcpy-Implementierungen ( Copy/Clone ) für große Zuordnungen bereitzustellen, z. B. durch Kopieren von Seiten beim Schreiben.

Das wäre ziemlich cool und trivial, um eine Standardimplementierung für bereitzustellen.

Was würde eine solche alloc_copy -Funktion verwenden? impl Clone for Box<T, A> ?

Ja, das Gleiche gilt für Vec .

Nach eingehender Betrachtung scheint es ein Ansatz zu sein, Copy-on-Write-Seiten innerhalb desselben Prozessbereichs zwischen hackig und unmöglich zu erstellen, zumindest wenn Sie dies mehr als eine Ebene tiefer tun möchten. alloc_copy wäre also kein großer Vorteil.

Stattdessen könnte eine allgemeinere Notluke von Nutzen sein, die zukünftige Spielereien im virtuellen Speicher ermöglicht. Das heißt, wenn eine Zuordnung groß ist, ohnehin von mmap unterstützt wird und zustandslos ist, könnte der Zuweiser versprechen, zukünftige Änderungen an der Zuordnung nicht zu bemerken. Der Benutzer kann diesen Speicher dann in eine Pipe verschieben, die Zuordnung aufheben oder ähnliche Dinge.
Alternativ könnte es einen dummen MMAP-All-the-Things-Allokator und eine Try-Transfer-Funktion geben.

Stattdessen eine allgemeinere Notluke, die zukünftigen virtuellen Speicher ermöglicht

Speicherzuweiser (malloc, jemalloc, ...) lassen Sie im Allgemeinen keine Art von Speicher von ihnen stehlen, und sie lassen Sie im Allgemeinen nicht abfragen oder ändern, welche Eigenschaften des Speichers sie besitzen. Was hat diese allgemeine Notluke mit Speicherzuordnungen zu tun?

Außerdem unterscheidet sich die Unterstützung des virtuellen Speichers zwischen den Plattformen erheblich, so dass für die effektive Nutzung des virtuellen Speichers häufig unterschiedliche Algorithmen pro Plattform erforderlich sind, häufig mit völlig unterschiedlichen Garantien. Ich habe einige tragbare Abstraktionen über den virtuellen Speicher gesehen, aber ich habe noch keine gesehen, die aufgrund ihrer "Portabilität" in einigen Situationen nicht so verkrüppelt war, dass sie unbrauchbar wurden.

Du hast recht. Ein solcher Anwendungsfall (ich habe hauptsächlich an plattformspezifische Optimierungen gedacht) lässt sich wahrscheinlich am besten mit einem benutzerdefinierten Allokator bedienen.

Irgendwelche Gedanken zur Composable Allocator API, die Andrei Alexandrescu in seiner CppCon-Präsentation beschrieben hat? Das Video ist auf YouTube hier verfügbar: https://www.youtube.com/watch?v=LIb3L4vKZ7U (er beginnt gegen 26:00 Uhr mit der Beschreibung seines vorgeschlagenen Designs, aber der Vortrag ist unterhaltsam genug, dass Sie ihn vielleicht lieber ansehen möchten) .

Es klingt nach der unvermeidlichen Schlussfolgerung, dass Sammlungsbibliotheken eine generische WRT-Zuordnung sein sollten und der App-Programmierer selbst in der Lage sein sollte, Allokatoren und Sammlungen auf der Baustelle frei zusammenzustellen.

Irgendwelche Gedanken zur Composable Allocator API, die Andrei Alexandrescu in seiner CppCon-Präsentation beschrieben hat?

Die aktuelle Alloc API ermöglicht das Schreiben von zusammensetzbaren Allokatoren (z. B. MyAlloc<Other: Alloc> ), und Sie können Merkmale und Spezialisierungen verwenden, um so ziemlich alles zu erreichen, was in Andreis Talk erreicht wird. Abgesehen von der "Idee", dass man dazu in der Lage sein sollte, kann so ziemlich nichts aus Andrei's Vortrag auf Rust zutreffen, da die Art und Weise, wie Andrei die API erstellt, von Anfang an auf uneingeschränkten Generika + SFINAE / statisch und Rusts Generika-System basiert ist völlig anders als dieser.

Ich möchte vorschlagen, den Rest der Layout -Methoden zu stabilisieren. Diese sind bereits mit der aktuellen globalen Allokator-API nützlich.

Sind das alle Methoden, die Sie meinen?

  • pub fn align_to(&self, align: usize) -> Layout
  • pub fn padding_needed_for(&self, align: usize) -> usize
  • pub fn repeat(&self, n: usize) -> Result<(Layout, usize), LayoutErr>
  • pub fn extend(&self, next: Layout) -> Result<(Layout, usize), LayoutErr>
  • pub fn repeat_packed(&self, n: usize) -> Result<Layout, LayoutErr>
  • pub fn extend_packed(&self, next: Layout) -> Result<(Layout, usize), LayoutErr>
  • pub fn array<T>(n: usize) -> Result<Layout, LayoutErr>

@gnzlbg Ja.

@Aanieu Klingt für mich in

von Allokatoren und Lebensdauern :

  1. (für Allokator-Impls): Das Verschieben eines Allokatorwerts darf seine ausstehenden Speicherblöcke nicht ungültig machen.

    Alle Clients können dies in ihrem Code annehmen.

    Wenn also ein Client einen Block von einem Allokator zuweist (nennen Sie ihn a1) und dann a1 an einen neuen Ort verschiebt (z. B. Vialet a2 = a1;), bleibt es für den Client einwandfrei, diesen Block über a2 freizugeben.

Bedeutet dies, dass ein Allocator muss Unpin ?

Guter Fang!

Da das Merkmal Alloc immer noch instabil ist, können wir die Regeln meiner Meinung nach immer noch ändern, wenn wir diesen Teil des RFC ändern möchten. Aber es ist in der Tat etwas zu beachten.

@gnzlbg Ja, ich bin mir der großen Unterschiede in den Generika-Systemen bewusst und dass nicht alles, was er beschreibt, in Rust auf die gleiche Weise implementiert werden kann. Ich arbeite jedoch seit dem Posten immer wieder an der Bibliothek und mache gute Fortschritte.

Bedeutet dies, dass ein Allokator Unpin ?

Das tut es nicht. Unpin handelt vom Verhalten eines Typs, wenn er in ein Pin ist. Es besteht keine besondere Verbindung zu dieser API.

Aber kann Unpin nicht verwendet werden, um die erwähnte Einschränkung durchzusetzen?

Eine weitere Frage zu dealloc_array : Warum gibt die Funktion Result ? In der aktuellen Implementierung kann dies in zwei Fällen fehlschlagen:

  • n ist Null
  • Kapazitätsüberlauf für n * size_of::<T>()

Zum ersten haben wir zwei Fälle (wie in der Dokumentation kann der Implementierer zwischen diesen wählen):

  • Die Zuordnung gibt Ok auf Null zurück n => dealloc_array sollte auch Ok .
  • Die Zuordnung gibt Err auf Null zurück n => Es gibt keinen Zeiger, der an dealloc_array .

Die zweite wird durch die folgende Sicherheitsbeschränkung sichergestellt:

Das Layout von [T; n] muss zu diesem Speicherblock

Dies bedeutet, dass wir dealloc_array mit dem gleichen n wie in der Zuordnung aufrufen müssen . Wenn ein Array mit n Elementen zugewiesen werden könnte, gilt n für T . Andernfalls wäre die Zuordnung fehlgeschlagen.

Bearbeiten: Zum letzten Punkt: Auch wenn usable_size einen höheren Wert als n * size_of::<T>() zurückgibt, ist dies immer noch gültig. Andernfalls verstößt die Implementierung gegen diese Merkmalsbeschränkung:

Die Größe des Blocks muss in den Bereich [use_min, use_max] , wobei:

  • [...]
  • use_max ist die Kapazität, die zurückgegeben wurde (oder hätte zurückgegeben werden sollen), als (wenn) der Block über einen Aufruf an alloc_excess oder realloc_excess zugewiesen wurde.

Dies gilt nur, da für das Merkmal ein unsafe impl erforderlich ist.

Zum ersten haben wir zwei Fälle (wie in der Dokumentation kann der Implementierer zwischen diesen wählen):

  • Die Zuordnung gibt Ok auf null n

Woher haben Sie diese Informationen?

Alle Alloc::alloc_ -Methoden in den Dokumenten geben an, dass das Verhalten von Zuordnungen mit der Größe Null unter ihrer "Sicherheits" -Klausel undefiniert ist.

Dokumente von core::alloc::Alloc (hervorgehobene relevante Teile):

Ein Hinweis zu Typen mit der Größe Null und Layouts mit der Größe Null: Viele Methoden im Merkmal Alloc geben an, dass Zuweisungsanforderungen eine Größe ungleich Null haben müssen, da sonst undefiniertes Verhalten auftreten kann.

  • Einige übergeordnete Zuweisungsmethoden ( alloc_one , alloc_array ) sind für Typen mit der Größe Null gut definiert und können sie optional unterstützen : Es bleibt dem Implementierer überlassen, ob Err oder um Ok mit einem Zeiger zurückzugeben.
  • Wenn eine Alloc -Implementierung in diesem Fall Ok zurückgibt (dh der Zeiger kennzeichnet einen unzugänglichen Block mit der Größe Null), muss dieser zurückgegebene Zeiger als "aktuell zugewiesen" betrachtet werden. Bei einem solchen Allokator müssen alle Methoden, die aktuell zugewiesene Zeiger als Eingaben verwenden, diese Zeiger mit der Größe Null akzeptieren, ohne ein undefiniertes Verhalten

  • Mit anderen Worten, wenn ein Zeiger mit der Größe Null aus einem Allokator herausfließen kann, muss dieser Allokator diesen Zeiger ebenfalls akzeptieren, der in seine Freigabe- und Neuzuweisungsmethoden zurückfließt .

Eine der Fehlerbedingungen von dealloc_array ist also definitiv verdächtig:

/// # Safety
///
/// * the layout of `[T; n]` must *fit* that block of memory.
///
/// # Errors
///
/// Returning `Err` indicates that either `[T; n]` or the given
/// memory block does not meet allocator's size or alignment
/// constraints.

Wenn [T; N] die Zuordnungsgrößen- oder Ausrichtungsbeschränkungen nicht erfüllt, passt AFAICT nicht zum Speicherblock der Zuordnung, und das Verhalten ist undefiniert (gemäß der Sicherheitsklausel).

Die andere Fehlerbedingung lautet "Gibt beim arithmetischen Überlauf immer Err ." Das ist ziemlich allgemein. Es ist schwer zu sagen, ob es sich um eine nützliche Fehlerbedingung handelt. Für jede Alloc Trait-Implementierung könnte man sich eine andere einfallen lassen, die eine Arithmetik ausführen könnte, die theoretisch umbrechen könnte, also 🤷‍♂️


Dokumente von core::alloc::Alloc (hervorgehobene relevante Teile):

Tatsächlich. Ich finde es seltsam, dass so viele Methoden (z. B. Alloc::alloc ) angeben, dass Zuordnungen mit der Größe Null undefiniertes Verhalten sind, aber dann stellen wir Alloc::alloc_array(0) implementierungsdefiniertes Verhalten zur Verfügung. In gewissem Sinne ist Alloc::alloc_array(0) ein Lackmustest, um zu überprüfen, ob ein Allokator Allokationen mit der Größe Null unterstützt oder nicht.

Wenn [T; N] die Zuordnungsgrößen- oder Ausrichtungsbeschränkungen nicht erfüllt, passt AFAICT nicht zum Speicherblock der Zuordnung, und das Verhalten ist undefiniert (gemäß der Sicherheitsklausel).

Ja, ich denke, diese Fehlerbedingung kann gelöscht werden, da sie redundant ist. Entweder brauchen wir die Sicherheitsklausel oder eine Fehlerbedingung, aber nicht beide.

Die andere Fehlerbedingung lautet "Gibt beim arithmetischen Überlauf immer Err ." Das ist ziemlich allgemein. Es ist schwer zu sagen, ob es sich um eine nützliche Fehlerbedingung handelt.

IMO, es wird durch die gleiche Sicherheitsklausel wie oben geschützt; Wenn die Kapazität von [T; N] überlaufen würde, passt dieser Speicherblock nicht zur Freigabe. Vielleicht könnte @pnkfelix darauf näher eingehen ?

In gewissem Sinne ist Alloc::alloc_array(1) ein Lackmustest, um zu überprüfen, ob ein Allokator Allokationen mit der Größe Null unterstützt oder nicht.

Meinten Sie Alloc::alloc_array(0) ?

IMO, es wird durch die gleiche Sicherheitsklausel wie oben geschützt; Wenn die Kapazität von [T; N] überlaufen würde, passt dieser Speicherblock nicht zur Freigabe.

Beachten Sie, dass dieses Merkmal von Benutzern für ihre eigenen benutzerdefinierten Allokatoren implementiert werden kann und dass diese Benutzer die Standardimplementierungen dieser Methoden überschreiben können. Wenn Sie also überlegen, ob dies Err für einen arithmetischen Überlauf zurückgeben soll oder nicht, sollten Sie sich nicht nur darauf konzentrieren, was die aktuelle Standardimplementierung der Standardmethode bewirkt, sondern auch darüber nachdenken, was für Benutzer, die diese für andere implementieren, sinnvoll sein könnte Allokatoren.

Meinten Sie Alloc::alloc_array(0) ?

Ja Entschuldigung.

Beachten Sie, dass dieses Merkmal von Benutzern für ihre eigenen benutzerdefinierten Allokatoren implementiert werden kann und dass diese Benutzer die Standardimplementierungen dieser Methoden überschreiben können. Wenn man also überlegt, ob dies Err für einen arithmetischen Überlauf zurückgeben soll oder nicht, sollte man sich nicht nur darauf konzentrieren, was die aktuelle Standardimplementierung der Standardmethode bewirkt, sondern auch darüber nachdenken, was für Benutzer, die diese für andere implementieren, sinnvoll sein könnte Allokatoren.

Ich verstehe, aber für die Implementierung von Alloc sind unsafe impl Alloc erforderlich, und die Implementierer müssen die unter https://github.com/rust-lang/rust/issues/32838#issuecomment -467093527 genannten Sicherheitsregeln befolgen .

Jede API, die hier für ein Tracking-Problem angezeigt wird, ist das Merkmal Alloc oder steht im Zusammenhang mit dem Merkmal Alloc . @ rust-lang / libs, halten Sie es für nützlich, dies zusätzlich zu https://github.com/rust-lang/rust/issues/42774 offen zu halten

Einfache Hintergrundfrage: Was ist die Motivation für die Flexibilität bei ZSTs? Es scheint mir, dass wir, da wir zur Kompilierungszeit wissen, dass ein Typ eine ZST ist, sowohl die Zuordnung (um einen konstanten Wert zurückzugeben) als auch die Freigabe vollständig optimieren können. Angesichts dessen scheint es mir, dass wir eines der folgenden sagen sollten:

  • Es ist immer Sache des Implementierers, ZSTs zu unterstützen, und sie können Err für ZSTs nicht zurückgeben
  • Es ist immer UB, ZSTs zuzuweisen, und es liegt in der Verantwortung des Anrufers, in diesem Fall einen Kurzschluss zu verursachen
  • Es gibt eine Art alloc_inner -Methode, die Aufrufer implementieren, und eine alloc -Methode mit einer Standardimplementierung, die den Kurzschluss ausführt. alloc muss ZSTs unterstützen, aber alloc_inner darf NICHT für eine ZST aufgerufen werden (dies ist nur, damit wir die Kurzschlusslogik an einer einzigen Stelle - in der Merkmalsdefinition - der Reihe nach hinzufügen können um den Implementierern etwas Boilerplate zu ersparen)

Gibt es einen Grund, warum die Flexibilität, die wir mit der aktuellen API haben, benötigt wird?

Gibt es einen Grund, warum die Flexibilität, die wir mit der aktuellen API haben, benötigt wird?

Es ist ein Kompromiss. Möglicherweise wird das Alloc-Merkmal häufiger verwendet als implementiert. Daher ist es möglicherweise sinnvoll, die Verwendung von Alloc durch die integrierte Unterstützung von ZSTs so einfach wie möglich zu gestalten.

Dies würde bedeuten, dass sich Implementierer des Alloc-Merkmals darum kümmern müssen, aber was für mich noch wichtiger ist, dass diejenigen, die versuchen, das Alloc-Merkmal weiterzuentwickeln, ZSTs bei jeder API-Änderung berücksichtigen müssen. Es verkompliziert auch die Dokumente der API, indem erklärt wird, wie ZSTs behandelt werden (oder sein könnten, wenn sie "implementierungsdefiniert" sind).

C ++ - Allokatoren verfolgen diesen Ansatz, bei dem der Allokator versucht, viele verschiedene Probleme zu lösen. Dies machte sie nicht nur schwieriger zu implementieren und weiterzuentwickeln, sondern auch für Benutzer schwieriger zu verwenden, da all diese Probleme in der API interagieren.

Ich denke, dass der Umgang mit ZSTs und das Zuweisen / Freigeben von Rohspeicher zwei orthogonale und unterschiedliche Probleme sind. Daher sollten wir die Alloc-Trait-API einfach halten, indem wir sie einfach nicht behandeln.

Benutzer von Alloc wie libstd müssen ZSTs verarbeiten, z. B. für jede Sammlung. Das ist definitiv ein Problem, das es wert ist, gelöst zu werden, aber ich denke nicht, dass das Alloc-Merkmal der richtige Ort dafür ist. Ich würde erwarten, dass ein Dienstprogramm, das dieses Problem löst, notgedrungen in libstd auftaucht, und wenn dies passiert, können wir möglicherweise versuchen, ein solches Dienstprogramm auf RFC zu übertragen und es in std :: heap verfügbar zu machen.

Das klingt alles vernünftig.

Ich denke, dass der Umgang mit ZSTs und das Zuweisen / Freigeben von Rohspeicher zwei orthogonale und unterschiedliche Probleme sind. Daher sollten wir die Alloc-Trait-API einfach halten, indem wir sie einfach nicht behandeln.

Bedeutet das nicht, dass die API explizit keine ZSTs verarbeiten soll, anstatt implementierungsdefiniert zu sein? IMO, ein "nicht unterstützter" Fehler ist zur Laufzeit nicht sehr hilfreich, da die überwiegende Mehrheit der Anrufer keinen Fallback-Pfad definieren kann und daher davon ausgehen muss, dass ZSTs ohnehin nicht unterstützt werden. Scheint sauberer zu sein, nur die API zu vereinfachen und zu erklären, dass sie niemals unterstützt werden.

Würde die Spezialisierung von alloc Benutzern verwendet, um ZST zu handhaben? Oder nur if size_of::<T>() == 0 Schecks?

Würde die Spezialisierung von alloc Benutzern verwendet, um ZST zu handhaben? Oder nur if size_of::<T>() == 0 Schecks?

Letzteres sollte ausreichen; Die entsprechenden Codepfade würden beim Kompilieren trivial entfernt.

Bedeutet das nicht, dass die API explizit keine ZSTs verarbeiten soll, anstatt implementierungsdefiniert zu sein?

Für mich ist eine wichtige Einschränkung, dass die Alloc -Methoden davon ausgehen können, dass die an sie übergebenen Layout nicht null sind, wenn wir Zuweisungen mit der Größe Null verbieten.

Es gibt mehrere Möglichkeiten, dies zu erreichen. Eine Möglichkeit wäre, allen Alloc -Methoden eine weitere Safety -Klausel hinzuzufügen, die besagt, dass das Verhalten undefiniert ist, wenn Layout Größe Null hat.

Alternativ könnten wir Layout s mit der Größe Null verbieten, dann muss Alloc nichts über Zuweisungen mit der Größe Null sagen, da dies nicht sicher passieren kann, aber dies hätte einige Nachteile.

Zum Beispiel bauen einige Typen wie HashMap die Layout aus mehreren Layout , und während die endgültigen Layout möglicherweise nicht null sind, ist die Zwischenprodukte könnten sein (z. B. in HashSet ). Diese Typen müssten also "etwas anderes" verwenden (z. B. einen LayoutBuilder -Typ), um ihre endgültigen Layout s aufzubauen, und für einen Scheck "oder eine Größe ungleich Null" bezahlen (oder verwenden) eine _unchecked ) Methode bei der Konvertierung in Layout .

Würde die Spezialisierung von Allokationsbenutzern verwendet, um ZST zu handhaben? Oder nur wenn size_of ::() == 0 Schecks?

Wir können uns noch nicht auf ZSTs spezialisieren. Im Moment verwendet der gesamte Code size_of::<T>() == 0 .

Es gibt mehrere Möglichkeiten, dies zu erreichen. Eine Möglichkeit wäre, allen Alloc -Methoden eine weitere Safety -Klausel hinzuzufügen, die besagt, dass das Verhalten undefiniert ist, wenn Layout Größe Null hat.

Es wäre interessant zu überlegen, ob es Möglichkeiten gibt, dies zu einer Garantie für die Kompilierungszeit zu machen, aber selbst ein debug_assert dass das Layout nicht null ist, sollte ausreichen, um 99% der Fehler zu erkennen.

Ich habe den Diskussionen über Allokatoren keine Aufmerksamkeit geschenkt, also tut mir das leid. Aber ich habe mir lange gewünscht, dass der Allokator Zugriff auf die Art des Wertes hat, den er zuweist. Möglicherweise gibt es Allokator-Designs, die dies verwenden könnten.

Dann hätten wir wahrscheinlich die gleichen Probleme wie C ++ und es ist Allokator-API.

Aber ich habe mir lange gewünscht, dass der Allokator Zugriff auf die Art des Wertes hat, den er zuweist. T.

Wofür brauchst du das?

@gnzblg @brson Heute hatte ich einen möglichen Anwendungsfall, um etwas über die Art des zugewiesenen Werts zu wissen.

Ich arbeite an einem globalen Allokator, der zwischen drei zugrunde liegenden Allokatoren umgeschaltet werden kann - einem lokalen Thread-Allokator, einem globalen mit Sperren und einem für Coroutinen zu verwendenden. Die Idee ist, eine Coroutine, die eine Netzwerkverbindung darstellt, auf ein Maximum zu beschränken Umfang der dynamischen Speichernutzung (da keine Zuordnungen in Sammlungen gesteuert werden können, insbesondere im Code von Drittanbietern) *.

Es wäre möglicherweise praktisch zu wissen, ob ich einen Wert zuordne, der sich über Threads (z. B. Arc) bewegt, gegenüber einem Wert, der dies nicht tut. Oder vielleicht auch nicht. Aber es ist ein mögliches Szenario. Im Moment hat der globale Allokator einen Schalter, mit dem der Benutzer ihm mitteilt, von welchem ​​Allokator er Allokationen vornehmen soll (nicht für die Neuzuweisung oder kostenlos erforderlich; wir können uns nur die Speicheradresse dafür ansehen).

* [Außerdem kann ich, wo immer möglich, den lokalen NUMA-Speicher ohne Sperren verwenden und mit einem 1-Kern-1-Thread-Modell die gesamte Speichernutzung begrenzen].

@raphaelcohn

Ich arbeite an einem globalen Allokator

Ich glaube nicht, dass dies auf das Merkmal GlobalAlloc zutreffen würde (oder könnte), und das Merkmal Alloc verfügt bereits über generische Methoden, die Typinformationen verwenden können (z. B. alloc_array<T>(1) weist ein einzelnes T , wobei T der tatsächliche Typ ist, sodass der Allokator den Typ bei der Zuweisung berücksichtigen kann. Ich denke, es wäre für die Zwecke dieser Diskussion nützlicher, tatsächlich Code zu sehen, der Allokatoren implementiert, die Typinformationen verwenden. Ich habe kein gutes Argument darüber gehört, warum diese Methoden Teil eines generischen Allokatormerkmals sein müssen, anstatt nur Teil der Allokator-API oder eines anderen Allokatormerkmals zu sein.

Ich denke, es wäre auch sehr interessant zu wissen, welche der durch Alloc parametrisierten Typen Sie mit Allokatoren kombinieren möchten, die Typinformationen verwenden, und was Sie als Ergebnis erwarten.

AFAICT, der einzig interessante Typ dafür wäre Box da er direkt T zuweist. So ziemlich alle anderen Typen in std weisen niemals T , sondern einen privaten internen Typ, von dem Ihr Allokator nichts wissen kann. Zum Beispiel könnten Rc und Arc (InternalRefCounts, T) , List / BTreeSet / usw. interne Knotentypen zuweisen, Vec / Deque / ... ordnen Arrays von T s zu, aber nicht T s selbst usw.

Für Box und Vec könnten wir abwärtskompatibel ein BoxAlloc und ein ArrayAlloc Merkmal mit Blanket Impls für Alloc hinzufügen, die Allokatoren könnten Spezialisieren Sie sich darauf, wie sich diese verhalten, wenn es jemals notwendig ist, diese Probleme generisch anzugreifen. Aber gibt es einen Grund, warum es keine praktikable Lösung ist, eigene MyAllocBox und MyAllocVec Typen bereitzustellen, die sich mit Ihrem Allokator zur Nutzung von Typinformationen verschwören?

Da wir jetzt ein dediziertes Repository für die Allokator-Arbeitsgruppe haben und die Liste im OP veraltet ist, kann dieses Problem geschlossen werden, um Diskussionen und Nachverfolgung dieser Funktion an einem Ort zu führen.

Ein guter Punkt @TimDiekmann! Ich werde fortfahren und dies zugunsten von Diskussionsthreads in diesem Repository schließen.

Dies ist immer noch das Tracking-Problem, auf das einige #[unstable] -Attribute verweisen. Ich denke, es sollte nicht geschlossen werden, bis diese Funktionen entweder stabilisiert oder veraltet sind. (Oder wir könnten die Attribute ändern, um auf ein anderes Problem hinzuweisen.)

Ja, instabile Funktionen, auf die in git master verwiesen wird, sollten definitiv ein offenes Tracking-Problem haben.

Einverstanden. Außerdem wurde ein Hinweis und ein Link zum OP hinzugefügt.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen