VRAM-beheer Deel 2: Voorbij de limieten van fysiek VRAM

Ter viering hiervan kijken we wat dieper naar één zin uit mijn vorige bericht: "[Games] zouden veel stabieler moeten presteren — zolang de game zelf niet meer VRAM gebruikt dan je daadwerkelijk hebt."

Men zou zich dan kunnen afvragen: wat als ze dat wél doen? De algemene verwachting is dat je op dat moment kansloos bent. Games zullen constant crashen, de prestaties kelderen naar onspeelbare niveaus en een goede game-ervaring wordt onmogelijk.

Maar is dat echt een onvermijdelijk feit? Wat maakt het precies zo problematisch als het VRAM opraakt? En, belangrijker nog: hoe kunnen we ervoor zorgen dat dit zo min mogelijk impact heeft?

Verwachtingen scheppen

In theorie zou het opraken van VRAM uitsluitend een prestatieprobleem moeten zijn, geen stabiliteitsprobleem. Ondersteuning voor het overcommitten van VRAM bestaat al sinds de eerste GPU-drivers: als de driver VRAM overcommitteert, mag je in principe zoveel VRAM aanvragen als je wilt, en je krijgt zoveel als de kernel-driver in het fysieke geheugen van de GPU kan passen.

Wat de prestaties betreft, is de reden voor de slechte performance bij VRAM-tekort vrij simpel. Zodra een game meer VRAM aanvraagt dan er fysiek aanwezig is, moet een deel van het geheugen van de game worden verplaatst of verwijderd (evicted) naar het CPU-RAM. Voor de GPU is het benaderen van CPU-RAM veel trager dan VRAM: niet alleen is CPU-RAM in het algemeen trager dan het VRAM van een dedicated GPU, maar alle geheugenacties moeten ook via de PCI-bus verlopen. De PCI-bus voegt latentie toe en is doorgaans ook de beperkende factor in bandbreedte bij het ophalen van gegevens uit het CPU-geheugen.

Door de beperkingen van de PCI-snelheid zijn er onvermijdelijke prestatiebeperkingen bij het overcommitten van VRAM. Uitgaande van een GPU die is aangesloten via een PCIe 4.0x16-verbinding, krijg je iets minder dan 32 GiB/s bandbreedte. Per milliseconde kan die PCIe-bus ongeveer 32,2 MiB aan data overbrengen. Voor een minimale framerate van 30 frames per seconde (33,3 ms per frame) is de absolute maximale hoeveelheid data die de GPU kan benaderen ongeveer 1.075,5 MiB, net iets meer dan 1 GiB aan data. Met andere woorden: als er zoveel geheugen is verplaatst dat de GPU in één enkel frame meer dan 1 GiB uit het verplaatste geheugen moet ophalen, is het simpelweg onmogelijk om 30 FPS te halen.

Niet al het geheugen is gelijk

Tegelijkertijd is het lezen van een kleine hoeveelheid CPU-geheugen op de GPU niet direct een doodvonnis voor de prestaties. Sterker nog, GPU-drivers besluiten soms om zaken als command buffer-data en gerelateerde allocaties in het CPU-RAM te laten leven, zelfs als er voldoende VRAM beschikbaar is. Wanneer de GPU deze commando's uitvoert, moet hij het CPU-geheugen benaderen, en toch verloopt dit in deze gevallen volledig probleemloos. Waarom zijn deze toegangen dan acceptabel, terwijl het opraken van VRAM catastrofaal lijkt?¹

Een factor die de berekening aanzienlijk beïnvloedt, is caching. Aangezien de latentie bij een cache hit hetzelfde is, ongeacht of het gecachte geheugen in het CPU- of GPU-geheugen staat, kunnen de hoge initiële kosten van het ophalen via de PCI-bus (tot op zekere hoogte) worden gecompenseerd door cache hits. We kunnen de latentieverschillen tussen CPU-RAM en VRAM schatten door microbenchmarks te schrijven die de toegangslatentie voor verschillende buffergroottes meten (met een vijandig toegangspatroon om de cache-hitrates zoveel mogelijk te minimaliseren). Het resultaat (vastgelegd op RDNA3) ziet er als volgt uit:

