ctDeflate schreibt Byte für Byte dieselben .gz-Dateien wie gzip aus libdeflate 1.26 und komprimiert eine einzelne Datei auf allen Kernen – auf einer 8-Kern-CPU bis zu 4,9-mal so schnell. Der Kompressor von libdeflate arbeitet von Grund auf sequenziell: Wo ein Block endet und wie eine Stelle kodiert wird, hängt von allem davor ab. Die acht Bilder zeigen, wie aus einem einzigen Kompressor Schritt für Schritt ein Task-Graph auf ThinkMeta ConcurrentTasks wurde, der alle Kerne auslastet – und warum jeder Schritt nötig war.

Read, libdeflate, Writechunk N−1chunk Nchunk N+1bufferReadFile MappingReadFile MappingReadFile Mappinglibdeflateall stageslibdeflateall stageslibdeflateall stagesWriteat the endWriteat the endWriteat the end Read, libdeflate, Write1 thread · one thing after anotherchunk N−1chunk Nchunk N+1bufferReadFile MappingReadFile MappingReadFile Mappinglibdeflateall stageslibdeflateall stageslibdeflateall stagesWriteat the endWriteat the endWriteat the end

Bild 1 von 8

Die Referenz: gzip aus libdeflate

gzip aus libdeflate bildet die Datei per File Mapping in den Adressraum ab – das Betriebssystem lädt die Teile, auf die zugegriffen wird –, komprimiert sie in einem Aufruf und baut die ganze Ausgabe im Speicher auf, bevor es sie schreibt – alles auf einem Thread, nacheinander. Jede Stufe hängt vom Vorherigen ab: wo ein Block endet, wie eine Stelle kodiert wird, an welchem Bit der nächste Block beginnt. Ein Kern arbeitet, die anderen warten.

Diese Bits sind die Messlatte: Jedes weitere Bild muss Byte für Byte dieselbe .gz-Datei schreiben – bei jeder Thread-Zahl und jeder Chunk-Größe.

Dreifach sequenziell

  • Wo ein Block endet, hängt von der Symbolstatistik seit Blockbeginn ab.
  • Die minimale Matchlänge (3 bis 9) wird aus den ersten 4 KiB jedes Blocks bestimmt und bei den Lazy-Stufen laufend neu berechnet. Dieselbe Stelle wird also je nach Blockbeginn anders kodiert.
  • Jeder Block beginnt an dem Bit, an dem der vorige endete, und ob er unkomprimiert geschrieben wird, hängt von diesem Bit-Offset ab.

Die Referenz

gzip.exe aus dem offiziellen Windows-Build von libdeflate 1.26. Auf enwik9, 1 GB Wikipedia-Text, schafft es bei den Stufen 1, 6 und 9 217, 106 und 61 MB/s, gemessen über den ganzen Programmlauf.

Durchsatz · enwik9, Stufe 9, ganzer Programmlauf

61 MB/s

Read, Compressor, Writechunk N−1chunk Nchunk N+1input windowoutput windowReadasynchronousReadasynchronousReadasynchronousCompressorthe port, one taskCompressorthe port, one taskCompressorthe port, one taskWriteasynchronousWriteasynchronousWriteasynchronous Read, Compressor, WriteI/O schedulercompute schedulerI/O schedulerchunk N−1chunk Nchunk N+1input windowoutput windowReadasynchronousReadasynchronousReadasynchronousCompressorthe port, one taskCompressorthe port, one taskCompressorthe port, one taskWriteasynchronousWriteasynchronousWriteasynchronous

Bild 2 von 8

Der Port: ein Kompressor-Task zwischen asynchronem Lesen und Schreiben

