Any() vs Count: part 2

Published on Sunday 11 May 2025

In Part 1, we compared Any() and Count methods for different collections and proposed optimization approaches.

Array

Let's start with the simplest collection type - arrays. We'll check if there's any difference between array.Any() and array.Length != 0.

The following configuration is used throughout:

BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.5796/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.203
  [Host]             : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  .NET 5.0           : .NET 5.0.17 (5.0.1722.21314), X64 RyuJIT AVX2
  .NET 6.0           : .NET 6.0.36 (6.0.3624.51421), X64 RyuJIT AVX2
  .NET 8.0           : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  .NET 9.0           : .NET 9.0.4 (9.0.425.16305), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  .NET Core 3.1      : .NET Core 3.1.32 (CoreCLR 4.700.22.55902, CoreFX 4.700.22.56512), X64 RyuJIT AVX2
  .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256

Benchmark code. Click to expand.
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace AnyVsCount
{
    [MemoryDiagnoser]
    [SimpleJob(RuntimeMoniker.Net90)]
    [SimpleJob(RuntimeMoniker.Net80)]
    [SimpleJob(RuntimeMoniker.Net60)]
    [SimpleJob(RuntimeMoniker.Net50)]
    [SimpleJob(RuntimeMoniker.NetCoreApp31)]
    [SimpleJob(RuntimeMoniker.Net48)]
    [HideColumns("Job", "Error", "StdDev", "Gen0")]
    public class ArrayBenchmark
    {
        [Params(10, 10000)]
        public int N;

        private int[] array;

        [GlobalSetup]
        public void SetUp()
        {
            array = new int[N];
        }

        [Benchmark]
        public bool ArrayAny()
        {
            return array.Any();
        }

        [Benchmark]
        public bool ArrayCount()
        {
            return array.Length != 0;
        }
    }
}

Results

Method Runtime N Mean Median Allocated
ArrayAny .NET Framework 4.8 10 8.7962 ns 8.8213 ns 32 B
ArrayCount .NET Framework 4.8 10 0.0073 ns 0.0028 ns -
ArrayAny .NET Core 3.1 10 8.0450 ns 8.0462 ns 32 B
ArrayCount .NET Core 3.1 10 0.0433 ns 0.0399 ns -
ArrayAny .NET 5.0 10 6.2080 ns 6.1852 ns -
ArrayCount .NET 5.0 10 0.0030 ns 0.0000 ns -
ArrayAny .NET 6.0 10 6.0673 ns 6.0671 ns -
ArrayCount .NET 6.0 10 0.0135 ns 0.0135 ns -
ArrayAny .NET 8.0 10 5.0980 ns 5.2591 ns -
ArrayCount .NET 8.0 10 0.0000 ns 0.0000 ns -
ArrayAny .NET 9.0 10 2.1643 ns 2.1216 ns -
ArrayCount .NET 9.0 10 0.0030 ns 0.0034 ns -
ArrayAny .NET Framework 4.8 10000 9.0093 ns 9.0043 ns 32 B
ArrayCount .NET Framework 4.8 10000 0.0123 ns 0.0137 ns -
ArrayAny .NET Core 3.1 10000 7.9624 ns 7.7794 ns 32 B
ArrayCount .NET Core 3.1 10000 0.0311 ns 0.0295 ns -
ArrayAny .NET 5.0 10000 4.8357 ns 4.8352 ns -
ArrayCount .NET 5.0 10000 0.0008 ns 0.0007 ns -
ArrayAny .NET 6.0 10000 6.0913 ns 6.0747 ns -
ArrayCount .NET 6.0 10000 0.0147 ns 0.0153 ns -
ArrayAny .NET 8.0 10000 4.7691 ns 4.7521 ns -
ArrayCount .NET 8.0 10000 0.0110 ns 0.0078 ns -
ArrayAny .NET 9.0 10000 2.2933 ns 2.2906 ns -
ArrayCount .NET 9.0 10000 0.0121 ns 0.0109 ns -

Values close to zero (e.g., 0.0000 ns) should be considered negligible - they are within measurement error.

Legend for all tables:

  N         : Value of the 'N' parameter
  Mean      : Arithmetic mean of all measurements
  Median    : Value separating the higher half of all measurements (50th percentile)
  Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  1 ns      : 1 Nanosecond (0.000000001 sec)

For arrays, results are nearly independent of size, so we'll focus on N = 10000. Directly accessing the Length property is at least 2 orders of magnitude faster than Any(). However, in generic code working with IEnumerable<T>, we often use:

public bool IsEmpty<T>(IEnumerable<T> collection)
{
    return !collection.Any();
}

Let's analyze why Any() performance varies across .NET versions. From .NET 4.8 to 9.0, Any() execution time decreased 4x:

Method Any performance

In .NET 4.8 and Core 3.1, Any() creates an iterator and checks MoveNext(), allocating 32 bytes:

public static bool Any<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
        return e.MoveNext();
    }
}

