ctOpus schreibt Byte für Byte dieselben Opus-Dateien wie opusenc und verteilt eine einzige Datei auf alle Kerne. Mit maximaler Komplexität ist ctOpus doppelt so schnell wie opusenc mit minimaler; auf 45 Stunden Musik ist es 4,4-mal so schnell wie opusenc mit denselben Einstellungen. Opus ist von Grund auf sequenziell: Jeder Frame setzt den Bitstrom, die Energievorhersage und die Filterspeicher des vorigen fort. Die elf Bilder zeigen, wie daraus Schritt für Schritt ein Task-Graph auf ThinkMeta ConcurrentTasks wurde – bis hin zu Tasks, die künftige Frames spekulativ vorausrechnen.

Read, Resampler, Encoder, Writeframe N−1frame Nframe N+1bufferbufferReadsynchronousReadsynchronousReadsynchronousResampler44.1 → 48 kHzResampler44.1 → 48 kHzResampler44.1 → 48 kHzEncoderanalysis to bitstreamEncoderanalysis to bitstreamEncoderanalysis to bitstreamWritesynchronousWritesynchronousWritesynchronous Read, Resampler, Encoder, Write1 thread · one thing after anotherframe N−1frame Nframe N+1bufferbufferReadsynchronousReadsynchronousReadsynchronousResampler44.1 → 48 kHzResampler44.1 → 48 kHzResampler44.1 → 48 kHzEncoderanalysis to bitstreamEncoderanalysis to bitstreamEncoderanalysis to bitstreamWritesynchronousWritesynchronousWritesynchronous

Bild 1 von 11

Die Referenz: opusenc

opusenc liest die WAV-Datei, rechnet sie von 44,1 auf 48 kHz um, kodiert sie und schreibt Ogg-Seiten – alles auf einem Thread, nacheinander. Während er liest oder schreibt, wartet der Thread auf die Datei. Jede Stufe trägt Zustand von Frame zu Frame: die Filterspeicher, die Energievorhersage, das Bit-Reservoir und den Bitstrom. Ein Kern arbeitet, die anderen warten.

Diese Bits sind die Messlatte: Jedes weitere Bild muss Byte für Byte dieselbe .opus-Datei schreiben. ctOpus beginnt mit einer Portierung des Encoders, Funktion für Funktion: Auf einem Thread ist sie gleich schnell wie opusenc (2,09 s) und bit-identisch.

Wo die Zeit liegt

Ein Profil von opusenc bei Komplexität 10: Der Resampler braucht 34,5 %, die Quantisierung der Frequenzbänder 25,1 %, die Tonalitäts-Analyse 8,2 %, der Vorfilter 6,6 %, die Transformation und die Zeit-Frequenz-Entscheidungen zusammen etwa 9 %. Die Testmusik ist durchweg 44,1-kHz-Stereo, der Resampler läuft also bei jeder Datei mit.

Komplexität

Opus tauscht Geschwindigkeit gegen Qualität, von Komplexität 0 (am schnellsten) bis 10 (am besten, die Voreinstellung). Für die Testdatei braucht opusenc bei Komplexität 10 2,16 s, bei Komplexität 0 0,97 s. Das Ziel von ctOpus: Komplexität 10 schneller als opusenc mit Komplexität 0.

Laufzeit · eine Datei, 254 s Musik, comp 10

2,16 s

Read, Encode, Writeframe N−1frame Nframe N+1read bufferwrite bufferReadasynchronousReadasynchronousReadasynchronousEncodeone task does it allEncodeone task does it allEncodeone task does it allWriteasynchronousWriteasynchronousWriteasynchronous Read, Encode, WriteI/O schedulercompute schedulerI/O schedulerframe N−1frame Nframe N+1read bufferwrite bufferReadasynchronousReadasynchronousReadasynchronousEncodeone task does it allEncodeone task does it allEncodeone task does it allWriteasynchronousWriteasynchronousWriteasynchronous

Bild 2 von 11

Lesen und Schreiben asynchron