Der Kompressor von libdeflate wird in C++ nachgebaut, vom Matchfinder bis zum Bitstrom, und läuft als ein Task auf dem Rechen-Scheduler. Lesen und Schreiben werden eigene Tasks mit überlappender I/O auf einem I/O-Scheduler. Dazwischen liegen begrenzte Fenster: Der Kompressor lässt den Lese-Task höchstens 8 MiB vor dem aktuellen Block lesen und gibt die Eingabe hinter seinem 32-KiB-Suchfenster wieder frei; der Schreib-Task schreibt jedes fertige MiB und gibt es frei. Der Port ist so geschnitten, dass die Matchsuche an jeder beliebigen Stelle beginnen kann, wenn man ihm den vollständigen Zustand mitgibt – das braucht jedes spätere Bild.

Lesen und Schreiben laufen jetzt während der Kompression, nicht davor und danach. Auf enwik9 braucht der Port 17 MB statt der 1.329 MB von gzip – der Speicher hängt nicht mehr von der Dateigröße ab. Über den ganzen Programmlauf ist er gleich schnell wie derselbe Port mit der ganzen Datei im Speicher oder bis zu 7 % schneller, bit-identisch auf allen Stufen.

Erst bit-identisch

Nichts durfte parallel laufen, bevor der Port dieselben Bytes schrieb wie libdeflate: Stufen 0 bis 9 über 23 echte Dateien bis 163 MB.

Jedes Prozent zählt

Die Geschwindigkeit auf einem Kern ist die Grundlage jedes späteren Faktors. Gemessen im Speicher auf einem Thread, brachten CRC-32 mit Carry-less-Multiplikation, Prefetch in den Matchfindern und konsequentes Inlining den Port von 79–91 % auf 90–107 % von libdeflate, Median etwa 95 %. Der Rest ist Compiler: Mit demselben Compiler gebaut, ist libdeflate nicht schneller als der Port.

Speicher des Ports

Höchster Speicherbedarf: enwik9 (1 GB) bei Stufe 1 / 6 17 / 17 MB statt 1.354 / 1.329 MB mit gzip; die Datenbankdatei (163 MB) bei Stufe 6 21 statt 191 MB.

Ausprobiert und verworfen: ein I/O-Thread

Ein Schreibvorgang, der die Datei verlängert, läuft in NTFS synchron. Mit nur einem I/O-Thread hielt er diesen fest und damit auch das Lesen; bei Stufe 1 warteten bis zu 47 MB fertige Ausgabe auf das Schreiben. Mit zwei I/O-Threads blockieren sich Lesen und Schreiben nicht mehr.

Bis heute im Einsatz

Dieser Graph steckt noch im Code: ctDeflate --sequential führt ihn aus – der Referenzweg, an dem jedes spätere Bild gemessen wird.

Spitzenspeicher · enwik9, Stufe 6, gzip → Port

1.329 → 17 MB

Read, Match search, Resolve, Huffman coding, Assemble, Writechunk N−1chunk Nchunk N+1chunkssearch resultsblocksReadasynchronousReadasynchronousReadasynchronousMatch searchspeculative, 1 MiBResolvereplays block logicHuffman codingonly after resolvingAssemblebit by bit, in orderWriteasynchronousWriteasynchronousWriteasynchronous Read, Match search, Resolve, Huffman coding, Assemble, WriteI/O schedulercompute schedulerI/O schedulerchunk N−1chunk Nchunk N+1chunkssearch resultsblocksReadasynchronousReadasynchronousReadasynchronousMatch searchspeculative, 1 MiBResolvereplays block logicHuffman codingonly after resolvingAssemblebit by bit, in orderWriteasynchronousWriteasynchronousWriteasynchronous

Bild 3 von 8

Spekulativ suchen, exakt abgleichen

Die Eingabe wird in Chunks à 1 MiB geteilt. Für jeden Chunk läuft die Matchsuche parallel, als ob an seinem Anfang ein Block begänne. Ein serieller Resolver spielt danach die Blocklogik von libdeflate in Reihenfolge nach: Wo die Suche eines Chunks das wahre Ergebnis trifft, übernimmt er sie; wo keine passt, sucht er diesen Abschnitt selbst exakt nach (gestrichelt).

