In this blog post I will show you 4 different ways of creating an array and how they differ from each other.
new
The first one is pretty obvious, we use the new
operator to create a new array:
var myArray = new int[100];
We create an array with 100 elements. Important here is that "we" have to do the heavy work of creating the array, but on the other side we are not responsible to free up the memory. The dear Garbage Collector (GC) does that for us.
ArrayPool
You can imagine an ArrayPool like a car pool. You will share the array with others. When you need an array, you can "rent" it from the pool, but you have to give back after you are done. That is the big difference to creating an array with new
. The responsibility of clearing up that memory shifts to you.
var myArray = ArrayPool<int>.Shared.Rent(100);
// Do something
ArrayPool<int>.Shared.Return(myArray);
Be sure to return your array otherwise the ArrayPool
can "starve" and has to create new elements in the pool, which can hit your performance. Also what is special here is, that we use the Shared
instance of the ArrayPool
. Special word to Rent
. Its only parameter is called minimumLength
, that means you array is guaranteed to have the minimumLength
but can also be longer than that. If you want to know more, check this post out.
GC.AllocateArray
GC.AllocateArray
is another way of creating an array. It has two parameters: First is the length
plus a boolean flag describing whether or not the array should be pinned.
Pinned arrays mean that the GC should not move around the memory block associated with the array. This is useful if you have to work with unmanaged resources, otherwise there are not that many reasons to do so.
By the way you can achieve the same with "normal" arrays and the fixed
keyword. Here is how you would use the GC.AllocateArray
:
var myArray = GC.AllocateArray<int>(100);
GC.AllocateUninitializedArray
This API Call does almost the same as GC.AllocateArray<int>
with one little twist. Normally in .NET arrays are initialized with the default value, so if you have something like: var myArray = new int[2]
; Then myArray[0] == 0;
and myArray[1] == 0
. In terms of performance that can squeeze out a little bit more than the initialized version. By the way, you can achieve the same with the SkipLocalsInitAttribute
.
var myArray = GC.AllocateUninitializedArray<int>(100);
GC Remarks
The last two methods shown above are meant for micro-optimization. Be aware that those methods also have some constraints, for example you can't use GC.AllocateArray
and GC.AllocateUninitializedArray
with reference types. One can check with RuntimeHelpers.IsReferenceOrContainsReferences<T>()
if a type is eligible or not.
Comparison
Let's check how those different types are doing with different array sizes. Disclaimer: Use new[]
in almost all cases. The other options are meant for hot path optimizations and not for general use. So benchmark your use case and decide afterwards.
[MemoryDiagnoser]
public class ArrayBenchmark
{
[Params(10, 100, 1_000, 10_000, 100_000, 1_000_000)]
public int ArraySize { get; set; }
[Benchmark(Baseline = true)]
public int[] NewArray() => new int[ArraySize];
[Benchmark]
public int[] ArrayPoolRent() => ArrayPool<int>.Shared.Rent(ArraySize);
[Benchmark]
public int[] GCZeroInitialized() => GC.AllocateArray<int>(ArraySize);
[Benchmark]
public int[] GCZeroUninitialized() => GC.AllocateUninitializedArray<int>(ArraySize);
}
Results:
| Method | ArraySize | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------- |---------- |-----------------:|---------------:|---------------:|-----------------:|------:|--------:|---------:|---------:|---------:|------------:|
| NewArray | 10 | 4.674 ns | 0.2270 ns | 0.6175 ns | 4.500 ns | 1.00 | 0.00 | 0.0153 | - | - | 64 B |
| ArrayPoolRent | 10 | 16.858 ns | 0.7670 ns | 2.2008 ns | 16.246 ns | 3.70 | 0.56 | 0.0210 | - | - | 88 B |
| GCZeroInitialized | 10 | 32.604 ns | 0.6481 ns | 0.5412 ns | 32.407 ns | 6.83 | 0.75 | 0.0153 | - | - | 64 B |
| GCZeroUninitialized | 10 | 5.170 ns | 0.3419 ns | 0.9588 ns | 4.884 ns | 1.12 | 0.27 | 0.0153 | - | - | 64 B |
| | | | | | | | | | | | |
| NewArray | 100 | 18.643 ns | 0.4770 ns | 1.3057 ns | 18.034 ns | 1.00 | 0.00 | 0.1014 | - | - | 424 B |
| ArrayPoolRent | 100 | 33.094 ns | 0.8942 ns | 2.4628 ns | 32.237 ns | 1.79 | 0.18 | 0.1281 | - | - | 536 B |
| GCZeroInitialized | 100 | 47.771 ns | 1.1578 ns | 3.3033 ns | 47.263 ns | 2.59 | 0.25 | 0.1013 | - | - | 424 B |
| GCZeroUninitialized | 100 | 18.287 ns | 0.4325 ns | 0.3611 ns | 18.164 ns | 0.98 | 0.08 | 0.1014 | - | - | 424 B |
| | | | | | | | | | | | |
| NewArray | 1000 | 156.640 ns | 2.9920 ns | 2.7987 ns | 157.671 ns | 1.00 | 0.00 | 0.9613 | - | - | 4,024 B |
| ArrayPoolRent | 1000 | 116.015 ns | 0.8502 ns | 0.7953 ns | 115.891 ns | 0.74 | 0.01 | 0.9813 | - | - | 4,120 B |
| GCZeroInitialized | 1000 | 186.634 ns | 3.8069 ns | 8.7471 ns | 185.534 ns | 1.24 | 0.05 | 0.9613 | - | - | 4,024 B |
| GCZeroUninitialized | 1000 | 111.039 ns | 2.3022 ns | 3.9712 ns | 110.275 ns | 0.71 | 0.03 | 0.9587 | - | - | 4,024 B |
| | | | | | | | | | | | |
| NewArray | 10000 | 1,450.991 ns | 28.9030 ns | 59.0412 ns | 1,454.230 ns | 1.00 | 0.00 | 9.5234 | 0.0019 | - | 40,024 B |
| ArrayPoolRent | 10000 | 678.110 ns | 13.5437 ns | 16.1228 ns | 681.002 ns | 0.47 | 0.02 | 15.6240 | 0.0010 | - | 65,560 B |
| GCZeroInitialized | 10000 | 1,352.078 ns | 20.4704 ns | 18.1465 ns | 1,349.767 ns | 0.93 | 0.05 | 9.5234 | 0.0019 | - | 40,024 B |
| GCZeroUninitialized | 10000 | 419.071 ns | 8.2441 ns | 10.1245 ns | 417.218 ns | 0.29 | 0.01 | 9.5234 | 0.0005 | - | 40,024 B |
| | | | | | | | | | | | |
| NewArray | 100000 | 22,770.844 ns | 531.6404 ns | 1,473.1730 ns | 22,217.963 ns | 1.00 | 0.00 | 124.9695 | 124.9695 | 124.9695 | 400,066 B |
| ArrayPoolRent | 100000 | 18,859.646 ns | 451.2290 ns | 1,287.3816 ns | 18,214.809 ns | 0.83 | 0.07 | 166.6565 | 166.6565 | 166.6565 | 524,317 B |
| GCZeroInitialized | 100000 | 22,818.945 ns | 456.0302 ns | 1,293.6817 ns | 22,739.250 ns | 1.01 | 0.08 | 124.9695 | 124.9695 | 124.9695 | 400,066 B |
| GCZeroUninitialized | 100000 | 13,473.716 ns | 436.1365 ns | 1,265.3112 ns | 13,172.961 ns | 0.60 | 0.07 | 124.9847 | 124.9847 | 124.9847 | 400,025 B |
| | | | | | | | | | | | |
| NewArray | 1000000 | 1,108,601.149 ns | 22,592.9715 ns | 18,866.1545 ns | 1,114,450.342 ns | 1.00 | 0.00 | 139.6484 | 139.4043 | 139.4043 | 4,000,068 B |
| ArrayPoolRent | 1000000 | 219,394.967 ns | 19,411.7449 ns | 57,235.9678 ns | 231,375.549 ns | 0.22 | 0.02 | 23.1934 | 23.1934 | 23.1934 | 4,194,329 B |
| GCZeroInitialized | 1000000 | 1,146,780.574 ns | 14,884.3974 ns | 13,922.8745 ns | 1,144,215.051 ns | 1.03 | 0.02 | 140.8691 | 140.6250 | 140.6250 | 4,000,070 B |
| GCZeroUninitialized | 1000000 | 190,029.061 ns | 16,728.2807 ns | 49,323.7129 ns | 200,659.369 ns | 0.15 | 0.05 | 22.7051 | 22.7051 | 22.7051 | 4,000,023 B |
As you can see the ArrayPool
use a bit more space than asked for. Also for smaller arrays there is no real usage of the alternative API's. Again, please benchmark your case first and be aware of the consequences and "new" responsibilities.
Resources
- Performance comparison repo can be found here