Die Datei-Ein- und -Ausgabe wird vom Kodieren getrennt. Lesen und Schreiben werden Tasks auf dem I/O-Scheduler; ein einziger Task kodiert und macht alles, was opusenc macht. Lesen liest mit fünf überlappenden Anfragen zu 1 MB in einen Lesepuffer voraus, der Encode-Task nimmt sich nur noch fertige Blöcke. Die Ogg-Seiten kopiert der Encode-Task in einen Schreibpuffer, und Schreiben bringt volle Blöcke in die Datei, zwei Anfragen gleichzeitig.

Warten Lesen oder Schreiben auf die Platte, belegen sie keinen Thread: Der I/O-Scheduler führt in der Zeit den anderen Task aus. Gemessen mit der Datei frisch von der Platte ist das gleich schnell oder schneller – und bit-identisch in jeder Prüfung. Mit einem einzigen Encode-Task dauern beide Wege gleich lang; mit dem ganzen Graphen von Bild 10 spart derselbe Schritt 2 bis 3 %, weil dort der Encode-Task die Ogg-Seiten auf dem kritischen Pfad schrieb: 16 Threads 0,486 → 0,474 s, 4 Threads 0,780 → 0,760 s.

Lesepuffer und Schreibpuffer

Der Lesepuffer fasst 8 Blöcke zu 1 MB, der Schreibpuffer 8 Blöcke zu 64 KB. Ein voller Puffer hält den Task an, der ihn füllt, ein leerer den, der ihn leert. opusenc dagegen schreibt jede Ogg-Seite mit einem eigenen Aufruf – bei der Testdatei mehrere tausend Mal.

Kalt gemessen

Vor jedem Lauf wurde die Eingabe frisch und ungepuffert kopiert, kam also wirklich von der Platte. Ein Encode-Task: 1,891 s synchron, 1,876 s asynchron – im Rauschen; er verbraucht seine Eingabe mit etwa 25 MB/s, ein Bruchteil dessen, was die Platte liefert. Blockgröße und Zahl der gleichzeitigen Anfragen spielen keine Rolle (±3 %).

Laufzeit · Datei frisch von der Platte, ein Encode-Task

1,891 → 1,876 s · opusenc 2,083 s

Read, Distribute, samples, Resample, block ring, Encode, Writeframe N−1frame Nframe N+1write bufferReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesWriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Encode, WriteI/O schedulercompute schedulerI/O schedulerframe N−1frame Nframe N+1write bufferReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesWriteasynchronousWriteasynchronousWriteasynchronous

Bild 3 von 11

Resampeln parallel

Verteilen (Distribute) entpackt die WAV-Daten in die Samples – auf dem Rechen-Scheduler mit hoher Priorität (dicker Rahmen), weil alle Resampler auf seine Blöcke warten. Der Resampler berechnet jedes Ausgabesample allein aus der Eingabe, seine Filterphase folgt aus der Position – Blöcke lassen sich deshalb unabhängig rechnen, auf allen Kernen, in beliebiger Reihenfolge, mit exakt denselben Werten. Der Encode-Task nimmt die Blöcke in Reihenfolge aus dem Blockring.

Der Resampler ist aus dem kritischen Pfad verschwunden: 1,86 s bei Komplexität 10, bei Komplexität 0 0,70 s statt 0,97 s (1,38×). Übrig bleibt fast nur noch der serielle Encoder.

Samples und Blockring

Die ganze Eingabe liegt in reserviertem Speicher, gefüllt beim Verteilen und freigegeben, sobald alle Blöcke sie gelesen haben. Verteilen gibt einen Block frei, sobald seine Eingabe vollständig ist und im Ring ein Platz frei ist; ein Platz fasst 16.384 resampelte Samples je Kanal.

Warum Verteilen rechnet

Seit das Lesen asynchron ist, wartet Verteilen nicht mehr auf die Platte, nur noch auf andere Tasks – und es rechnet nur: Samples umwandeln und nach Kanälen aufteilen. Es gehört deshalb auf den Rechen-Scheduler, der weit von einer Sättigung entfernt ist. Gemessen gegen den I/O-Scheduler: 16 Threads 0,474 → 0,466 s, 4 Threads 0,760 → 0,744 s, bit-identisch.

Gleiche Summen, gleiche Bits