Die teure Arbeit, die Matchsuche, hängt fast nur von den Daten ab und läuft jetzt auf allen Kernen. Der Zustand, der libdeflate sequenziell macht, ist klein und lässt sich nachspielen. Auf einer Binärdatenbank: 3,6-mal so schnell wie libdeflate bei Stufe 6, 8,2-mal bei Stufe 9.

Übernehmen oder nachsuchen

Jeder Chunk baut den Matchfinder aus den 32 KiB davor auf. Erreicht seine Suche dieselbe Stelle mit derselben minimalen Matchlänge wie die wahre, laufen beide ab dort gleich, und der Resolver übernimmt die Arbeit des Chunks. Nur wo keine Suche passt, sucht er nach – nur diesen Abschnitt.

Unkomprimierte Blöcke

Ob ein Block unkomprimiert geschrieben wird, hängt vom Bit-Offset ab, an dem er beginnt. Ein solcher Block wird vermerkt und erst beim Anhängen mit dem echten Offset entschieden.

Vier Testdateien

Faktor gegenüber libdeflate bei Stufe 1 / 6 / 9, 16 Threads, ganze Datei im Speicher:

  • Textprotokoll, 26 MB: 2,4× / 1,5× / 3,1×
  • Quelltext, 12 MB: 2,0× / 0,9× / 1,7×
  • Binärdatenbank, 163 MB: 2,3× / 3,6× / 8,2×
  • vorkompilierter Header, 77 MB: 2,2× / 2,6× / 6,8×

Die Schwachstellen: Wo die minimale Matchlänge von der Annahme des Chunks abweicht, muss der Resolver seriell reparieren – beim Textprotokoll auf Stufe 6 sind das 19 % der Datei. Und Kodieren und Zusammensetzen (Assemble) laufen erst nach dem Resolver.

Durchsatz · Datenbankdatei, Stufe 6, im Speicher

802 MB/s · 3,6× libdeflate

Read, Match search, History track, Min. length 3, Min. length 4 …, Merge, Resolve, Coding · assembly, Writechunk N−1chunk Nchunk N+1chunkssearch resultsblocksReadasynchronousReadasynchronousReadasynchronousMatch searchone for all tracksHistory trackown block logicMin. length 3Min. length 4 …Mergecommon starting pointResolvepicks the true trackCoding · assemblyas beforeWriteasynchronousWriteasynchronousWriteasynchronous Read, Match search, History track, Min. length 3, Min. length 4 …, Merge, Resolve, Coding · assembly, WriteI/O schedulercompute schedulerI/O schedulerone search taskchunk N−1chunk Nchunk N+1chunkssearch resultsblocksReadasynchronousReadasynchronousReadasynchronousMatch searchone for all tracksHistory trackown block logicMin. length 3Min. length 4 …Mergecommon starting pointResolvepicks the true trackCoding · assemblyas beforeWriteasynchronousWriteasynchronousWriteasynchronous

Bild 4 von 8

Mehrere Spuren in einer Suche

Der Graph bleibt gleich, aber die Matchsuche wird von innen umgebaut: Ein Chunk durchsucht mehrere Spuren gemeinsam – eine je möglicher minimaler Matchlänge und eine Verlaufsspur (History track) mit eigener Blocklogik. Die erste Suche jedes Schritts ist für alle Spuren dieselbe; die Spuren trennen sich nur, wo die Entscheidung für einen Match abweicht, und finden an einem gemeinsamen Startpunkt wieder zusammen.

Reparaturen sind seriell – jede ist Zeit, in der nur ein Kern arbeitet. Mit mehreren Spuren findet der Resolver fast immer eine passende: Beim Textprotokoll sinken die Reparaturen von 4,9 MB auf 21 KB. Quelltext auf Stufe 6 steigt von 411 auf 700 MB/s.

Exakt dieselbe Suche

