HLQ012: Use CollectionsMarshal.AsSpan() to iterate List.

July 17, 2023 ยท View on GitHub

Cause

'foreach' loop is being used on a List<T>.

Severity

Warning

Rule description

When iterating over a List<T> in a foreach loop, the default behavior is to create an enumerator and allocate an iterator object. This incurs additional memory allocations and can impact performance, especially in scenarios with large collections or frequent iterations. By using the CollectionsMarshal.AsSpan() method, you can eliminate these overheads and directly access the underlying elements of the List<T> without creating an enumerator. Iterating a Span<T> directly avoids the overhead of the enumerator, resulting in faster loop iterations.

Benchmarking

Source: https://github.com/NetFabric/NetFabric.Hyperlinq.Analyzer/blob/master/NetFabric.Hyperlinq.Analyzer.Benchmarks/HLQ012_UseCollectionsMarshalAsSpanAnalyzer.cs


BenchmarkDotNet v0.13.6, Windows 10 (10.0.19045.3155/22H2/2022Update)
Intel Core i7-7567U CPU 3.50GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.100-preview.5.23303.2
  [Host] : .NET 6.0.20 (6.0.2023.32017), X64 RyuJIT AVX2
  .NET 6 : .NET 6.0.20 (6.0.2023.32017), X64 RyuJIT AVX2
  .NET 7 : .NET 7.0.8 (7.0.823.31807), X64 RyuJIT AVX2
  .NET 8 : .NET 8.0.0 (8.0.23.28008), X64 RyuJIT AVX2


MethodJobRuntimeCountMeanErrorStdDevMedianRatioRatioSDCode SizeAllocatedAlloc Ratio
ForEach.NET 6.NET 6.001.0956 ns0.0530 ns0.1300 ns1.0414 nsbaseline****99 B-NA
CollectionsMarshalAsSpan.NET 6.NET 6.000.5871 ns0.0329 ns0.0379 ns0.5835 ns1.83x faster0.16xNA-NA
ForEach.NET 7.NET 7.001.3218 ns0.0395 ns0.0440 ns1.3141 nsbaselineNA-NA
CollectionsMarshalAsSpan.NET 7.NET 7.000.8831 ns0.0532 ns0.1510 ns0.8114 ns1.50x faster0.20xNA-NA
ForEach.NET 8.NET 8.000.0441 ns0.0310 ns0.0793 ns0.0000 ns??NA-?
CollectionsMarshalAsSpan.NET 8.NET 8.000.4798 ns0.0416 ns0.1146 ns0.4198 ns??NA-?
ForEach.NET 6.NET 6.01014.2010 ns0.1204 ns0.1005 ns14.1930 nsbaseline****99 B-NA
CollectionsMarshalAsSpan.NET 6.NET 6.0104.0248 ns0.1112 ns0.1189 ns3.9801 ns3.51x faster0.11xNA-NA
ForEach.NET 7.NET 7.01010.3653 ns0.1658 ns0.1470 ns10.3078 nsbaselineNA-NA
CollectionsMarshalAsSpan.NET 7.NET 7.0103.6369 ns0.1270 ns0.3583 ns3.4921 ns2.92x faster0.21xNA-NA
ForEach.NET 8.NET 8.0107.1217 ns0.1269 ns0.1125 ns7.0938 nsbaselineNA-NA
CollectionsMarshalAsSpan.NET 8.NET 8.0103.8439 ns0.1792 ns0.5084 ns3.6169 ns1.88x faster0.24xNA-NA
ForEach.NET 6.NET 6.0100142.4614 ns2.6891 ns3.4966 ns142.0638 nsbaseline****99 B-NA
CollectionsMarshalAsSpan.NET 6.NET 6.010046.1275 ns0.9312 ns0.9963 ns46.0060 ns3.09x faster0.12xNA-NA
ForEach.NET 7.NET 7.0100113.1277 ns2.2898 ns2.9773 ns111.7294 nsbaselineNA-NA
CollectionsMarshalAsSpan.NET 7.NET 7.010037.8990 ns0.5435 ns0.5816 ns37.6491 ns3.00x faster0.10xNA-NA
ForEach.NET 8.NET 8.010067.2375 ns1.3082 ns1.1596 ns66.9356 nsbaselineNA-NA
CollectionsMarshalAsSpan.NET 8.NET 8.010039.8413 ns0.4387 ns0.4308 ns39.7005 ns1.69x faster0.04xNA-NA
ForEach.NET 6.NET 6.010001,316.1551 ns8.4594 ns6.6045 ns1,315.7101 nsbaseline****99 B-NA
CollectionsMarshalAsSpan.NET 6.NET 6.01000392.1319 ns6.4124 ns6.8612 ns388.7779 ns3.34x faster0.07xNA-NA
ForEach.NET 7.NET 7.010001,045.3751 ns5.2175 ns4.3569 ns1,045.0901 nsbaselineNA-NA
CollectionsMarshalAsSpan.NET 7.NET 7.01000325.3411 ns2.4156 ns2.0171 ns325.4291 ns3.21x faster0.03xNA-NA
ForEach.NET 8.NET 8.01000594.4719 ns7.7370 ns6.0406 ns593.9274 nsbaselineNA-NA
CollectionsMarshalAsSpan.NET 8.NET 8.01000352.6162 ns5.7492 ns10.3669 ns348.4033 ns1.67x faster0.06xNA-NA

How to fix violations

Use the CollectionsMarshal.AsSpan() method to convert List<T> to a Span<T>.

When to suppress warnings

The CollectionsMarshal.AsSpan() method is only available in .NET Standard 2.1 or later, and .NET Core 3.0 or later. Make sure your project targets a compatible framework version.

It's important to validate that using CollectionsMarshal.AsSpan() is suitable for your specific scenario. While it provides performance benefits, it may not be appropriate for all use cases.

Example of a violation

var source = new List<int>();
foreach (var item in source)
    Console.WriteLine(item);

Example of how to fix

var source = new List<int>();
foreach (var item in CollectionsMarshal.AsSpan(source))
    Console.WriteLine(item);