Die parallelen Blöcke summieren in derselben Reihenfolge wie opusenc, und am Dateiende hängt Verteilen dieselbe Verlängerung des Signals an: eine Fortsetzung der letzten 480 Samples, dann Nullen. Der Encoder sieht exakt dieselben Zahlen.

Laufzeit · eine Datei, 254 s Musik, comp 10

2,16 → 1,86 s (1,16×)

Read, Distribute, samples, Resample, block ring, Analysis, analysis per frame, Encode, Writeframe N−1frame Nframe N+1write bufferReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonality per frameAnalysistonality per frameAnalysistonality per frameanalysis per frameEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesWriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Analysis, analysis per frame, Encode, WriteI/O schedulercompute schedulerI/O schedulerframe N−1frame Nframe N+1write bufferReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonality per frameAnalysistonality per frameAnalysistonality per frameanalysis per frameEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesWriteasynchronousWriteasynchronousWriteasynchronous

Bild 4 von 11

Die Analyse läuft voraus

Die Tonalitäts-Analyse eines Frames hängt nur vom Signal ab, nicht von den Entscheidungen des Encoders. Sie wird ein eigener Task, der dem Encode-Task vorausläuft und sein Ergebnis für jeden Frame in einen Ring legt. Der Blockring hat jetzt zwei Leser: Ein Platz ist erst wieder frei, wenn beide ihn haben. Der Encode-Task liest die Blöcke weiterhin selbst (der Pfeil rechts).

Dazwischen wurde der Encoder selbst nach C++ portiert: Das allein senkte die Laufzeit von 1,86 auf 1,73 s.

Zeitstempel zeigen: Der Encode-Task wartet jetzt weder auf Blöcke (1 ms) noch auf die Analyse (2 ms). Die verbleibenden 1,5 s sind reine serielle Arbeit, davon etwa 0,67 s die Quantisierung der Frequenzbänder.

Vorsprung mit Bremse

Die Analyse spielt genau dieselbe Folge von Aufrufen nach wie opusenc und hält eine Historie von 512 Einträgen, damit sie nichts überschreibt, was der Encode-Task noch liest. Sie läuft höchstens 200 Frames voraus; ist sie so weit vorn, wartet sie, bis der Abstand auf die Hälfte geschrumpft ist – ohne diese Bremse weckte der Encode-Task sie bei jedem Frame, das kostete bei 16 Threads 0,13 s.

Laufzeit · eine Datei, 254 s Musik, comp 10

1,86 → 1,52 s

Read, Distribute, samples, Resample, block ring, Analysis, analysis + signal frames, Encode, Writeframe N−1frame Nframe N+1write bufferReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysis+ signal stageAnalysis+ signal stageAnalysis+ signal stageanalysis + signal framesEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesWriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Analysis, analysis + signal frames, Encode, WriteI/O schedulercompute schedulerI/O schedulerframe N−1frame Nframe N+1write bufferReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysis+ signal stageAnalysis+ signal stageAnalysis+ signal stageanalysis + signal framesEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesEncodeencoder, Ogg pagesWriteasynchronousWriteasynchronousWriteasynchronous

Bild 5 von 11

Die Signalstufe wandert in die Analyse

Auch ein Teil des Encoders hängt nur vom Signal ab, nicht von früheren Entscheidungen: Vorverzerrung, Ton- und Transientenerkennung, die Tonhöhensuche. Der Analyse-Task rechnet diese Signalstufe voraus und gibt Signal-Frames mit seinen Ergebnissen weiter.

Der Encode-Task vergleicht die komplette Eingabe dieser Stufe – etwa 16 KB je Frame – und übernimmt einen Frame nur bei exakter Gleichheit. Läuft die Analyse einmal auseinander, wird es langsamer, nie falsch. Gemessen: 12.725 von 12.725 Frames übernommen. ctOpus braucht jetzt 0,62 der Zeit von opusenc im selben Lauf, vorher 0,74.

Eine neue Messreihe

Ab hier laufen alle Varianten abwechselnd im selben Lauf, opusenc eingeschlossen: An einzelnen Tagen war die Maschine 10 bis 15 % langsamer. Die Zeiten sind deshalb nicht direkt mit den Bildern davor vergleichbar.