Wie auch immer sich die Spuren teilen und vereinigen: Jede Suche sieht genau den Matchfinder-Zustand, den libdeflate an dieser Stelle sähe. Das hält jede Spur bit-identisch zur echten Suche.

Der Preis

Mit nur einer Spur war die neue Suche etwa 20 % langsamer als die alte; die Datenbankdatei verlor deshalb zunächst (Stufe 6: 802 → 616 MB/s). Profilgeführte Arbeit danach brachte sie auf 1.479 / 750 / 346 MB/s bei Stufe 1 / 6 / 9.

Durchsatz · Textprotokoll, Stufe 6, im Speicher

420 → 630 MB/s

Read, Match search, search results, Resolve, resolved blocks, Huffman coding, coded blocks, Assemble, Writechunk N−1chunk Nchunk N+1chunksbitstreamReadasynchronousReadasynchronousReadasynchronousMatch searchworkers, speculativeMatch searchworkers, speculativeMatch searchworkers, speculativesearch resultsResolvein orderResolvein orderResolvein orderresolved blocksHuffman codingthe same workersHuffman codingthe same workersHuffman codingthe same workerscoded blocksAssemblein the worker, no taskAssemblein the worker, no taskAssemblein the worker, no taskWriteasynchronousWriteasynchronousWriteasynchronous Read, Match search, search results, Resolve, resolved blocks, Huffman coding, coded blocks, Assemble, WriteI/O schedulercompute schedulerI/O schedulerchunk N−1chunk Nchunk N+1chunksbitstreamReadasynchronousReadasynchronousReadasynchronousMatch searchworkers, speculativeMatch searchworkers, speculativeMatch searchworkers, speculativesearch resultsResolvein orderResolvein orderResolvein orderresolved blocksHuffman codingthe same workersHuffman codingthe same workersHuffman codingthe same workerscoded blocksAssemblein the worker, no taskAssemblein the worker, no taskAssemblein the worker, no taskWriteasynchronousWriteasynchronousWriteasynchronous

Bild 5 von 8

Kodieren und Zusammensetzen laufen mit

Matchsuche und Huffman-Kodierung laufen in denselben Worker-Tasks, einer je Thread. Ein Worker kodiert zuerst den nächsten Block, den der Resolver freigegeben hat; sonst durchsucht er den nächsten Chunk. Wer einen Block fertig kodiert hat, hängt alle Blöcke an, die in Reihenfolge fertig sind – das Zusammensetzen (Assemble) braucht keinen eigenen Task.

Vorher begann das Kodieren erst nach dem Resolver: ein zweiter serieller Abschnitt am Ende, der bei Stufe 1 die Laufzeit bestimmte. Jetzt bleibt nach dem Resolver fast nichts zu tun – bei der Datenbankdatei weniger als eine Millisekunde. Stufe 1 wird um 13 bis 24 % schneller; bei Stufe 6 und 9 begrenzen Resolver und Matchsuche.

Warum zuerst kodiert wird

Kodieren vor Suchen lässt abgeglichene Blöcke nur kurz warten, und das Zusammensetzen läuft genau dort, wo ein Block gerade fertig wurde.

Keine Übergabe geht verloren

Es hängt immer nur ein Worker an. Ist er fertig, prüft er noch einmal, ob der nächste Block inzwischen bereitsteht – so wartet kein Block auf ein Anhängen, das nie kommt.

Durchsatz · Datenbankdatei, Stufe 1, im Speicher

1.479 → 1.828 MB/s

