ctOpus writes exactly the same Opus files as opusenc, byte for byte, and spreads a single file over all cores. At maximum complexity it is twice as fast as opusenc at minimum complexity; on 45 hours of music it is 4.4 times as fast as opusenc with the same settings. Opus is sequential by design: every frame continues the bitstream, the energy prediction and the filter memories of the frame before it. These eleven pictures show how, step by step, a task graph on ThinkMeta ConcurrentTasks grew out of it – up to tasks that compute future frames ahead on speculation.

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

Picture 1 of 11

The reference: opusenc

opusenc reads the WAV file, resamples it from 44.1 to 48 kHz, encodes it and writes Ogg pages – all on one thread, one after another. While it reads or writes, the thread waits for the file. Every stage carries state from frame to frame: the filter memories, the energy prediction, the bit reservoir and the bitstream. One core works, the others wait.

These bits are the yardstick: every following picture has to write exactly the same .opus file, byte for byte. ctOpus starts from a port of the encoder, function by function: on one thread it is as fast as opusenc (2.09 s) and bit-identical.

Where the time goes

A profile of opusenc at complexity 10: the resampler takes 34.5 %, the quantization of the frequency bands 25.1 %, the tonality analysis 8.2 %, the pre-filter 6.6 %, the transform and the time-frequency decisions together about 9 %. The test music is 44.1 kHz stereo throughout, so the resampler runs for every file.

Complexity

Opus lets you trade speed for quality, from complexity 0 (fastest) to 10 (best, the default). On the test file opusenc needs 2.16 s at complexity 10 and 0.97 s at complexity 0. The goal of ctOpus: complexity 10 faster than opusenc at complexity 0.

Run time · one file, 254 s of music, 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

Picture 2 of 11

Reading and writing asynchronously

File input and output move out of the encoder. Read and Write become tasks on the I/O scheduler; a single task encodes and does everything opusenc does. Read reads ahead with five overlapping requests of 1 MB into a read buffer, Encode only takes finished blocks from it. Encode copies its Ogg pages into a write buffer, and Write puts full blocks into the file, two requests at a time.

While Read or Write wait for the disk, they occupy no thread: the I/O scheduler runs the other task in the meantime. Measured with the file fresh from the disk, it is as fast or faster – and bit-identical in every check. With a single encoding task both ways take the same time; with the full graph of picture 10 the same step saves 2 to 3 %, because there the encoding task wrote the Ogg pages on the critical path: 16 threads 0.486 → 0.474 s, 4 threads 0.780 → 0.760 s.

Read buffer and write buffer

The read buffer holds 8 blocks of 1 MB, the write buffer 8 blocks of 64 KB. A full buffer stops the task that fills it, an empty one the task that drains it. opusenc, in contrast, writes every Ogg page with a call of its own – several thousand times for the test file.

Measured cold

Before every run the input was copied afresh and unbuffered, so it really came from the disk. One encoding task: 1.891 s synchronous, 1.876 s asynchronous – within the noise; it consumes its input at about 25 MB/s, far less than the disk delivers. Block size and the number of requests in flight make no difference (±3 %).

Run time · file fresh from the disk, one encoding 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

Picture 3 of 11

Resampling in parallel

Distribute unpacks the WAV data into the samples – on the compute scheduler with high priority (bold border), because every resampler waits for its blocks. The resampler computes every output sample from the input alone, its filter phase follows from the position – so blocks can be computed independently, on every core, in any order, and give exactly the same values. Encode takes the blocks from the block ring in order.

The resampler has left the critical path: 1.86 s at complexity 10, and 0.70 s instead of 0.97 s at complexity 0 (1.38×). What remains is almost only the serial encoder.

Samples and block ring

The whole input lies in reserved memory, filled by Distribute and released as soon as every block has read it. Distribute releases a block as soon as its input is complete and a slot in the ring is free; a slot holds 16,384 resampled samples per channel.

Why Distribute computes

Since reading is asynchronous, Distribute no longer waits for the disk, only for other tasks – and it only computes: converting samples and splitting them into channels. So it belongs on the compute scheduler, which is far from saturated. Measured against the I/O scheduler: 16 threads 0.474 → 0.466 s, 4 threads 0.760 → 0.744 s, bit-identical.

Same sums, same bits