Wenn die Analyse abweicht

Nach einem Reset, im Sprachmodus oder während einer Blende passt die Analyse womöglich nicht mehr zum Encoder. Der Encode-Task bemerkt es beim Vergleich und rechnet den Frame selbst.

Laufzeit · dieselbe Datei, alle Varianten im selben Lauf

1,47 s · opusenc 2,38 s

Read, Distribute, samples, Resample, block ring, Analysis, front-end frames, Encode, Writeframe N−1frame Nframe N+1write bufferReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysis+ front end, aheadAnalysis+ front end, aheadAnalysis+ front end, aheadfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamWriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Analysis, front-end frames, Encode, WriteI/O schedulercompute schedulerI/O schedulerframe N−1frame Nframe N+1write bufferReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysis+ front end, aheadAnalysis+ front end, aheadAnalysis+ front end, aheadfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamWriteasynchronousWriteasynchronousWriteasynchronous

Bild 6 von 11

Das Front-End wird vorausberechnet

Nach der Signalstufe rechnet die Analyse jetzt auch das Front-End des nächsten Frames: den Vorfilter, die Transformation in den Frequenzbereich, die Bandenergien und, ab 5 ms langen Frames, Normierung, Bit-Verteilung und Zeit-Frequenz-Entscheidungen. Sie hängen nur von wenigen Werten des seriellen Encoders ab, und die Analyse nimmt sie einfach an – „wie beim letzten Abgleich“.

Der Encode-Task übernimmt ein Front-End nur, wenn diese Werte stimmen. Nach einem Fehlschuss schickt er seine echten Werte zurück (gestrichelt), und die Analyse rechnet ab dort neu. Trefferquote: 12.724 von 12.725 Frames. Komplexität 10 ist jetzt etwa so schnell wie opusenc mit Komplexität 0 (0,97 s im selben Lauf).

Beim Bau gefunden

Ein halb geschriebenes Ergebnis, das zu früh als fertig galt, und eine Verklemmung zwischen Korrektur und Warten auf Blöcke – beides gefunden und behoben, bevor der Schritt fertig war.

Laufzeit · dieselbe Datei, alle Varianten im selben Lauf

1,21 → 1,00 s

Read, Distribute, samples, Resample, block ring, Analysis, Front end, front-end frames, Encode, Helper, Writeframe N−1frame Nframe N+1write buffertrialReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonality, signal stageAnalysistonality, signal stageAnalysistonality, signal stageFront endpre-filter, spectrumFront endpre-filter, spectrumFront endpre-filter, spectrumfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamHelper2nd rounding trialWriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Analysis, Front end, front-end frames, Encode, Helper, WriteI/O schedulercompute schedulerI/O schedulerframe N−1frame Nframe N+1write buffertrialReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonality, signal stageAnalysistonality, signal stageAnalysistonality, signal stageFront endpre-filter, spectrumFront endpre-filter, spectrumFront endpre-filter, spectrumfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamHelper2nd rounding trialWriteasynchronousWriteasynchronousWriteasynchronous

Bild 7 von 11

Das Front-End wird ein Task, ein Helfer rundet mit

Das Front-End wird ein eigener Task: Die Analyse muss für eine Korrektur nicht mehr unterbrochen werden, und der Encode-Task wartet nicht mehr auf sie.

Bei hoher Komplexität quantisiert der Encoder jedes Stereo-Band zweimal, vom selben Zustand aus, mit ab- und mit aufgerundetem Stereowinkel, und behält den günstigeren Versuch. Den zweiten Versuch rechnet jetzt ein Helfer, gleichzeitig mit dem ersten im Encode-Task. Ein Versuch dauert nur 1 bis 2 µs, deshalb warten beide Seiten zuerst aktiv und schlafen erst danach.

Ziel erreicht: Komplexität 10 ist 19 % schneller als opusenc mit Komplexität 0 und 2,7-mal so schnell wie opusenc mit Komplexität 10.

Die Grenze: der Turbo-Takt