Zoals verwacht is de toegangslatentie voor geheugen ondersteund door CPU-RAM en geheugen ondersteund door VRAM exact hetzelfde als de buffer in de L2 (of een hogere cachelaag) past, omdat de data in beide gevallen direct uit de cache wordt opgehaald. Bij een grootte van 6 MB (de L2-cachegrootte op RDNA3) stijgt de latentie van het CPU-geheugen naar ongeveer 2400 cycli per toegang, terwijl de latentie van het apparaatgeheugen in dezelfde buurt blijft.

Let op dat VRAM-toegangen ook via de Infinity Cache lopen, maar CPU-geheugentoegangen niet (zij raken direct de PCIe bij een L2-miss). Ik vermoed dat dit komt omdat de Infinity Cache direct bovenop het VRAM zit; elke toegang die het VRAM niet raakt, bereikt dus ook de Infinity Cache niet.

Natuurlijk wordt geheugen niet direct gecached, dus de eerste toegang zal nog steeds een aanzienlijk hogere latentie hebben. Ook het missen van de Infinity Cache doet pijn: PCIe-ophalingen lijken ongeveer 7,3x zoveel latentie te hebben als een Infinity Cache-hit, en ongeveer 4,6x zoveel latentie als een ophaalactie uit VRAM.

Deze verhoogde latentie vereist zeer hoge cache-hitrates om de kosten van de PCIe-verbinding volledig te compenseren. Dit betekent dat er slechts een kleine set use-cases is waarbij het gebruik van CPU-geheugen zulke minimale vertragingen oplevert dat je er actief voor zou kiezen boven VRAM. Wanneer je geheugen uit VRAM verwijdert, zal er bijna onvermijdelijk in zekere mate sprake zijn van tragere prestaties.

Toch is er geheugen waarbij verwijdering zwaarder weegt en geheugen waarbij het minder effect heeft op de algehele performance. Geheugen dat op een cache-vriendelijke manier wordt benaderd, heeft minder last van de vertraging van CPU-RAM. Als de toegangspatronen niet cache-vriendelijk zijn, maar het geheugen niet vaak wordt benaderd, kan het ook prima gaan. Er kunnen veel geheugenallocaties zijn waarbij de GPU slechts een klein deel van de totale allocatiegrootte benadert. Als deze allocaties worden verwijderd, verplaats je wellicht meerdere GiB aan data, maar blijf je nog steeds ruim onder de harde limiet van 1 GiB aan data die daadwerkelijk per frame wordt benaderd.

Al deze variabelen maken het verrassend moeilijk om te voorspellen hoe de prestaties in de praktijk uitpakken bij het verwijderen van geheugen. Kortom: afhankelijk van hoeveel het verplaatste geheugen wordt benaderd en hoe goed deze toegangen cachen, zou je wel eens VRAM tekort kunnen komen zonder de prestaties (volledig) te ruïneren.

De realiteit onder ogen zien

We hebben nu theoretisch bepaald dat performante VRAM-overcommitment mogelijk is. Geweldig! Laten we SteamOS opstarten, een game starten en de instellingen omhoog schroeven—

radv/amdgpu: Not enough memory for command submission.

Oeps.

Het blijkt dat het in de praktijk opraken van VRAM gepaard gaat met veel stabiliteitsproblemen. Deze fout is echter niet zomaar een reguliere "kon niet alloceren, out of memory"-fout. Let op dat het bericht specifiek klaagt over de command submission. RADV print dit bericht wanneer de kernel -ENOMEM teruggeeft bij het proberen in te dienen van commando's², terwijl het enkel indienen van commando's geen nieuwe resources alloceert. Alle command buffers waren vooraf gealloceerd, en die allocatie was duidelijk geslaagd.

Ondanks dat al het geheugen succesvol was gealloceerd, resulteert het gebruik ervan in een GPU-submission plotseling in "out of memory"-fouten. Tijd voor een nieuw kernel-avontuur.

De gruwelen van kernel-locking

Een van de dingen die de amdgpu-driver bij elke submission moet doen, voordat hij de GPU kan aansturen om commando's uit te voeren, is controleren of al het geheugen dat potentieel door de GPU-commando's wordt geraadpleegd, toegankelijk is. Met moderne bindless graphics API's moet je ervan uitgaan dat al het gealloceerde geheugen op een bepaald moment kan worden geraadpleegd. Daarom probeert amdgpu ervoor te zorgen dat al het gealloceerde geheugen ook toegankelijk is.