Read, Match search, search results, Resolve, block ring · 4096 slots, Huffman coding, same ring · coded blocks, Assemble, Writechunk N−1chunk Nchunk N+1input windowoutput windowReadasynchronousReadasynchronousReadasynchronousMatch search+ ≤ threads + 4 aheadMatch search+ ≤ threads + 4 aheadMatch search+ ≤ threads + 4 aheadsearch resultsResolvein orderResolvein orderResolvein orderblock ring · 4096 slotsHuffman codingthe same workersHuffman codingthe same workersHuffman codingthe same workerssame ring · coded blocksAssemble+ frees memoryAssemble+ frees memoryAssemble+ frees memoryWriteasynchronousWriteasynchronousWriteasynchronous Read, Match search, search results, Resolve, block ring · 4096 slots, Huffman coding, same ring · coded blocks, Assemble, WriteI/O schedulercompute schedulerI/O schedulerchunk N−1chunk Nchunk N+1input windowoutput windowReadasynchronousReadasynchronousReadasynchronousMatch search+ ≤ threads + 4 aheadMatch search+ ≤ threads + 4 aheadMatch search+ ≤ threads + 4 aheadsearch resultsResolvein orderResolvein orderResolvein orderblock ring · 4096 slotsHuffman codingthe same workersHuffman codingthe same workersHuffman codingthe same workerssame ring · coded blocksAssemble+ frees memoryAssemble+ frees memoryAssemble+ frees memoryWriteasynchronousWriteasynchronousWriteasynchronous

Bild 6 von 8

Begrenztes Fenster: der Speicher hängt nicht mehr an der Dateigröße

Ohne Grenze laufen die Suchen beliebig weit voraus, und Eingabe und Suchergebnisse wachsen mit der Datei. Jetzt werden Chunks höchstens einige Plätze vor dem Chunk durchsucht, auf den der Resolver wartet (Standard: Thread-Zahl plus 4), und der Lese-Task liest nur, was dieses Fenster brauchen kann. Das Zusammensetzen (Assemble) gibt hinter dem Bitstrom Eingabe und Suchergebnisse wieder frei, der Schreib-Task die geschriebene Ausgabe. Der Block-Ring fasst 4096 Plätze.

Neu sind das begrenzte Fenster und das Freigeben – Lesen und Schreiben neben der Kompression gibt es seit dem Port. Der Speicher hängt jetzt von den Chunks in Arbeit ab, nicht von der Dateigröße.

Eine 5,2-GB-Datei

Stufe 6 mit 144 MB Speicher in 7,3 s statt 28,8 s mit gzip (4,0×) – bei derselben Ausgabe.

Speicher im Vergleich

Höchster Speicherbedarf beim Komprimieren von enwik9 (1 GB), Core i9-11900K, Stufe 1 / 6 / 9:

  • gzip aus libdeflate: 1.354 / 1.329 / 1.326 MB – die gelesenen Seiten bleiben im Speicher, dazu die ganze Ausgabe
  • der Port als ein Kompressor-Task: 17 / 17 MB bei Stufe 1 / 6 (auf der 163-MB-Datenbankdatei: 21 MB bei Stufe 6)
  • ctDeflate heute, 16 Threads: 135–165 / 135–142 / 129–136 MB

Fenster statt Kopien

Die Eingabe liegt an ihrer Dateiposition in einem reservierten Adressbereich; Speicher belegt nur, was das Suchfenster braucht, und der Kompressionscode bleibt unverändert. Die Ausgabe wird in 1-MiB-Segmenten geschrieben und gleich wieder freigegeben.

Erste Messung des ganzen Programms

Über die vier Testdateien (zusammen 278 MB), gemessen über den ganzen Programmlauf: 3,19× / 3,05× / 6,02× so schnell wie gzip bei Stufe 1 / 6 / 9.

Spitzenspeicher · Datenbankdatei, Stufe 6, 16 Threads

246 → 84 MB