Die Quantisierung der Bänder allein braucht 0,72 s; mit zwei weiteren ausgelasteten Kernen 0,76 s, mit sechs 0,80 s. Jeder zusätzlich arbeitende Kern senkt den Turbo-Takt aller Kerne, auch des seriellen.

Laufzeit · dieselbe Datei, alle Varianten im selben Lauf

0,80 s · opusenc comp 0: 0,99 s

Read, Distribute, samples, Resample, block ring, Analysis, Front end, front-end frames, Encode, Helper, Writeframe N−1frame Nframe N+1write buffertrialReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysis+ input stageAnalysis+ input stageAnalysis+ input stageFront end+ stereo anglesFront end+ stereo anglesFront end+ stereo anglesfront-end framesEncodeless workEncodeless workEncodeless workHelper2nd rounding trialWriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Analysis, Front end, front-end frames, Encode, Helper, WriteI/O schedulercompute schedulerI/O schedulerframe N−1frame Nframe N+1write buffertrialReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysis+ input stageAnalysis+ input stageAnalysis+ input stageFront end+ stereo anglesFront end+ stereo anglesFront end+ stereo anglesfront-end framesEncodeless workEncodeless workEncodeless workHelper2nd rounding trialWriteasynchronousWriteasynchronousWriteasynchronous

Bild 8 von 11

Der Encode-Task wird weiter entlastet

Der Graph bleibt gleich, die Arbeit wandert. Die Eingangsstufe – Gleichanteil-Filter, Stille-Erkennung, Frame-Energie, Stereobreite – geht in die Analyse, die Stereowinkel der obersten Ebene gehen in das Front-End. Nach dem letzten Rundungsversuch lässt der Encode-Task Arbeit weg, die nur den Versuchen diente, und er kopiert weniger.

Was der Encode-Task nicht selbst rechnen muss, verkürzt die serielle Kette, und jede Übernahme ist an einen exakten Vergleich gebunden. Im selben Lauf braucht opusenc 1,02 s bei Komplexität 0 und 2,11 s bei Komplexität 10.

Was übrig bleibt

Der Encode-Task braucht jetzt etwa 0,68 s je Datei, davon 0,31 s für seinen eigenen Rundungsversuch. Dieses serielle Rückgrat ist das Ziel des nächsten Schritts.

Laufzeit · dieselbe Datei, alle Varianten im selben Lauf

0,80 → 0,70 s

Read, Distribute, samples, Resample, block ring, Analysis, Signal stage, Front end, front-end frames, Encode, Helper, Speculation, Writeframe N−1frame Nframe N+1trialcopysymbol logReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonalityAnalysistonalityAnalysistonalitySignal stagetransients, pitchSignal stagetransients, pitchSignal stagetransients, pitchFront endpre-filter, spectrumFront endpre-filter, spectrumFront endpre-filter, spectrumfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamHelper2nd rounding trialSpeculationframe N+1 on a copyWriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Analysis, Signal stage, Front end, front-end frames, Encode, Helper, Speculation, WriteI/O schedulercompute schedulerI/O schedulerframe N−1frame Nframe N+1trialcopysymbol logReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonalityAnalysistonalityAnalysistonalitySignal stagetransients, pitchSignal stagetransients, pitchSignal stagetransients, pitchFront endpre-filter, spectrumFront endpre-filter, spectrumFront endpre-filter, spectrumfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamHelper2nd rounding trialSpeculationframe N+1 on a copyWriteasynchronousWriteasynchronousWriteasynchronous

Bild 9 von 11

Spekulation: Frame N+1 auf einer Kopie

Innerhalb eines Frames sind die Bänder seriell, zwischen zwei Frames gehen aber nur zwei kleine Dinge über: ein paar übrige Bits am Ende des Frames und der Zustand des Bitstrom-Kodierers, der erst spät gebraucht wird, wenn überhaupt. Der Encode-Task kopiert deshalb seinen Zustand, bevor er Frame N quantisiert, setzt für die übrigen Bits einen Schätzwert ein, und die Spekulation quantisiert gleichzeitig Frame N+1 auf der Kopie und protokolliert dabei jedes geschriebene Symbol.