Elke geheugenallocatie bevat informatie over welk type geheugen (voor ons hier: systeem-RAM of GPU-VRAM) geschikt is voor toegang. De meeste allocaties kunnen vanuit beide types worden benaderd. Sommige allocaties moeten echter uitsluitend in VRAM worden geplaatst. Als deze allocaties naar het systeem-RAM zijn verplaatst omdat een andere applicatie in de tussentijd VRAM heeft gealloceerd, moet amdgpu ze terugverplaatsen naar VRAM. Omdat er helemaal geen vrij VRAM beschikbaar is, vereist het terugplaatsen van de allocatie dat er iets anders wordt verwijderd. Om redenen die we nog moeten ontdekken, mislukte dat en rapporteerde de kernel een out-of-memory conditie.

Om uit te leggen waarom het willekeurig verwijderen van iets mislukt, moeten we kijken naar hoe de kernel (CPU-zijde) locking afhandelt voor GPU-allocaties. Om een geheugenallocatie te verwijderen, moet je een lock verwerven die bij die allocatie hoort. Tijdens een submission moet je echter ook elke allocatie locken die in die submission wordt geraadpleegd, om te voorkomen dat een andere applicatie de allocatie verplaatst terwijl je bezig bent met het voorbereiden van GPU-werk. Als een andere GPU-submission gelijktijdig hetzelfde doet, kun je in deze situatie terechtkomen:

Als de ene submit een allocatie wil verwijderen die de andere submit al heeft gelocked, maar die andere submit moet ook een allocatie van de eerste locken om vooruitgang te boeken, hebben we een schoolvoorbeeld van een ABBA-deadlock.

De kernel kan deadlocks echter detecteren en oplossen. De kernel koppelt locking-operaties aan een "transactie". Als twee transacties zouden deadlocken, wordt één van de transacties gemarkeerd als "wounded". De volgende keer dat deze een lock probeert te verwerven, wordt de fout -EDEADLCK teruggegeven. Deze fout vraagt de transactie om te worden afgebroken: alle verworven locks moeten worden vrijgegeven en de transactie wordt vanaf nul herstart. In de context van command submission betekent dit dat de driver het proces van het controleren van alle geheugenallocaties opnieuw start.

Waar is dan de addertje onder het gras? Er is geen. Deze aanpak is rotsvast en werkt erg goed. Althans, zolang het overal correct is geïmplementeerd.

In het graphics-subsyteem worden de interne details van de wound-abort-retry loop geabstraheerd via een kleine helper-bibliotheek genaamd drmexec. In plaats van handmatig bij te houden welke allocaties zijn gelocked, gebruik je simpelweg de drmexeclockobj helper. Als je de locking-code in TTM bestudeert (de gedeelde Linux GPU-geheugenbeheerslaag), valt een diep gebrek aan het gebruik van drm_exec op. Er staat zelfs een comment die opmerkt dat -EDEADLCK ertoe zal leiden dat verwijdering mislukt. Daar hebben we ons probleem! Zodra deze deadlock-conditie optreedt door intense geheugendruk tijdens command submission, geeft de kernel op en weigert de submission in plaats van deze te herstarten.

Er bestonden al patchsets om de drm_exec helper in TTM te implementeren, maar die zijn nooit doorgevoerd. Mijn taak was om de patchset te rebasen op mijn kernelversie en de resterende bugs op te lossen. Dit kostte een week van intense frustratie waarbij games willekeurig hingen na 3 minuten zware VRAM-druk.

Nu het opraken van VRAM in ieder geval je apps niet meer willekeurig laat crashen, kunnen we de instellingen echt omhoog schroeven en naar de performance kijken. Het initiële resultaat gaf me een prestatiegrafiek die absoluut verschrikkelijk was.

Wacht eens, waar is alle performance gebleven?

Om uit te zoeken waarom de prestaties zo slecht zijn, moet ik begrijpen wat het systeem precies doet dat zo traag is. Hiervoor gebruik ik gpuvis, dat kernel-tracepoints gebruikt om een tijdlijn van gebeurtenissen op te bouwen.

Uit de tijdlijn blijkt dat de meeste tijd niet wordt besteed aan de submission zelf (de gfx_0.0.0 activiteit), maar aan het verplaatsen van geheugen ter voorbereiding op die submission (sdma0 activiteit).

