This post started from discussion on SO:Why LINQ method Any does not check Count? After it I decided to check the most common collections on performance in methodsAnyandCount.
First of all, we create new extension method:
publicstaticclassEnumerableExtensions
{
publicstaticbool CustomAny<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
thrownew ArgumentNullException(nameof(source));
}
if (source is ICollection<TSource> collection)
{
return collection.Count != 0;
}
using (var enumerator = source.GetEnumerator())
{
if (enumerator.MoveNext())
returntrue;
}
returnfalse;
}
}
Benchmarks are grouped by category — type of collection. MethodAnyis defaultEnumerable.Any,Custom— our custom implementationEnumerableExtensions.CustomAny.
Categories : Collection type
_size : Number of elements in collection
Mean : Arithmetic mean of all measurements
Error : Half of99.9% confidence interval
StdDev : Standard deviation of all measurements
Median : Value separating the higher half of all measurements (50th percentile)
Scaled : Mean(CurrentBenchmark) / Mean(BaselineBenchmark)
ScaledSD : Standard deviation of ratio of distribution of [CurrentBenchmark] and [BaselineBenchmark]
1 ns : 1 Nanosecond (0.000000001 sec)
As we see, in common the new method withCountis faster than the old one or the same, except some collections. Lets examine them in more detail.
Array
Our new method gives much worse results. Why? Answer is not obvious. Array can be converted toICollection<T>, but it takes much time due to internal implementation of array in .NET.
So, lets add array check:
publicstaticclassEnumerableExtensions
{
publicstaticbool CustomAny<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
thrownew ArgumentNullException(nameof(source));
}
if (source is TSource[] array)
{
return array.Length != 0;
}
if (source is ICollection<TSource> collection)
{
return collection.Count != 0;
}
using (var enumerator = source.GetEnumerator())
{
if (enumerator.MoveNext())
returntrue;
}
returnfalse;
}
}
Stack and Queue
These collections do not implementICollection<T>interface:
public classStack<T> : IEnumerable<T>, IEnumerable, ICollection, IReadOnlyCollection<T>
public classQueue<T> : IEnumerable<T>, IEnumerable, ICollection, IReadOnlyCollection<T>
OnlyICollection, we can use it as the most common:
publicstaticclassEnumerableExtensions
{
publicstaticbool CustomAny<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
thrownew ArgumentNullException(nameof(source));
}
if (source is TSource[] array)
{
return array.Length != 0;
}
if (source is ICollection<TSource> collection)
{
return collection.Count != 0;
}
if (source is ICollection baseCollection)
{
return baseCollection.Count != 0;
}
using (var enumerator = source.GetEnumerator())
{
if (enumerator.MoveNext())
returntrue;
}
returnfalse;
}
}
Second run result
Method
Categories
_size
Mean
Error
StdDev
Scaled
ScaledSD
Any
List
1000
37.52 ns
0.6950 ns
0.6501 ns
1.00
0.00
Custom
List
1000
19.69 ns
0.2409 ns
0.2254 ns
0.53
0.01
Any
Array
1000
27.11 ns
0.5799 ns
1.0004 ns
1.00
0.00
Custom
Array
1000
18.87 ns
0.0513 ns
0.0455 ns
0.70
0.02
Any
HashSet
1000
39.42 ns
0.3633 ns
0.3034 ns
1.00
0.00
Custom
HashSet
1000
18.76 ns
0.0440 ns
0.0367 ns
0.48
0.00
Any
Dictinary
1000
50.13 ns
0.8848 ns
0.8276 ns
1.00
0.00
Custom
Dictinary
1000
19.95 ns
0.1613 ns
0.1509 ns
0.40
0.01
Any
Stack
1000
39.56 ns
0.5305 ns
0.4702 ns
1.00
0.00
Custom
Stack
1000
31.66 ns
0.1149 ns
0.1075 ns
0.80
0.01
Any
Queue
1000
42.90 ns
0.2227 ns
0.2083 ns
1.00
0.00
Custom
Queue
1000
31.69 ns
0.1502 ns
0.1331 ns
0.74
0.00
Any
SortedList
1000
40.53 ns
0.1220 ns
0.1019 ns
1.00
0.00
Custom
SortedList
1000
19.71 ns
0.0362 ns
0.0339 ns
0.49
0.00
Any
SortedSet
1000
219.88 ns
0.5931 ns
0.4289 ns
1.00
0.00
Custom
SortedSet
1000
20.68 ns
0.0420 ns
0.0328 ns
0.09
0.00
Ok, in all tests our method is better than default one. But there is one collection, where result is 10 times better — SortedSet.
SortedSet — what’s wrong?
Why this collection is so bad in simple methodAny? As we know,Anyjust create enumerator and tries to take first element. Open source code ofSortedSet:
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return (IEnumerator<T>) new SortedSet<T>.Enumerator(this);
}
GetEnumeratorcreates new struct of typeEnumerator, constructor getsSortedSet<T>as a parameter:
It is worth noting thatSortedSetis built on the basis of binary tree, and methodIntializebypasses all nodes in tree and puts elements into the stack. So, when you create enumerator ofSortedSet, internal implementation creates new collections with all elements. The more elements you have, the more time it takes:
Method
_size
Mean
Error
StdDev
Scaled
Any
1000
230.64 ns
4.5659 ns
6.9727 ns
1.00
Custom
1000
22.23 ns
0.4906 ns
0.7036 ns
0.10
Any
10000
288.27 ns
1.3822 ns
1.0791 ns
1.00
Custom
10000
21.72 ns
0.1599 ns
0.1417 ns
0.08
Any
100000
343.54 ns
6.7375 ns
9.8757 ns
1.00
Custom
100000
21.70 ns
0.3254 ns
0.3043 ns
0.06
Ok, we checked simple most popular collections, and I think, we can useCountproperty instead ofEnumerable.Any. But is it safe to useCountevery time? Is there a collection, whereCounttakes more time than creating enumerator?
Concurrent collections
All collections we checked before are not thread-safe, and can not be used in multi-threading applications. There is a special class of collections — concurrent collections:
ConcurrentBag
ConcurrentDictionary
ConcurrentQueue
ConcurrentStack
I will fill collections from different threads to simulate multi-threading environment:
[GlobalSetup]
publicvoidSetUp(){
_bag = new ConcurrentBag<int>();
_dictionary = new ConcurrentDictionary<int, int>();
_queue = new ConcurrentQueue<int>();
_stack = new ConcurrentStack<int>();
var tasksCount = 10;
var batch = _size / tasksCount;
var tasks = new Task[tasksCount];
for (int i = 0; i < tasksCount; i++)
{
var task = i;
tasks[task] = Task.Run(() =>
{
varfrom = task * batch;
var to = (task + 1) * batch;
for (int j = from; j < to; j++)
{
_bag.Add(j);
_dictionary[j] = j;
_queue.Enqueue(j);
_stack.Push(j);
}
});
}
Task.WaitAll(tasks);
}
Concurrent collections result
Method
Categories
_size
Mean
Error
StdDev
Scaled
ScaledSD
Any
ConcurrentBag
1000
7,065.75 ns
37.9584 ns
33.6491 ns
1.00
0.00
Custom
ConcurrentBag
1000
267.89 ns
5.3096 ns
4.7068 ns
0.04
0.00
Any
ConcurrentDictionary
1000
39.29 ns
0.7618 ns
0.5948 ns
1.00
0.00
Custom
ConcurrentDictionary
1000
6,319.42 ns
27.8553 ns
26.0558 ns
160.88
2.45
Any
ConcurrentStack
1000
28.08 ns
0.1495 ns
0.1399 ns
1.00
0.00
Custom
ConcurrentStack
1000
3,179.18 ns
23.4161 ns
21.9035 ns
113.23
0.93
Any
ConcurrentQueue
1000
72.12 ns
1.4450 ns
1.9779 ns
1.00
0.00
Custom
ConcurrentQueue
1000
48.28 ns
0.3934 ns
0.3487 ns
0.67
0.02
We have got different unpredictable results. SomewhereAnyis very slow, and vice versa.
ConcurrentBag
MethodGetEnumeratorofConcurrentBagworks likeSortedSet: it locks collection and copies all elements into new list:
Countproperty locks collection and just sum up all internal lists counts:
privateintGetCountInternal(){
int num = 0;
for (ConcurrentBag<T>.ThreadLocalList threadLocalList = this.m_headList; threadLocalList != null; threadLocalList = threadLocalList.m_nextList)
checked { num += threadLocalList.Count; }
return num;
}
ConcurrentDictionary
GetEnumeratormethod does not create new collection and usesyieldto iterate through collection, so,Anyis cheap:
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
foreach (ConcurrentDictionary<TKey, TValue>.Node bucket inthis.m_tables.m_buckets)
{
ConcurrentDictionary<TKey, TValue>.Node current;
for (current = Volatile.Read<ConcurrentDictionary<TKey, TValue>.Node>(ref bucket); current != null; current = current.m_next)
yieldreturnnew KeyValuePair<TKey, TValue>(current.m_key, current.m_value);
current = (ConcurrentDictionary<TKey, TValue>.Node) null;
}
}
Countlocks collection and sums up counts of internal tables:
publicint Count
{
[__DynamicallyInvokable] get
{
int locksAcquired = 0;
try
{
this.AcquireAllLocks(ref locksAcquired);
returnthis.GetCountInternal();
}
finally
{
this.ReleaseLocks(0, locksAcquired);
}
}
}
privateintGetCountInternal(){
int num = 0;
for (int index = 0; index < this.m_tables.m_countPerLock.Length; ++index)
num += this.m_tables.m_countPerLock
;
return num;
}
And it takes more time in collection with more elements:
Method
_size
Mean
Error
StdDev
Scaled
ScaledSD
Any
10
37.79 ns
0.7850 ns
1.0207 ns
1.00
0.00
Custom
10
239.63 ns
2.2313 ns
2.0872 ns
6.34
0.18
Any
100
34.46 ns
0.4621 ns
0.4322 ns
1.00
0.00
Custom
100
823.62 ns
16.0617 ns
17.8525 ns
23.90
0.58
Any
1000
38.22 ns
0.5311 ns
0.4968 ns
1.00
0.00
Custom
1000
6,264.17 ns
15.1371 ns
12.6402 ns
163.92
2.09
Any
10000
35.57 ns
0.5368 ns
0.5021 ns
1.00
0.00
Custom
10000
24,819.31 ns
283.1502 ns
264.8588 ns
697.82
11.97
Profiler shows that locking takes a lot of time:
ConcurrentStack
GetEnumeratoruses lazy enumeration and it’s cheap to use:
private IEnumerator<T> GetEnumerator(ConcurrentStack<T>.Node head){
for (ConcurrentStack<T>.Node current = head; current != null; current = current.m_next)
yieldreturn current.m_value;
}
ButCountjust counts all elements even without locking:
publicint Count
{
[__DynamicallyInvokable] get
{
int num = 0;
for (ConcurrentStack<T>.Node node = this.m_head; node != null; node = node.m_next)
++num;
return num;
}
}
ConcurrentQueue
GetEnumeratorimplementation is a bit complicated, but it uses lazyyieldinternal.Countuses simple calculations without enumerating all elements. You can use both methods.
As you see, in concurrent world everything is not so simple and internal implementation differs very much.
Universal method for everything?
No, it’s impossible. You can use customAnymethod that checksCountproperty for some simple collections, but better useCountorAnydepending on collection.