Kommt der Encode-Task bei Frame N+1 an, prüft er jede Annahme. Stimmen alle, spielt er nur das Protokoll ab, statt den Frame zu rechnen; sonst bricht die Spekulation an der nächsten Bandgrenze ab. Etwa 96 % der spekulierten Frames werden übernommen – rund 20 % schneller. Für ihren Rundungsversuch hat die Spekulation einen eigenen Helfer.

Die Signalstufe wird ein Task

Mit der Spekulation wurde die Analyse zum Engpass: Frame N+1 fehlte bei 45 % der Abfragen. Mit der Signalstufe auf einem eigenen Task fehlte er nur noch bei etwa 100 von 12.725.

Wann nicht spekuliert wird

Encode-Task und Spekulation wechseln sich ab: Übernimmt der Encode-Task Frame N+1, rechnet er selbst N+2 und spekuliert auf N+3. Ist das echte Ergebnis schon da, bevor die Spekulation starten könnte, unterbleibt sie. Und unter 7 Threads gibt es mehr arbeitende Tasks als Threads – dann kostet die Spekulation mehr, als sie bringt, und bleibt aus.

Laufzeit · Testdatei, 8 Threads, ohne → mit einer Spekulationsstufe

0,70 → 0,56 s

Read, Distribute, samples, Resample, block ring, Analysis, Signal stage, Front end, front-end frames, Encode, Helper, Speculation ×3, Writeframe N−1frame Nframe N+1trialcopiessymbol logsReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonalityAnalysistonalityAnalysistonalitySignal stagetransients, pitchSignal stagetransients, pitchSignal stagetransients, pitchFront endpre-filter, spectrumFront endpre-filter, spectrumFront endpre-filter, spectrumfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamHelper2nd rounding trialSpeculation ×3frames N+1 to N+3WriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Analysis, Signal stage, Front end, front-end frames, Encode, Helper, Speculation ×3, WriteI/O · 1 threadcompute scheduler · 1 thread per logical processorI/O · 1 threadframe N−1frame Nframe N+1trialcopiessymbol logsReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonalityAnalysistonalityAnalysistonalitySignal stagetransients, pitchSignal stagetransients, pitchSignal stagetransients, pitchFront endpre-filter, spectrumFront endpre-filter, spectrumFront endpre-filter, spectrumfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamHelper2nd rounding trialSpeculation ×3frames N+1 to N+3WriteasynchronousWriteasynchronousWriteasynchronous

Bild 10 von 11

Die Kaskade: drei Frames voraus

Mit genug Threads arbeitet der serielle Encoder mehrere Frames voraus: Die Kopie der ersten Spekulation baut die Kopie für die zweite, diese die für die dritte. Bis zu drei Spekulationen rechnen die Frames N+1 bis N+3, während der Encode-Task Frame N quantisiert, ab 12 Threads jede mit einem eigenen Helfer. Das ist der heutige Graph.

Für die Testdatei braucht ctOpus bei Komplexität 10 0,50 s, bei Komplexität 0 0,31 s – opusenc 2,21 s und 0,99 s. ctOpus mit maximaler Komplexität ist doppelt so schnell wie opusenc mit minimaler. Für 45 Stunden Musik braucht es 289,5 Sekunden statt 21 Minuten, 561-fache Echtzeit – alle 343 Dateien bit-identisch.

Wie viele Stufen sich lohnen

Komplexität 10, die Testdatei:

  • 8 Threads: 0,71 s ohne Spekulation, 0,56 s mit einer Stufe, 0,53 s mit zwei
  • 10 Threads: 0,71 s, 0,56 s, 0,46 s
  • drei Stufen mit eigenen Helfern, 12 Threads: 0,455 s

Daraus die Regel: keine Spekulation unter 7 Threads, eine Stufe bei 7, drei ab 8, die Helfer der Stufen erst ab 12 – darunter kosten sie mehr Takt, als sie bringen.

Warum Fibers den Unterschied machen

