I have following benchmark which read string from file using Stack allocation, Heap allocation and ArrayPool allocation.
I would expect that Stack allocation is fastest, because it is just stack pointer increment, but according to benchmark ArrayPool is slightly faster.
How it is possible?
static void Main(string[] args)
{
BenchmarkRunner.Run<BenchmarkRead>();
}
using BenchmarkDotNet.Attributes;
using System;
using System.Buffers;
using System.IO;
using System.Linq;
namespace RealTime.Benchmark
{
[MemoryDiagnoser]
public class BenchmarkRead
{
const string TestFile = "TestFiles/animals.txt";
public BenchmarkRead()
{
Directory.CreateDirectory(Path.GetDirectoryName(TestFile));
// cca 100 KB of text
string content = string.Concat(Enumerable.Repeat("dog,cat,spider,cat,bird,", 4000));
File.WriteAllText(TestFile, content);
}
[Benchmark]
public void ReadFileOnPool() => ReadFileOnPool(TestFile);
[Benchmark]
public void ReadFileOnHeap() => ReadFileOnHeap(TestFile);
[Benchmark]
public void ReadFileOnStack() => ReadFileOnStack(TestFile);
public void ReadFileOnHeap(string filename)
{
string text = File.ReadAllText(filename);
// ....call parse
}
public void ReadFileOnStack(string filename)
{
Span<byte> span = stackalloc byte[1024 * 200];
using (var stream = File.OpenRead(filename))
{
int count = stream.Read(span);
if (count == span.Length)
throw new Exception($"Buffer size {span.Length} too small, use array pooling.");
span = span.Slice(0, count);
// ....call parse
}
}
public void ReadFileOnPool(string filename)
{
ArrayPool<byte> pool = ArrayPool<byte>.Shared;
using (var stream = File.OpenRead(filename))
{
long len = stream.Length;
byte[] buffer = pool.Rent((int)len);
try
{
int count = stream.Read(buffer, 0, (int)len);
if (count != len)
throw new Exception($"{count} != {len}");
Span<byte> span = new Span<byte>(buffer).Slice(0, count);
// ....call parse
}
finally
{
pool.Return(buffer);
}
}
}
}
}
Results:
| Method | Mean | Gen 0/1k Op | Gen 2/1k Op |Al. memory/Op|
|---------------- |---------:|------------:|------------:|------------:|
| ReadFileOnPool | 109.9 us | 0.1221 | - | 480 B |
| ReadFileOnHeap | 506.0 us | 87.8906 | 58.5938 | 393440 B |
| ReadFileOnStack | 115.2 us | 0.1221 | - | 480 B |
Thread SafetyThis class is thread-safe. All members may be used by multiple threads concurrently.
By using stackalloc instead of a heap allocated array, you create less GC pressure (the GC needs to run less), you don't need to pin the arrays down, it's faster to allocate than a heap array, an it is automatically freed on method exit (heap allocated arrays are only deallocated when GC runs).
Span<byte> span = stackalloc byte[1024 * 200]
will be zero-initialized due to InitLocals.
byte[] buffer = pool.Rent((int)len);
will not be zero-initialized at all.
So you have reached the point where the cost of zero initializing your local array is more expensive than the whole Rent()
routine.
I actually created a nuget package exatly for this a few months ago https://github.com/josetr/InitLocals but we'll soon have something similar from Microsoft as well: https://github.com/dotnet/corefx/issues/29026.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With