Home > OS >  Channel/BlockingCollection alloc free alternatives?
Channel/BlockingCollection alloc free alternatives?

Time:11-17

I recently benchmarked my framework and noticed that it allocates tons of garbage.

I'm using a Channel<T> and the TryRead or ReadAsync operation allocates memory every single call. So I exchanged that with a BlockingCollection<T> which also allocates memory during TryTake.

I used a unbounded channel with a single writer/reader. And a normal BlockingCollection<T>.

// Each thread runs this, jobmeta is justa struct 
while (!token.IsCancellationRequested)
{
    var jobMeta = await Reader.ReadAsync(token);  // <- allocs here
    jobMeta.Job.Execute();
    jobMeta.JobHandle.Notify();
}

The profiler told me that all allocations are caused by the ChannelReader.ReadAsync method. Unfortunately I can't show the full code, however since I use them in a hot path, I need to avoid allocations at all cost.

Are there any alternatives which do not allocate memory during read/write/get and behave the same (Concurrent classes for producer/consumer multithreading) ? How could I implement one by myself?

CodePudding user response:

The System.Threading.Channels library currently has three built-in Channel<T> implementations:

From those implementations, the less allocatey is the BoundedChannel<T>. If you don't want bounds, you can configure it with capacity: Int32.MaxValue. The UnboundedChannel<T> is based internally on a ConcurrentQueue<T>, which is a very performant and non-contentious collection (it's lock-free). The allocations are a necessary compromise for being lock-free. The BoundedChannel<T> is based on an internal Deque<T> collection, which is synchronized with locks. It allocates memory only when it has to expand the capacity of it's backing array, which will happen only a few times during the lifetime of the channel.

The BlockingCollection<T> is also based on a ConcurrentQueue<T> by default, so it has the same advantages and disadvantages. If you want to reduce the allocations (reducing also the performance and increasing the contention), you could implement an IProducerConsumerCollection<T> based on a synchronized Queue<T>, and pass it as an argument to the BlockingCollection<T> constructor. You could use this answer as a starting point.

Finally passing CancellationTokens to any of these APIs will result to allocations no matter what. The CancellationToken must register a callback in order to have instantaneous effect, and callbacks without allocations are not possible. My suggestion is to get rid of the CancellationToken, and find some other way of completing gracefully. Like using the ChannelWriter<T>.Complete or the BlockingCollection<T>.CompleteAdding methods.

  • Related