Wanneer je de event-lijst van gpuvis gebruikt met een filter voor een specifiek buffer-object, wordt duidelijk dat concurrerende processen (in dit geval gamescope en de game zelf) constant om de beurt hetzelfde stukje geheugen verwijderen en weer terugplaatsen. Dat is erg slecht! Dit doet denken aan een probleem dat ik in mijn eerste blogpost beschreef:

Normaal gesproken wisselen twee concurrerende applicaties om de beurt het uitvoeren van GPU-werk af. Met de huidige aanpak zou geheugen na elke single submission heen en weer worden verplaatst. De ene applicatie wordt eruit gegooid en onmiddellijk weer teruggeplaatst, waardoor de andere applicatie eruit vliegt. Al dit verplaatsen resulteerde in slechtere prestaties dan wanneer het geheugen nooit was verplaatst.

Dit was een oud probleem waarbij te agressieve VRAM-allocatie leidde tot "ping-pong" bewegingen. Dat was opgelost door niet te proberen VRAM op te eisen wanneer er geen vrij VRAM meer was. Echter, de kernel begon weer aggressief te worden toen ik VRAM-bescherming met dmem cgroups implementeerde. Dit moet het ping-pongen onbedoeld hebben geherintroduceerd.

Conceptueel zou het ontwerp van dmem cgroup VRAM-bescherming nooit mogen leiden tot ping-pong bewegingen, omdat de kernel alleen geheugen zou moeten verwijderen dat geen cgroup VRAM-bescherming heeft. De enige uitzondering is geheugen dat absoluut in VRAM moet leven om te kunnen functioneren. In de praktijk is er voor applicaties slechts één buffer die dit vereist: de buffer die beelddata bevat die naar het display moet worden gescand (scanout data)⁴.

Display-hardware is eigenzinnig

Niet alleen wil de display-hardware dat gescande beelden in VRAM staan, maar het slaat ook de virtuele geheugenarchitectuur van de GPU volledig over en werkt uitsluitend met fysieke adressen. Bijgevolg moeten gescande beelden ook continu (aaneengesloten) in het fysieke geheugen staan.

Dankzij virtueel geheugen en paginatabellen zijn typische applicatie-buffers alleen continu in het virtuele geheugen en kunnen ze overal in het fysieke geheugen verspreid zijn⁵. Als je scanout-data alloceert, is deze fragmentatie geen optie. Dit heeft zeer ongelukkige interacties met het verwijderen van andere data.

Stel dat de scanout-data al is verwijderd, maar het is nu tijd om deze data te scannen, dus moet het terug naar VRAM. Het simpelweg verwijderen van één buffer is niet voldoende, zelfs als die buffer dezelfde grootte heeft als de scanout-data, omdat dit niet resulteert in genoeg continu fysiek geheugen. Het verwijderingsalgoritme houdt namelijk helemaal geen rekening met fysieke geheugenbeperkingen. Het is een simpele loop:

while (true) {
    evict(getLeastRecentlyUsedBuffer())
    if (tryAllocate(newBuffer) == SUCCESS)
        break;
}

In een scenario met veel fragmentatie kan het gebeuren dat bijna alle allocaties in VRAM moeten worden verwijderd om één aaneengesloten blok voor de scanout-buffer te vinden. In praktijkscenario's observeerde ik dat tot 4 GiB aan VRAM werd "genuked" om ruimte te maken voor scanout-beelden (die ongeveer 32 MiB per beeld zijn). Alleen het verplaatsen van al die data uit VRAM zou al minstens 130 ms kosten.

Heuristieken toepassen

Hoewel scanout het meest extreme geval is, is het probleem algemener: er zullen altijd allocaties zijn die telkens terug naar VRAM worden verplaatst, waarbij mogelijk geheugen wordt verwijderd dat een applicatie liever in VRAM had gehouden.

Hoewel dmem cgroup-bescherming geen complete oplossing is, verkleint het de reikwijdte van het probleem aanzienlijk. Met cgroup-bescherming weet je dat een willekeurige app niet zomaar belangrijke game-resources zal verwijderen. Toch moeten we voorzichtig zijn en niet proberen verwijderd geheugen met geweld terug te eisen.