The parallel blocks add up in the same order as opusenc, and at the end of the file Distribute appends the same extension of the signal: a continuation of the last 480 samples, then zeros. The encoder sees exactly the same numbers.

Run time · one file, 254 s of music, 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

Picture 4 of 11

The analysis runs ahead

The tonality analysis of a frame depends only on the signal, not on the decisions of the encoder. It becomes a task of its own that runs ahead of Encode and puts its result for every frame into a ring. The block ring now has two readers: a slot is only free again when both have it. Encode still reads the blocks itself (the arrow on the right).

In between, the encoder itself was ported to C++: that alone took the run time from 1.86 to 1.73 s.

Time stamps show: Encode now waits neither for blocks (1 ms) nor for the analysis (2 ms). The 1.5 s that remain are pure serial work, about 0.67 s of it the quantization of the frequency bands.

A head start with a brake

The analysis replays exactly the same sequence of calls as opusenc and keeps a history of 512 entries, so it never overwrites what Encode still reads. It runs at most 200 frames ahead; once that far, it waits until the gap has shrunk to half – without that brake, Encode woke it at every frame, which cost 0.13 s with 16 threads.

Run time · one file, 254 s of music, 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

Picture 5 of 11

The signal stage joins the analysis

Part of the encoder depends only on the signal as well, not on earlier decisions: pre-emphasis, tone and transient detection, the pitch search. The analysis task computes this signal stage ahead and passes signal frames along with its results.

Encode compares the complete input of that stage – about 16 KB per frame – and only takes a frame over if it is exactly equal. If the analysis ever diverges, it gets slower, never wrong. Measured: 12,725 of 12,725 frames taken over. ctOpus now needs 0.62 of the time of opusenc in the same run, before 0.74.

A new series of measurements

From here on all variants run alternately in the same run, opusenc included: on some days the machine was 10 to 15 % slower. The times are therefore not directly comparable with the pictures before.

When the analysis diverges

After a reset, in speech mode or during a fade, the analysis may no longer match the encoder. Encode notices it in the comparison and computes the frame itself.

Run time · the same file, all variants in the same run

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

Picture 6 of 11

The front end is computed ahead

After the signal stage the analysis now also computes the front end of the next frame: the pre-filter, the transform into the frequency domain, the band energies and, for frames of 5 ms and more, normalization, bit allocation and time-frequency decisions. They depend on only a few values of the serial encoder, and the analysis simply assumes them – “as at the last sync”.

Encode takes a front end over only if those values match. After a miss it sends its real values back (dashed), and the analysis recomputes from there. Hit rate: 12,724 of 12,725 frames. Complexity 10 is now about as fast as opusenc at complexity 0 (0.97 s in the same run).

Found while building

A result that counted as finished while it was only half written, and a deadlock between the correction and the wait for blocks – both found and fixed before the step was done.

Run time · the same file, all variants in the same run

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

Picture 7 of 11

The front end becomes a task, a helper takes the rounding trial

The front end becomes a task of its own: the analysis no longer has to be interrupted for a correction, and Encode no longer waits for it.

At high complexity the encoder quantizes every stereo band twice, from the same state, with the stereo angle rounded down and up, and keeps the cheaper trial. The second trial now runs on a helper, at the same time as the first one in Encode. A trial takes only 1 to 2 µs, so both sides first wait actively and only then go to sleep.

Goal reached: complexity 10 is 19 % faster than opusenc at complexity 0, and 2.7 times as fast as opusenc at complexity 10.

The limit: turbo clock

The quantization of the bands alone takes 0.72 s; with two more busy cores 0.76 s, with six 0.80 s. Every additional busy core lowers the turbo clock of all cores, the serial one included.

Run time · the same file, all variants in the same run

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

Picture 8 of 11

Taking more work off the encoding task

The graph stays the same; the work moves. The input stage – DC filter, silence detection, frame energy, stereo width – goes into the analysis, the top-level stereo angles into the front end. After the last rounding trial Encode skips work that only served the trials, and it copies less.

Whatever Encode does not compute itself shortens the serial chain, and every takeover is tied to an exact comparison. In the same run opusenc needs 1.02 s at complexity 0 and 2.11 s at complexity 10.

What is left

