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?
The System.Threading.Channels library currently has three built-in
Channel<T>implementations:UnboundedChannel<T>SingleConsumerUnboundedChannel<T>BoundedChannel<T>From those implementations, the less allocatey is the
BoundedChannel<T>. If you don't want bounds, you can configure it withcapacity: Int32.MaxValue. TheUnboundedChannel<T>is based internally on aConcurrentQueue<T>, which is a very performant and non-contentious collection (it's lock-free). The allocations are a necessary compromise for being lock-free. TheBoundedChannel<T>is based on an internalDeque<T>collection, which is synchronized withlocks. 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 aConcurrentQueue<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 anIProducerConsumerCollection<T>based on a synchronizedQueue<T>, and pass it as an argument to theBlockingCollection<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. TheCancellationTokenmust register a callback in order to have instantaneous effect, and callbacks without allocations are not possible. My suggestion is to get rid of theCancellationToken, and find some other way of completing gracefully. Like using theChannelWriter<T>.Completeor theBlockingCollection<T>.CompleteAddingmethods.