📢 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
.
struct Layout
, trait Allocator
und Standardimplementierungen in alloc
crate (https://github.com/rust-lang/rust/pull/42313)alloc
crate, aber Layout
/ Allocator
_kann_ in libcore
...) (https://github.com/rust-lang/rust/pull/42313)Layout
auf Überlauffehler (möglicherweise nach Bedarf auf overflowing_add und overflowing_mul umschalten).realloc_in_place
durch grow_in_place
und shrink_in_place
( Kommentar ) (https://github.com/rust-lang/rust/pull/42313)fn dealloc
. (Siehe Diskussion zu Allocator RFC und Global Allocator RFC und Merkmal Alloc
PR .)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.AllocErr
stattdessen Error
? ( Kommentar )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)alloc_system
ist bei großen Ausrichtungen fehlerhaft (z. B. eine Ausrichtung von 1 << 32
) https://github.com/rust-lang/rust/issues/30170 # 43217Layout
eine fn stride(&self)
-Methode bereitstellen? (Siehe auch https://github.com/rust-lang/rfcs/issues/1397, https://github.com/rust-lang/rust/issues/17027)Allocator::owns
als Methode? https://github.com/rust-lang/rust/issues/44302Status 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 {
// ...
}
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:
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.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.
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. ObjectAllocator
Auf 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 ObjectAllocator Verantwortung. 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 vondealloc
unter Windows verwendetalign
, 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
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.
allocator.alloc(Layout::from_size_align(…))
?<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:
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.Error
um nur rohe Zeiger zu verwenden.u8
in der Benutzeroberfläche in void
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
inError
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:
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:
Die Startadresse des Blocks muss auf
layout.align()
.Die Größe des Blocks muss in den Bereich
[use_min, use_max]
, wobei:
use_min
istself.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
oderrealloc_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]
undEine 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 mitptr
)
Derzeit über einen Allokatora
zugewiesen, dann ist es legal zu
Verwenden Sie dieses Layout, um die Zuordnung aufzuheben, dha.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 inBox<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:
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:
alloc_zeroed
zu den stabilisierten Methoden hinzu, andernfalls haben Sie dieselbe Signatur wie alloc
.*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 .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.
std::unique_ptr
ist generisch für einen "Deleter" .
@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
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)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
.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:
Excess
überhaupt nicht zurück: https://github.com/rust-lang/rust/pull/45514grow_in_place_excess
und alloc_zeroed_excess
,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:
_excess
im Rost-Ökosystem_excess
in der Rost-StandardbibliothekVec
und String
können die _excess
API ordnungsgemäß in der Rost std
Bibliothek verwendenDie _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
Diese beiden Argumente erscheinen mir plausibel:
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 .Ihr Argument:
_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.
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!).
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ängen
,[T; n]
hat eine Größe vonn * 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
Result<T, A::Err>
für die Implementierung von T
unwrap
oder irgendetwas anderes teilweiseoom(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: -
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:
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.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.
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.
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.
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.
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:
Vec
komplexer, was sich auf Benutzer auswirken kann, die diese Funktion nicht verwendenVec
würde komplexer werden, da das Verhalten einiger Operationen vom Allokator abhängen würde.Ich denke, diese Kosten sind nicht zu vernachlässigen.
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.
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 Sinnpush
ist O(1)
anstelle von amortisierten O(1)
.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.
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:
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
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.
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:
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.").
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:
Allocator
Objekt im Inneren wie Rust RawVec
tut .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:
_excess
-MethodenEs 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:
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
Vec
wie es heute existiertDaher 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:
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:
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:
'static
.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.
@alexreg Siehe https://github.com/rust-lang/rfcs/pull/2321
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ält
und 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
Ich sehe keinen großen Vorteil darin, zB das Argument von dealloc
in NonNull
. Ich sehe ungefähr zwei Verwendungsklassen dieser API:
alloc
aufrufen, speichern Sie den zurückgegebenen Zeiger irgendwo und übergeben Sie den gespeicherten Zeiger nach einer Weile an dealloc
.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 einNonNull
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):
AllocErr
-Typ angegeben werden. Dies kann nützlich sein, um !
, oder, da AllocErr jetzt leer ist, optional mehr Informationen als "fehlgeschlagen" zu übermitteln.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:
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:
raw_vec
verwendet eine Mischung aus alloc_array
, alloc
/ alloc_zeroed
, verwendet aber nur dealloc
.alloc_array
/ dealloc_array
kann man ein Vec
sicher in ein Box
umwandeln, das dann dealloc
.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 sollen
nimm 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 einDeAlloc
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-Opdealloc
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 InternaLinkedList
Rohzeiger, die mit der InstanzAlloc
zugewiesen und freigegeben werden, die im ObjektLinkedList
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 :
(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 Nulln * size_of::<T>()
Zum ersten haben wir zwei Fälle (wie in der Dokumentation kann der Implementierer zwischen diesen wählen):
Ok
auf Null zurück n
=> dealloc_array
sollte auch Ok
.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 analloc_excess
oderrealloc_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 nulln
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, obErr
oder umOk
mit einem Zeiger zurückzugeben.Wenn eine
Alloc
-Implementierung in diesem FallOk
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 VerhaltenMit 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:
Err
für ZSTs nicht zurückgebenalloc_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 nurif 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 weitereSafety
-Klausel hinzuzufügen, die besagt, dass das Verhalten undefiniert ist, wennLayout
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.
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.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!).