Rund ein Dutzend Stufen laufen gleichzeitig, und jede ist eine einfache Schleife, die auf ihren nächsten Frame wartet. Auf ThinkMeta ConcurrentTasks hält ein solches Warten nur den Task an; sein Thread rechnet derweil eine andere Stufe, und alle Stufen teilen sich einen Thread je logischem Prozessor. Eine falsche Spekulation wird nicht von außen abgebrochen: Der Task prüft an jedem Band und wartet wieder.

Andere Rechner

  • Intel Core i9-11900K, 8 Kerne: 4,38×
  • Intel Xeon E3-1275 v6, 4 Kerne: 4,12×
  • Intel Core Ultra 7 155H, Notebook: 2,52×

Auf dem Notebook erreichen einzelne Titel 3,53×, die album-langen Dateien am Ende des Laufs nur 1,89× – wahrscheinlich seine Grenze unter Dauerlast. Auf jedem Rechner bit-identisch zu opusenc. Gemessen wurde vor dem asynchronen Lesen und Schreiben (Bild 2).

Faktor gegenüber opusenc · 343 Dateien, 45 h Musik · noch mit synchronem Lesen und Schreiben

4,38×

Read, Distribute, samples, Resample, block ring, Analysis, Signal stage, Front end, front-end frames, Encode, Helper, Speculation ×3, Writeframe N−1frame Nframe N+1trialcopiessymbol logsReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonalityAnalysistonalityAnalysistonalitySignal stagetransients, pitchSignal stagetransients, pitchSignal stagetransients, pitchFront endpre-filter, spectrumFront endpre-filter, spectrumFront endpre-filter, spectrumfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamHelper2nd rounding trialSpeculation ×3frames N+1 to N+3WriteasynchronousWriteasynchronousWriteasynchronous Read, Distribute, samples, Resample, block ring, Analysis, Signal stage, Front end, front-end frames, Encode, Helper, Speculation ×3, WriteI/O · 1 threadcompute scheduler · 1 thread per logical processorI/O · 1 threadframe N−1frame Nframe N+1trialcopiessymbol logsReadasynchronousReadasynchronousReadasynchronousDistributeunpacks WAVsamplesResampleany block, any orderResampleany block, any orderResampleany block, any orderblock ringAnalysistonalityAnalysistonalityAnalysistonalitySignal stagetransients, pitchSignal stagetransients, pitchSignal stagetransients, pitchFront endpre-filter, spectrumFront endpre-filter, spectrumFront endpre-filter, spectrumfront-end framesEncodebands, bitstreamEncodebands, bitstreamEncodebands, bitstreamHelper2nd rounding trialSpeculation ×3frames N+1 to N+3WriteasynchronousWriteasynchronousWriteasynchronous

Bild 11 von 11

Der Graph im Code

Bis hierher war der Graph über den Code verteilt: Alle Tasks griffen in eine gemeinsame Datenstruktur. Jetzt steht der Graph an einer Stelle im Code, so wie er gezeichnet ist: Er besitzt jede Kante und jeden Knoten in der Reihenfolge des Datenflusses, startet die Tasks mit ihren Prioritäten und wartet, bis alle fertig sind. An keiner Rechnung ändert sich etwas.

Der Weg mit einem einzigen Encode-Task bleibt als Referenz – etwa für Eingaben mit 48 kHz, die nichts zu resampeln haben. Mit dem asynchronen Lesen und Schreiben ist er genau der Graph von Bild 2.

Struktur zum Nulltarif

Gemessen gegen den Stand davor, alt und neu abwechselnd, 11 Paare: alle Threads 0,438 → 0,440 s, 4 Threads 0,747 → 0,757 s, ein Thread 1,836 → 1,841 s. Die einzelnen Paare streuen von −8 % bis +9 % – Rauschen. Die Struktur kostet nichts, und jede Prüfung bleibt bit-identisch.

Laufzeit · Testdatei, alle Threads, vor → nach dem Umbau im selben Lauf

0,438 → 0,440 s

Selbst ausprobieren

ctOpus, die Referenz-Version von opusenc und der Benchmark für Ihre eigene Musik liegen auf GitHub. Sie haben eine Arbeitslast, die jeden Kern nutzen soll? Nehmen Sie Kontakt auf.

An unhandled error has occurred. Reload