Na iteratief testen ben ik gekomen tot een set heuristieken die goed werken:

  • Wanneer de kernel detecteert dat het geheugen van een applicatie wordt verwijderd, gaat deze voor een paar milliseconden in een "hard throttle" fase. Tijdens deze fase probeert de kernel geen enkel geheugen voor die app terug te plaatsen in VRAM.
  • Daarna schakelt het systeem over naar een "soft throttle" fase. Hierbij mag vrije ruimte worden teruggewonnen door zaken terug te plaatsen in VRAM, maar er wordt niet geprobeerd geheugen te verwijderen dat door andere apps is gealloceerd. Deze periode kan enkele seconden duren om te zorgen dat alles in een stabiele staat is.
  • Als de "soft throttle" fase is voltooid zonder dat er opnieuw geheugen is verwijderd, wordt aangenomen dat het systeem stabiel is en worden de beperkingen op het verwijderen van geheugen van andere applicaties opgeheven.

Naar mijn mening bereikt dit een acceptabel evenwicht.

Vooruitgang boeken

Met deze heuristieken kunnen we de instellingen nu echt omhoog schroeven. Ik gebruikte Indiana Jones: The Great Circle, omdat je hier de streaming pool sizes kunt aanpassen om het VRAM-verbruik direct te beïnvloeden.

Zelfs wanneer de instellingen op een absurd punt staan—waarbij de game 9 GiB aanvraagt van de beschikbare 8 GiB VRAM (1 GiB overcommitted)—storten de prestaties niet meer in. Een gemiddelde van 19,6 ms per frame is nog steeds perfect speelbaar.

Als ik de instellingen nog verder opvoerde naar 10 GiB (2 GiB overcommitted), steeg de variantie in frametimes aanzienlijk, met spikes boven de 33,3 ms. Het gemiddelde lag rond de 29,8 ms. Dit zou merkbaar worden in de gameplay.

Hoewel dit een enorme stap vooruit is, zijn we er nog niet. De ervaring bij VRAM-overcommit kan nog steeds wisselvallig zijn. Dit komt omdat we momenteel niet rekening houden met hoe het verwijderde geheugen door de GPU wordt gebruikt. Als we onze beslissingen konden baseren op hoe goed de toegangspatronen van een applicatie werken met CPU-geheugen, zou veel van deze variantie kunnen verdwijnen.

De controle overdragen

Het probleem is dat toegangspatronen alleen bekend zijn bij de applicatie zelf. De driver kan hier niet zomaar rekening mee houden. Ideaal zou een API zijn waarbij de applicatie hints kan geven over hoe geschikt een bepaalde geheugenallocatie is om te worden verwijderd.

Precies dat is wat de VKEXTpageabledevicelocal_memory extensie doet. Hiermee kunnen applicaties elke prioriteit meegeven die ze willen voor een stuk device-geheugen. Als applicaties redelijke hints geven, kan het implementeren van prioritering in de kernel de boel enorm stabiliseren.

De kernel onderhoudt al een Least-Recently-Used (LRU) lijst van geheugenallocaties. Wanneer er geheugen moet worden verwijderd, wordt deze lijst doorlopen. De LRU-lijst is een goede heuristiek: applicaties die al lang niets hebben ingediend, hebben hun geheugen waarschijnlijk niet snel nodig.

Het probleem is dat wanneer een applicatie een set buffers gebruikt, die hele set in één blok naar het einde van de LRU-lijst wordt verplaatst. De volgorde binnen dat blok is niet gecontroleerd. Dat betekent dat zodra de kernel besluit geheugen van een applicatie te verwijderen, het willekeurig is welke buffers eruit gaan. Een zeer belangrijke buffer met hoge prioriteit kan dus per ongeluk als eerste worden verwijderd.

Omdat we nu specifieke prioriteiten voor individuele allocaties kennen, is de LRU-lijst een eenvoudige plek om dit te integreren. Het is simpelweg een kwestie van de lijstitems binnen één applicatie sorteren op hun prioriteit. Nu de kernel de LRU-lijst doorloopt, zal hij eerst de buffers met de laagste prioriteit vinden en proberen te verwijderen. De buffers met de hoogste prioriteit staan achteraan en worden pas verwijderd als het verwijderen van alle lagere prioriteiten niet voldoende was.

Adoptie van geheugenprioriteiten in apps

Helaas stellen niet alle applicaties prioriteiten in via VKEXTpageabledevicelocal_memory. Voor native Vulkan-applicaties heb ik bijvoorbeeld geen enkele idTech-game gezien die de extensie direct gebruikt.