Starting with .NET 5, optimizations were introduced:

public static bool Any<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
    }

    if (source is ICollection<TSource> collectionoft)
    {
        return collectionoft.Count != 0;
    }
    else if (source is IIListProvider<TSource> listProv)
    {
        int count = listProv.GetCount(onlyIfCheap: true);
        if (count >= 0)
        {
            return count != 0;
        }
    }
    else if (source is ICollection collection)
    {
        return collection.Count != 0;
    }

    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
        return e.MoveNext();
    }
}

This solution is similar to the one we used in Part 1. Firstly it checks interface ICollection<T>, that has property Count. And then we see usage of a new interface IIListProvider<TSource> (internal interface of .NET to optimize LINQ methods):

internal interface IIListProvider<TElement> : IEnumerable<TElement>
{
    TElement[] ToArray();

    List<TElement> ToList();

    int GetCount(bool onlyIfCheap);
}

The interface IIListProvider<TSource> is part of a major rework of LINQ that has resulted in significant optimizations noticeable even in the simple Any() method. Interestingly, in .NET 6 the performance has degraded somewhat, although the implementation of the Any() method has not changed.

In the eighth version of .NET, the code that calculates the size of an enumeration (without performing the enumeration) was moved to a separate method TryGetNonEnumeratedCount(). This method runs in constant time, but may not always return a value.

In .NET 9, the concept of using IIListProvider<TSource> was further developed and the LINQ methods were refactored to use the new Iterator<TSource> class, further improving performance. As we can see, the obvious solution with checking the enumeration type and the Count property has been implemented in new versions of .NET.

Will this be effective for all collections?

ConcurrentDictionary

Let's look at the most common thread-safe collection - ConcurrentDictionary<TKey, TValue>. It seems that there may be problems with the new implementation.

Benchmark code. Click to expand.
using System.Collections.Concurrent;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace AnyVsCount
{
    [MemoryDiagnoser]
    [SimpleJob(RuntimeMoniker.Net90)]
    [SimpleJob(RuntimeMoniker.Net80)]
    [SimpleJob(RuntimeMoniker.Net60)]
    [SimpleJob(RuntimeMoniker.Net50)]
    [SimpleJob(RuntimeMoniker.NetCoreApp31)]
    [SimpleJob(RuntimeMoniker.Net48)]
    [HideColumns("Job", "Error", "StdDev", "Gen0")]
    public class ConcurrentDictionaryBenchmark
    {
        [Params(10, 10000)]
        public int N;

        private ConcurrentDictionary<int, string> dictionary;

        [GlobalSetup]
        public void SetUp()
        {
            dictionary = new ConcurrentDictionary<int, string>();

            for (int i = 0; i < N; i++)
            {
                dictionary[i] = i.ToString();
            }
        }

        [Benchmark]
        public bool DictionaryAny()
        {
            return dictionary.Any();
        }

        [Benchmark]
        public bool DictionaryCount()
        {
            return dictionary.Count != 0;
        }
    }
}

Results

Method Runtime N Mean Median Allocated
DictionaryAny .NET Framework 4.8 10 18.48 ns 18.47 ns 64 B
DictionaryCount .NET Framework 4.8 10 110.77 ns 110.75 ns -
DictionaryAny .NET Core 3.1 10 18.21 ns 18.17 ns 64 B
DictionaryCount .NET Core 3.1 10 108.27 ns 110.12 ns -
DictionaryAny .NET 5.0 10 92.98 ns 92.81 ns -
DictionaryCount .NET 5.0 10 97.02 ns 96.42 ns -
DictionaryAny .NET 6.0 10 87.96 ns 87.79 ns -
DictionaryCount .NET 6.0 10 88.35 ns 88.40 ns -
DictionaryAny .NET 8.0 10 76.10 ns 76.08 ns -
DictionaryCount .NET 8.0 10 72.26 ns 72.27 ns -
DictionaryAny .NET 9.0 10 83.02 ns 83.52 ns -
DictionaryCount .NET 9.0 10 80.43 ns 79.34 ns -
DictionaryAny .NET Framework 4.8 10000 18.87 ns 18.71 ns 64 B
DictionaryCount .NET Framework 4.8 10000 6,870.38 ns 6,859.65 ns -
DictionaryAny .NET Core 3.1 10000 18.04 ns 18.06 ns 64 B
DictionaryCount .NET Core 3.1 10000 7,000.55 ns 7,000.95 ns -
DictionaryAny .NET 5.0 10000 5,877.30 ns 5,877.50 ns -
DictionaryCount .NET 5.0 10000 5,962.47 ns 5,958.17 ns -
DictionaryAny .NET 6.0 10000 5,993.15 ns 5,994.17 ns -
DictionaryCount .NET 6.0 10000 6,030.51 ns 6,029.08 ns -
DictionaryAny .NET 8.0 10000 5,082.70 ns 5,082.69 ns -
DictionaryCount .NET 8.0 10000 5,370.34 ns 5,368.76 ns -
DictionaryAny .NET 9.0 10000 5,917.83 ns 5,917.32 ns -
DictionaryCount .NET 9.0 10000 5,847.50 ns 5,848.04 ns -