Encode now needs about 0.68 s per file, 0.31 s of it for its own rounding trial. This serial backbone is the target of the next step.

Run time · the same file, all variants in the same run

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

Picture 9 of 11

Speculation: frame N+1 on a copy

Within a frame the bands are serial, but from one frame to the next only two small things pass: a few bits left over at the end of the frame and the state of the bitstream coder, which is needed late, if at all. So Encode copies its state before it quantizes frame N, puts in an estimate for the leftover bits, and the speculation quantizes frame N+1 on the copy at the same time, recording every symbol it writes.

When Encode reaches frame N+1, it checks every assumption. If all hold, it simply replays the recorded log instead of computing the frame; otherwise the speculation stops at the next band boundary. About 96 % of the speculated frames are taken over – about 20 % faster. The speculation has a helper of its own for its rounding trial.

The signal stage becomes a task

With the speculation, the analysis became the bottleneck: frame N+1 was missing in 45 % of the queries. With the signal stage on a task of its own, it was missing in only about 100 of 12,725.

When not to speculate

Encode and the speculation take turns: if Encode takes frame N+1 over, it computes N+2 itself and speculates on N+3. If the real result is already there before a speculation could start, there is none. And below 7 threads there are more busy tasks than threads – then the speculation costs more than it gains and stays off.

Run time · test file, 8 threads, without → with one speculation stage

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

Picture 10 of 11

The cascade: three frames ahead

With enough threads the serial encoder works several frames ahead: the copy of the first speculation builds the copy for the second, and that one the copy for the third. Up to three speculations compute frames N+1 to N+3 while Encode quantizes frame N, each with a helper of its own from 12 threads on. This is today's graph.

On the test file ctOpus needs 0.50 s at complexity 10 and 0.31 s at complexity 0 – opusenc 2.21 s and 0.99 s. ctOpus at maximum complexity is twice as fast as opusenc at minimum. On 45 hours of music it needs 289.5 seconds instead of 21 minutes, 561 times real time – all 343 files bit-identical.

How many levels pay off

Complexity 10, the test file:

  • 8 threads: 0.71 s without speculation, 0.56 s with one level, 0.53 s with two
  • 10 threads: 0.71 s, 0.56 s, 0.46 s
  • three levels with their own helpers, 12 threads: 0.455 s

Hence the rule: no speculation below 7 threads, one level at 7, three from 8, the helpers of the levels only from 12 – below that they cost more clock than they gain.

Why fibers make the difference

About a dozen stages run at once, and each is written as a plain loop that waits for its next frame. On ThinkMeta ConcurrentTasks such a wait suspends only the task; its thread meanwhile runs another stage, so all stages share one thread per logical processor. A wrong speculation is not cancelled from outside: the task checks at every band and goes back to waiting.

Other CPUs

  • Intel Core i9-11900K, 8 cores: 4.38×
  • Intel Xeon E3-1275 v6, 4 cores: 4.12×
  • Intel Core Ultra 7 155H, notebook: 2.52×

On the notebook single tracks reach 3.53×, the album-length files at the end of the run only 1.89× – most likely its limit under sustained load. Bit-identical to opusenc on every machine. These corpus runs were made before reading and writing became asynchronous (picture 2).

Speed-up over opusenc · 343 files, 45 h of music · still with synchronous reading and writing

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

Picture 11 of 11

The graph in the code

Up to here the graph was spread over the code: all tasks reached into one shared structure. Now the graph lives in one place in the code, just as it is drawn: it owns every edge and every node in the order of the data flow, starts the tasks with their priorities and waits until all of them are done. Not a single calculation changes.

The single-task path stays as the reference – for input at 48 kHz that has nothing to resample, for example. With asynchronous reading and writing it is exactly the graph of picture 2.

Structure for free

Measured against the state before, old and new alternately, 11 pairs: all threads 0.438 → 0.440 s, 4 threads 0.747 → 0.757 s, one thread 1.836 → 1.841 s. The single pairs scatter from −8 % to +9 % – noise. The structure costs nothing, and every check stays bit-identical.

Run time · test file, all threads, before → after the rework in the same run

0.438 → 0.440 s

Try it yourself

ctOpus, the reference build of opusenc and the benchmark for your own music are on GitHub. Have a workload that should use every core? Get in touch.

An unhandled error has occurred. Reload