De D3D-kant ziet er veel beter uit, omdat vkd3d-proton de extensie al gebruikt wanneer deze beschikbaar is. Het vertaalt zowel de ID3D12Device::MakeResident/ID3D12Device::Evict API-aanroepen als prioriteiten gesteld via ID3D12Device1::SetResidencyPriority naar Vulkan-prioriteitswaarden. Veel D3D12-games maken gebruik van minstens één van deze API's, waardoor hun hints nu worden benut.

Hoewel ik geen exacte cijfers heb voor hoeveel geheugen de meeste D3D12-apps overcommitten, lijkt het respecteren van geheugenprioriteiten de ervaring aanzienlijk te verbeteren. De prestaties lijken stabieler over tijd. In sommige gevallen vermoed ik dat het de prestaties met wel 30% heeft verhoogd ten opzichte van willekeurige verwijdering, hoewel dit sterk afhangt van geluk bij de eviction.

Conclusie

Hoe houdt het zich nu echt? Ik zou zeggen: best goed! In veel gevallen ben je verrast hoeveel prestaties je kunt behouden, zelfs als je een gigabyte of meer aan geheugen verplaatst.

Natuurlijk is dat een optimistisch scenario. Als het verkeerde stuk geheugen in het CPU-RAM belandt, kan dat zeer significante vertragingen veroorzaken. Verwijdering is lastig perfect te krijgen. Als een game al moeite heeft om 30 FPS te halen met alles in VRAM, kan het overcommitten onvermijdelijk leiden tot het missen van dat doel.

Desalniettemin hoop ik dat dit bericht aantoont dat de vertraging beheersbaar is wanneer geheugen naar het systeem-RAM wordt verplaatst. Er zijn maatregelen die drivers (vooral de kernel-driver) kunnen nemen om overcommit zo snel mogelijk te laten werken, en applicaties kunnen hun steentje bijdragen door te coördineren met de driver-stack. Met alles op hun plek is VRAM-overcommit minder problematisch dan men in eerste instantie denkt.

Al het beschreven werk is al enige tijd uitgebracht in SteamOS (zowel in Stable als Preview).

Een opmerking over upstreaming

Ik werk inmiddels aan het upstreamen van dit werk zodat het voor iedereen beschikbaar is. Er zijn echter veel bewegende delen en diepe refactors van core-concepten, dus het zal tijd kosten voordat alles is samengevoegd.

Als tussenoplossing heb ik het kernel-werk gerebaseerd op een recente upstream versie van de kernel en een git-branch gepubliceerd. Let op: dit is niet zo rigoureus getest als de SteamOS-kernel; gebruik het dus op eigen risico. Om de prioriteits-hints van applicaties door te geven aan de kernel, heb je ook een custom Mesa-branch nodig.

Eigen vragen

Hoewel ik een goed overzicht heb van de driver-zijde, ben ik niet bekend met hoe applicaties intern beslissen over geheugenbeheer-heuristieken. Ik vermoed dat het optimaliseren van scenario's waarin het VRAM al op is, niet bovenaan de TODO-lijst van ontwikkelaars staat. Misschien is er daar nog ruimte voor prestatieverbeteringen?

Als jij, beste lezer, meer weet over VRAM-beheer voor grote games/engines, dan praat ik graag met je! Ik heb het vermoeden dat er nog winst te behalen is door betere coördinatie tussen apps en drivers.

***

Voetnoten

¹ Een reden is simpelweg dat command buffers erg klein zijn, wat geen overweging is bij evicted VRAM (je hebt geen keuze in hoeveel je verwijdert). Dat is echter niet de enige reden: caching/toegangspatronen gelden voor beide. ² Zie de RADV-broncode. ³ Het is mogelijk dat alle allocaties slagen, maar dat het onmogelijk is om dat geheugen in een submission te gebruiken als zowel CPU-RAM als VRAM uitgeput zijn. Dit is normaal, maar in mijn geval was er echt genoeg geheugen beschikbaar. ⁴ Technisch gezien kan de display-hardware ook scannen vanuit het systeem-RAM, maar daar zitten lelijke trade-offs aan verbonden. ⁵ Voor de prestaties is het beter als buffers fysiek continu zijn, maar continuïteit is geen strikte vereiste. ⁶ In de praktijk zijn de buffers die als eerste zijn gealloceerd waarschijnlijk ook de eerste die worden verwijderd. ⁷ Sorteren is alleen haalbaar met de prioriteitswaarden van één enkele applicatie, omdat prioriteiten alleen betekenis hebben in relatie tot andere prioriteiten in dezelfde context.