Some features are already visible:

  • Execution time depends on the size of the collection.
  • In older versions of the framework, the Any() method was much faster.
  • In newer versions of the framework, the Any() method is on par with Count in performance:

ConcurrentDictionary Any performance

Let's look at the implementation of the Count property:

public int Count
{
    get
    {
        int locksAcquired = 0;
        try
        {
            AcquireAllLocks(ref locksAcquired);

            return GetCountNoLocks();
        }
        finally
        {
            ReleaseLocks(locksAcquired);
        }
    }
}

private void AcquireAllLocks(ref int locksAcquired)
{
    //...

    // First, acquire lock 0, then acquire the rest. _tables won't change after acquiring lock 0.
    AcquireFirstLock(ref locksAcquired);
    AcquirePostFirstLock(_tables, ref locksAcquired);
    Debug.Assert(locksAcquired == _tables._locks.Length);
}

Counting all elements requires acquiring locks on a special internal array _tables._locks. Each element of this array locks a part of the dictionary. Thus, starting with .NET 5, the Any() method of ConcurrentDictionary uses the Count property through ICollection<T>, which results in the same locks as calling Count directly.

Any() и IsEmpty

Let's reconstruct the implementation of Any() with an iterator and check the performance of this method:

[Benchmark]
public bool DictionaryEnumerator()
{
    using (var enumerator = dictionary.GetEnumerator())
    {
        return enumerator.MoveNext();
    }
}
Method Runtime N Mean Median Allocated
DictionaryEnumerator .NET Framework 4.8 10 15.51 ns 15.52 ns 64 B
DictionaryEnumerator .NET Core 3.1 10 14.60 ns 14.60 ns 64 B
DictionaryEnumerator .NET 5.0 10 15.16 ns 15.14 ns 64 B
DictionaryEnumerator .NET 6.0 10 18.60 ns 18.67 ns 64 B
DictionaryEnumerator .NET 8.0 10 12.97 ns 12.96 ns 64 B
DictionaryEnumerator .NET 9.0 10 12.80 ns 12.81 ns 64 B
DictionaryEnumerator .NET Framework 4.8 10000 15.41 ns 15.38 ns 64 B
DictionaryEnumerator .NET Core 3.1 10000 15.03 ns 14.96 ns 64 B
DictionaryEnumerator .NET 5.0 10000 15.90 ns 15.92 ns 64 B
DictionaryEnumerator .NET 6.0 10000 18.50 ns 18.50 ns 64 B
DictionaryEnumerator .NET 8.0 10000 13.38 ns 12.92 ns 64 B
DictionaryEnumerator .NET 9.0 10000 12.97 ns 12.53 ns 64 B

We see that the results do not depend on the number of elements and have remained virtually unchanged in newer versions of .NET. But we spend a little memory creating the iterator each time.

But you can use a specialized collection property - IsEmpty:

[Benchmark]
public bool DictionaryIsEmpty()
{
    return !dictionary.IsEmpty;
}
Method Runtime N Mean Median Allocated
DictionaryIsEmpty .NET Framework 4.8 10 99.023 ns 98.709 ns -
DictionaryIsEmpty .NET Core 3.1 10 2.313 ns 2.313 ns -
DictionaryIsEmpty .NET 5.0 10 2.545 ns 2.545 ns -
DictionaryIsEmpty .NET 6.0 10 2.247 ns 2.283 ns -
DictionaryIsEmpty .NET 8.0 10 2.567 ns 2.568 ns -
DictionaryIsEmpty .NET 9.0 10 10.917 ns 10.902 ns -
DictionaryIsEmpty .NET Framework 4.8 10000 6,027.991 ns 6,026.648 ns -
DictionaryIsEmpty .NET Core 3.1 10000 2.320 ns 2.315 ns -
DictionaryIsEmpty .NET 5.0 10000 2.513 ns 2.512 ns -
DictionaryIsEmpty .NET 6.0 10000 2.756 ns 2.759 ns -
DictionaryIsEmpty .NET 8.0 10000 2.673 ns 2.673 ns -
DictionaryIsEmpty .NET 9.0 10000 3.801 ns 3.804 ns -

We see that starting with .NET Core 3.1, the implementation of this property does not depend on the size of the collection, and in the case of non-empty collections, calling this property is not blocking. For .NET 4.8, we got results similar to calling the Count property - a lock on the entire collection is also used. For .NET 9, the results were slightly worse, but I don't have a quick answer as to why this happened.

Conclusion

The implementation of the Any() method is now optimized enough to be used in generic code, but there is still some room for its use in thread-safe collections.

Recommendations

  • Use Count properties for simple collections if you care about performance.
  • Use Any() method for generic code and IEnumerable<T>.
  • Use IsEmpty property if collections support it, starting with .NET 5+