Read, Match search, search results, Resolve, block ring, Huffman coding, same ring · coded blocks, Assemble, Writechunk N−1chunk Nchunk N+1input windowoutput windowReadasynchronousReadasynchronousReadasynchronousMatch search+ tracks set by resolveMatch search+ tracks set by resolveMatch search+ tracks set by resolvesearch resultsResolvein orderResolvein orderResolvein orderblock ringHuffman codingthe same workersHuffman codingthe same workersHuffman codingthe same workerssame ring · coded blocksAssemblefrees memoryAssemblefrees memoryAssemblefrees memoryWriteasynchronousWriteasynchronousWriteasynchronous Read, Match search, search results, Resolve, block ring, Huffman coding, same ring · coded blocks, Assemble, WriteI/O schedulercompute schedulerI/O schedulerchunk N−1chunk Nchunk N+1input windowoutput windowReadasynchronousReadasynchronousReadasynchronousMatch search+ tracks set by resolveMatch search+ tracks set by resolveMatch search+ tracks set by resolvesearch resultsResolvein orderResolvein orderResolvein orderblock ringHuffman codingthe same workersHuffman codingthe same workersHuffman codingthe same workerssame ring · coded blocksAssemblefrees memoryAssemblefrees memoryAssemblefrees memoryWriteasynchronousWriteasynchronousWriteasynchronous

Bild 7 von 8

Der Resolver meldet zurück

Die Spuren kosten mehr, als ihre zusätzlichen Suchen erklären, und auf gleichförmigen Daten braucht man sie fast nie. Ob sie gebraucht werden, weiß nur der Resolver – also fließt dieses Wissen zurück (gestrichelt): Nach 8 Chunks in Folge ohne Fehltreffer durchsuchen neue Chunks nur noch die Verlaufsspur (History track), mit einer schnelleren Suche für eine Spur. Beim nächsten Fehltreffer gibt es wieder die volle Auswahl.

Das kleine Suchfenster sorgt dafür, dass die Rückmeldung rechtzeitig ankommt: Nur wenige Chunks sind schon durchsucht, wenn sie sich ändert. Reparaturen bleiben gleich, die Bits auch.

Was eine Spur kostet

Datenbankdatei, Stufe 6, ein Thread: 89 MB/s mit einer Spur, 63 MB/s mit dreien. Auch die minimalen Matchlängen der letzten Blöcke gehen in die Wahl der Spuren ein.

Ausprobiert und verworfen

Ein großes Suchfenster mit 64 Chunks war auf der Datenbankdatei 6 % schneller (733 statt 690 MB/s), brauchte aber mehr als doppelt so viel Speicher (197 statt 84 MB) und verzögerte die Rückmeldung. Der Standard bleibt klein. Die Spuren nur aus den Werten der letzten Blöcke zu wählen, führte auf Datenbanken zu vielen Reparaturen.

Durchsatz · Datenbankdatei, Stufe 6, Datei → Datei

651 → 852 → 930 MB/s

Read, Match search, search results, Resolve, block ring, Huffman coding, same ring · coded blocks, Assemble, Writechunk N−1chunk Nchunk N+1input windowoutput windowReadasynchronousReadasynchronousReadasynchronousMatch searchspeculativeMatch searchspeculativeMatch searchspeculativesearch resultsResolveown threadResolveown threadResolveown threadblock ringHuffman codingthe same workersHuffman codingthe same workersHuffman codingthe same workerssame ring · coded blocksAssemblein the worker, no taskAssemblein the worker, no taskAssemblein the worker, no taskWriteasynchronousWriteasynchronousWriteasynchronous Read, Match search, search results, Resolve, block ring, Huffman coding, same ring · coded blocks, Assemble, WriteI/O schedulercompute scheduler · 1 thread per logical processorI/O schedulerchunk N−1chunk Nchunk N+1input windowoutput windowReadasynchronousReadasynchronousReadasynchronousMatch searchspeculativeMatch searchspeculativeMatch searchspeculativesearch resultsResolveown threadResolveown threadResolveown threadblock ringHuffman codingthe same workersHuffman codingthe same workersHuffman codingthe same workerssame ring · coded blocksAssemblein the worker, no taskAssemblein the worker, no taskAssemblein the worker, no taskWriteasynchronousWriteasynchronousWriteasynchronous

Bild 8 von 8

Der Graph heute

