alexeyfv

Published on

- 4 min read

What is ReadOnlySpan<T> in C# and how fast is it?

C# Performance
img of What is ReadOnlySpan<T> in C# and how fast is it?

I already wrote an article about the fastest way of extracting substrings in C#. Now I want to investigate Span structures more. Recently, Microsoft released the .NET 8 platform which has several new extension methods for ReadOnlySpan<T>. So I want to compare the performance of MemoryExtensions methods with counterparts in string class.

Benchmark

All the benchmarks use a JSON-file with a string array of size 135 892 elements. Each array element represents the different permissions for different folders. Strings has the following template:

   <permission> for Folder: \\server-name\path\to\folder\

For example:

   DENY Permission for Folder: \\server-name\path\to\folder\

My employer won’t be happy if I share the file, so you should trust me that this file contains such data. :)

In this benchmark, we’ll consider the following 8 methods:

stringReadOnlySpan<T>
ContainsContains
StartsWithStartsWith
IndexOfIndexOf
ReplaceReplace
SplitSplit
ToLowerInvariantToLowerInvariant
TrimTrim
SubstringSlice

In the context of Span-based methods, we also take into account the “Span + Allocation” scenario, which involves string allocation using the ToString method where applicable. This scenario arises when Span-based methods are initially used, but the ultimate outcome includes the allocation of a string through the ToString method.

As usual, for benchmarking, I used the BenchmarkDotNet library. The whole code of the benchmark class can be found here.

Results

Contains

The benchmark assesses whether each string in the provided collection contains the backslash character.

BenchmarkMean execution time, μsExecution time ratioGen 0 collections per 1000 operationsAllocated memory, bytesAllocated ratio
String615.701
ReadOnlySpan<T>624.1-2%01

StartsWith

The benchmark evaluates whether each string in the given collection starts with the specified substring.

BenchmarkMean execution time, μsExecution time ratioGen 0 collections per 1000 operationsAllocated memory, bytesAllocated ratio
String56 681.1082
ReadOnlySpan<T>329.6-99.4%00-100%

IndexOf

The benchmark retrieves the index of the substring “Folder” within each string in the given collection.

BenchmarkMean execution time, μsExecution time ratioGen 0 collections per 1000 operationsAllocated memory, bytesAllocated ratio
String224 182.40245
ReadOnlySpan<T>1 131.6-99.5%01-99.6%

Split

This benchmark, designed as a synthetic test, utilizes the Split method to determine the maximum number of substrings between separators in each string within the provided collection.

BenchmarkMean execution time, μsExecution time ratioGen 0 collections per 1000 operationsAllocated memory, bytesAllocated ratio
String16 241.34468.7556 127 351
ReadOnlySpan<T>9 977.2-39%01060-100%

Replace

These benchmarks focus on replacing backslashes with forward slashes in each string within the provided collection, using different methods for string manipulation.

BenchmarkMean execution time, μsExecution time ratioGen 0 collections per 1000 operationsAllocated memory, bytesAllocated ratio
String3 156.82019.5325 353 195
ReadOnlySpan<T>2 663.0-16%03-100%
ReadOnlySpan<T> with allocation16 701.5+430%2015.6225 353 204-0%

ToLowerInvariant

These benchmarks focus on converting each string within the provided collection to lowercase, utilizing different methods for case transformation.

BenchmarkMean execution time, μsExecution time ratioGen 0 collections per 1000 operationsAllocated memory, bytesAllocated ratio
String3 771.32019.5325 353 195
ReadOnlySpan<T>3 884.3+3%03-100%
ReadOnlySpan<T> with allocation17 938.6+374%2015.6225 353 204-0%

Trim

These benchmarks focus on trimming trailing backslashes from the specified substring in each string within the provided collection, utilizing various approaches to achieve the desired result.

BenchmarkMean execution time, μsExecution time ratioGen 0 collections per 1000 operationsAllocated memory, bytesAllocated ratio
String687.4553
ReadOnlySpan<T>469.1-32%-99.8%
ReadOnlySpan<T> with allocation2 968.1+332%2019.5325 353 195+4 584 565%

Substring

These benchmarks focus on extracting a substring between specific markers in each string within the provided collection, using different methods to achieve the desired substring extraction.

BenchmarkMean execution time, μsExecution time ratioGen 0 collections per 1000 operationsAllocated memory, bytesAllocated ratio
String1 523.8779.39784225
ReadOnlySpan<T>347.9-77%-100%
ReadOnlySpan<T> with allocation1 694.5+11%779.39784225+0%

Conclusion

ReadOnlySpan<T> without allocation

Performance comparison of ReadOnlySpan execution

ReadOnlySpan execution ratio

As we can see from the figure above, almost all extension methods for ReadOnlySpan<T> are faster than analogues from string class. The only exception is the ReadOnlySpan<T>.ToLower method. I assume that It is because this method copies the characters from the source span into the destination.

ReadOnlySpan<T> with allocation

Performance comparison of ReadOnlySpan with allocation execution

ReadOnlySpan execution ratio

The string.Replace, string.ToLower, string.TrimEnd, string.Substring methods outperform the combination of ReadOnlySpan<T> methods and ReadOnlySpan<T>.ToString. This difference is likely due to efficient implementations of these methods. For example, invoking the string.TrimEnd leads to a call of the Buffer.Memmove method. It appears that the string allocation process using Buffer.Memmove is more efficient than the implementation of ReadOnlySpan<T>.ToString.

Memory consumption

Span-based methods exhibit superior memory efficiency, with zero memory allocations and no observed Gen 0 collections. String methods, particularly in operations like Split, Replace, and ToLower, tend to incur more significant memory allocations and, in some cases, Gen 0 collections. Therefore, for memory-conscious applications, utilizing Span-based methods may offer performance advantages in terms of reduced memory footprint and improved garbage collection behavior.

Notice almost the same or even worse results for the “Span + Allocation” column. Despite the utilization of Span-based methods in the intermediate steps, the inclusion of string allocation in the final outcome appears to negate some of the memory efficiency gained by using Span. This indicates that, in this specific context, the allocation of strings during or after Span-based operations may mitigate the potential memory benefits associated with using Span.

Generation 0 collections per 1000 operations

CategoriesStringSpanSpan + Allocation
Contains00-
StartsWith00-
IndexOf00-
Split4468.750-
Replace2019.531302015.625
ToLower2019.531302015.625
Trim1333.333302019.5313
Substring779.30779.3

Memory allocated, Mb

CategoriesStringSpanSpan + Allocation
Contains0.000.00-
StartsWith0.000.00-
IndexOf0.000.00-
Split53.530.00-
Replace24.180.0024.18
ToLower24.180.0024.18
Trim0.000.0024.17
Substring9.330.009.33

Further reading

  1. MemoryExtensions Class.
  2. Spanification.
  3. Pro .NET Memory Management.