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 lock
s. 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 CancellationToken
s 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.