Bei 16 Threads übernehmen 16 Worker auf dem Rechen-Scheduler Matchsuche und Huffman-Kodierung; Lesen und Schreiben laufen wie beim Port auf dem I/O-Scheduler. Der Resolver bleibt auf dem aufrufenden Task, der neben den Workern einen eigenen Thread hat. Das Zusammensetzen (Assemble) läuft im Worker, der den nächsten Block fertig kodiert. Seit dem letzten Umbau ist das Schreiben ein eigener Task neben dem Zusammensetzen, statt aus ihm heraus zu schreiben – gleich schnell oder leicht schneller.

Auf enwik9, 1 GB Wikipedia-Text, war ctDeflate bei Stufe 1 4,3-mal so schnell wie gzip (217 → 928 MB/s), bei Stufe 6 3,7-mal (106 → 392 MB/s) und bei Stufe 9 4,9-mal – gemessen vor dem Umbau über den ganzen Programmlauf mit Lesen und Schreiben, jede Datei bit-identisch.

Warum Fibers den Unterschied machen

Der Resolver wartet mitten in seiner Schleife auf die nächste Chunk-Suche und, wenn der Ring voll ist, auf einen freien Platz. Eine Suche wartet auf ihre Eingabe, Lesen und Schreiben auf überlappende I/O, das Schreiben auch auf fertige Ausgabe. Auf ThinkMeta ConcurrentTasks hält ein solches Warten nur den Task an; sein Thread sucht oder kodiert derweil, und jeder Knoten bleibt eine einfache Schleife in Reihenfolge.

Der Umbau gemessen

Ganzer Programmlauf, Median abwechselnder Paare, gegen den Stand davor: 0,97–1,06 bei 16 Threads, 1,00–1,07 bei 2 Threads. Der höchste Speicherbedarf auf enwik9 bei 16 Threads bleibt bei Stufe 6 bei 135–142 MB und bei Stufe 9 bei 129–136 MB; bei Stufe 1 steigt er von 98–109 auf 135–165 MB. Dort entsteht die Ausgabe mit über 300 MB/s: Vorher bremste das Schreiben im Zusammensetzen die ganze Pipeline, jetzt läuft das Schreiben daneben, und bis zu 16 MiB fertige Ausgabe dürfen auf das Schreiben warten.

Ausprobiert und verworfen: der Resolver als Scheduler-Task

Als Task auf dem Rechen-Scheduler war der Resolver bei 16 Threads gleich schnell und mit 2 Threads bis zu 9 % langsamer. Auf dem aufrufenden Task hat er einen eigenen Thread und wartet nie auf einen freien Worker-Thread.

Viele kleinere Dateien

Auf dem Silesia-Korpus (12 Dateien, 212 MB) war ctDeflate bei Stufe 1 / 6 / 9 2,4- / 2,2- / 3,6-mal so schnell wie gzip. Kleine Dateien haben weniger Chunks, als die CPU Threads hat, und Programmstart und I/O wiegen schwerer. Gemessen vor dem Umbau.

Andere Rechner

Veröffentlichte Messungen, vor dem Umbau, Stufe 1 / 6 / 9:

  • Intel Core i9-11900K, 8 Kerne: enwik9 4,3× / 3,7× / 4,9×
  • Intel Core Ultra 7 155H, Notebook: enwik9 4,1× / 3,3× / 2,0×
  • Intel Xeon E3-1275 v6, 4 Kerne: Silesia 2,1× / 1,7× / 2,6×

Alle 117 Läufe bit-identisch zu gzip.

Durchsatz · enwik9, Stufe 9, ganzer Programmlauf, Stand vor dem Umbau

61 → 303 MB/s (4,9×)

Selbst ausprobieren

ctDeflate, die Referenz gzip.exe und der Benchmark für eigene Dateien liegen auf GitHub. Sie haben eine Arbeitslast, die jeden Kern nutzen soll? Nehmen Sie Kontakt auf.

An unhandled error has occurred. Reload