For, Foreach, or LINQ, Which One Is More Performant in .NET 6?
Ciao everyone,
We write lines of code every day. We use for, foreach and LINQ many times in our very functional and fun codes. Yes, LINQ. :) This article will be about .NET. How do these magic commands that we use perform when the data gets large? This is my first article on Medium, and I will tell you whether for, foreach or LINQ is more performant.
You can find the project I prepared for benchmark here.
Scenario
Suppose we have an array with n elements. This array will be of type object, and we will examine its performance in three cases using for, foreach and LINQ. For this performance analysis, we will use a console application with .NET 6. And of course, the BenchmarkDotNet library. This is a benchmark library with magical power developed for .NET.
Setup
I preferred the .NET 6 Console application for this analysis. The hardware used for analysis is Apple M2 silicon processor and 16 GB RAM. It seems to me that different RAM capacities can produce different results. I chose Rider as the IDE. I used BenchmarkDotNet 0.13.2 library for benchmarking. I used a user entity as an object to use in an array.
public class User
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Point { get; set; }
}
It was necessary to add data to this user entity, which will be used for three different cases. Using Guid.NewGuid() for Id should have been enough. Random.Next(0,1000) can be used to generate a random integer value between 0 and 1000 for the Point.
I wrote a helper to generate a string value between 5 and 10 letters for FirstName and LastName.
public static class StringGenerator
{
public static string RandomString(int size)
{
Random random = new Random();
var builder = new StringBuilder(size);
char offset = 'A';
const int lettersOffset = 26;
for (var i = 0; i < size; i++)
{
var @char = (char)random.Next(offset, offset + lettersOffset);
builder.Append(@char);
}
return builder.ToString();
}
}
This helper will generate an uppercase string of length value according to ASCII notation.
A base setup class will be needed for the three cases to be used in the benchmarking. This class will contain comparison results and graphing, array setting and some configurations. Params specifies the array size from 1000 to 3 million. Yes, we will use arrays of various sizes up to 3 million.
[RPlotExporter]
[SimpleJob(id: "TestQuize")]
public class BaseTestQuize
{
protected User[] users;
private readonly Random _random = new Random();
[Params(1_000, 10_000, 100_000, 200_000, 300_000, 400_000, 500_000, 600_000, 700_000, 800_000, 900_000, 1_000_000, 2_000_000, 3_000_000)]
public int size;
[IterationSetup]
public void Setup()
{
System.Diagnostics.Debugger.Launch();
users = new User[size];
for (int i = 0; i < size; i++)
{
users[i] = new User
{
Id = Guid.NewGuid(),
FirstName = StringGenerator.RandomString(_random.Next(5, 10)),
LastName = StringGenerator.RandomString(_random.Next(5, 10)),
Point = _random.Next(0,1000)
};
}
}
}
[RPlotExporter] provides graphical results. It is necessary to install R to use it. You can find it here.
Cases
For each case, there are three methods using for, foreach, and LINQ. These three methods filter, iterate, and transform the users in the array.
public class FiterTestQuize : BaseTestQuize
{
[Benchmark(Baseline = true, Description = "for")]
public User[] ForLoop()
{
var results = new List<User>();
for (var i = 0; i < users.Length; i++)
{
var c = users[i];
if (c.Point!<200)
results.Add(c);
}
return results.ToArray();
}
[Benchmark(Description = "foreach")]
public User[] ForEachLoop()
{
var results = new List<User>();
foreach (var c in users)
{
if (c.Point!<200)
results.Add(c);
}
return results.ToArray();
}
[Benchmark(Description = "LINQ")]
public User[] FilteringByLinq() => users.Where(c => c.Point!<200).ToArray();
}
For the filtering case, each method returns the users with less than 200 points in the array as an array.
public class IterationTestQuize : BaseTestQuize
{
[Benchmark(Description = "for")]
public int ForLoop()
{
var count = 0;
for (var i = 0; i < users.Length; i++)
{
if (users[i].Point<200)
{
count++;
}
}
return count;
}
[Benchmark(Description = "foreach")]
public int ForEachLoop()
{
var count = 0;
foreach (var c in users)
{
if (c.Point<200)
{
count++;
}
}
return count;
}
[Benchmark(Description = "LINQ")]
public int LinqCount() => users.Count(c => c.Point<200);
}
For the iteration case, each method returns the number of users in the array with less than 200 points.
public class TransformationTestQuize : BaseTestQuize
{
private static User ToNamesInLowerCase(User c) => new User
{
Id = c.Id,
FirstName = c.FirstName.ToLower(),
LastName = c.LastName.ToLower(),
Point = c.Point
};
[Benchmark(Baseline = true, Description = "for")]
public User[] UsersToNamesInLowerCase_For()
{
var usersToNamesInLowerCase = new List<User>(users.Length);
for (var i = 0; i < users.Length; i++)
{
usersToNamesInLowerCase.Add(ToNamesInLowerCase(users[i]));
}
return usersToNamesInLowerCase.ToArray();
}
[Benchmark(Description = "foreach")]
public User[] UsersToNamesInLowerCase_ForEach()
{
var usersToNamesInLowerCase = new List<User>(users.Length);
foreach (var user in users)
{
usersToNamesInLowerCase.Add(ToNamesInLowerCase(user));
}
return usersToNamesInLowerCase.ToArray();
}
[Benchmark(Description = "LINQ select")]
public User[] UsersToNamesInLowerCase_LINQ() => users.Select(ToNamesInLowerCase).ToArray();
}
For the transformation case, I decided to transform the data in the array. For this, I chose FirstName and LastName. This string in each method array converts the data to lowercase.
Build
For me, this is the most exciting phase. We will get the benchmark results according to the scenarios we have planned. As a reminder, the build results may vary depending on the hardware you are using, the library versions, the other processing sizes the computer is doing at the moment, as well as the IDE you are using.
First, the project may need to be cleaned in the terminal.
dotnet clean
To start the build process, it is necessary to authorize the terminal. This is really important. If the operating system is Windows, it must be run as an administrator. For macOS or other Linux-based operating systems, using sudo is sufficient. macOS was used at this phase.
sudo dotnet build -c Release
After the build process, a .dll file will be created on the path where the project is located. We will continue the process by running this .dll file.
dotnet /Users/eg/src/IterationBenchmark/IterationBenchmark/bin/Release/net6.0/IterationBenchmark.dll
With this command, the program will start benchmarking the scenarios. The duration of this process may increase depending on the array sizes you use for Params. Because of the sizes I gave, I waited for the results for a long time. During this process, my computer’s battery dropped rapidly.
Results
After a long wait, a file named Artifacts will be created in the file where the project is located. The Artifacts file contains log records and graphical representations of results.
// * Summary *
BenchmarkDotNet=v0.13.2, OS=macOS 13.0 (22A380) [Darwin 22.1.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.402
[Host] : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
TestQuize : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
Job=TestQuize InvocationCount=1 UnrollFactor=1
| Method | size | Mean | Error | StdDev | Median | Ratio | RatioSD |
|------------- |-------- |--------------:|------------:|------------:|--------------:|------:|--------:|
| for | 1000 | 6.929 μs | 0.1386 μs | 0.2638 μs | 6.917 μs | 1.00 | 0.00 |
| foreach | 1000 | 6.310 μs | 0.1270 μs | 0.2507 μs | 6.291 μs | 0.91 | 0.05 |
| 'LINQ where' | 1000 | 10.700 μs | 0.2060 μs | 0.3767 μs | 10.709 μs | 1.55 | 0.08 |
| | | | | | | | |
| for | 10000 | 57.810 μs | 1.1411 μs | 2.3566 μs | 57.666 μs | 1.00 | 0.00 |
| foreach | 10000 | 53.460 μs | 1.0677 μs | 2.0570 μs | 52.687 μs | 0.92 | 0.04 |
| 'LINQ where' | 10000 | 66.555 μs | 1.3303 μs | 1.9499 μs | 66.125 μs | 1.14 | 0.05 |
| | | | | | | | |
| for | 100000 | 553.609 μs | 10.4876 μs | 11.2216 μs | 548.917 μs | 1.00 | 0.00 |
| foreach | 100000 | 518.109 μs | 10.0984 μs | 12.0214 μs | 511.750 μs | 0.94 | 0.03 |
| 'LINQ where' | 100000 | 633.998 μs | 11.8616 μs | 11.0954 μs | 629.895 μs | 1.14 | 0.03 |
| | | | | | | | |
| for | 200000 | 1,129.314 μs | 22.1009 μs | 51.6601 μs | 1,109.500 μs | 1.00 | 0.00 |
| foreach | 200000 | 1,024.219 μs | 6.3320 μs | 4.9436 μs | 1,024.062 μs | 0.90 | 0.05 |
| 'LINQ where' | 200000 | 1,253.314 μs | 6.3433 μs | 5.2969 μs | 1,255.250 μs | 1.11 | 0.06 |
| | | | | | | | |
| for | 300000 | 1,845.792 μs | 43.3792 μs | 120.9238 μs | 1,824.583 μs | 1.00 | 0.00 |
| foreach | 300000 | 1,661.457 μs | 32.9846 μs | 83.9563 μs | 1,645.228 μs | 0.90 | 0.07 |
| 'LINQ where' | 300000 | 2,115.207 μs | 46.5926 μs | 135.9129 μs | 2,107.688 μs | 1.15 | 0.11 |
| | | | | | | | |
| for | 400000 | 2,621.005 μs | 51.4254 μs | 100.3013 μs | 2,634.084 μs | 1.00 | 0.00 |
| foreach | 400000 | 2,411.840 μs | 48.2223 μs | 95.1861 μs | 2,409.521 μs | 0.92 | 0.05 |
| 'LINQ where' | 400000 | 2,990.647 μs | 59.7136 μs | 115.0479 μs | 2,992.916 μs | 1.14 | 0.06 |
| | | | | | | | |
| for | 500000 | 3,414.757 μs | 68.2109 μs | 152.5635 μs | 3,362.374 μs | 1.00 | 0.00 |
| foreach | 500000 | 3,111.431 μs | 61.8931 μs | 122.1710 μs | 3,091.500 μs | 0.91 | 0.05 |
| 'LINQ where' | 500000 | 3,846.775 μs | 76.2924 μs | 135.6096 μs | 3,846.438 μs | 1.12 | 0.06 |
| | | | | | | | |
| for | 600000 | 4,082.236 μs | 80.8001 μs | 172.1916 μs | 4,034.854 μs | 1.00 | 0.00 |
| foreach | 600000 | 3,625.929 μs | 70.6486 μs | 94.3139 μs | 3,640.312 μs | 0.89 | 0.05 |
| 'LINQ where' | 600000 | 4,521.674 μs | 90.4271 μs | 88.8115 μs | 4,536.999 μs | 1.09 | 0.05 |
| | | | | | | | |
| for | 700000 | 4,706.450 μs | 87.9969 μs | 86.4247 μs | 4,705.708 μs | 1.00 | 0.00 |
| foreach | 700000 | 4,354.012 μs | 69.5921 μs | 61.6915 μs | 4,339.562 μs | 0.93 | 0.02 |
| 'LINQ where' | 700000 | 5,430.392 μs | 107.1792 μs | 153.7132 μs | 5,384.770 μs | 1.15 | 0.05 |
| | | | | | | | |
| for | 800000 | 5,523.564 μs | 109.4168 μs | 121.6165 μs | 5,543.625 μs | 1.00 | 0.00 |
| foreach | 800000 | 5,038.729 μs | 97.9864 μs | 112.8413 μs | 5,050.208 μs | 0.91 | 0.03 |
| 'LINQ where' | 800000 | 6,198.523 μs | 121.0680 μs | 129.5414 μs | 6,175.583 μs | 1.12 | 0.04 |
| | | | | | | | |
| for | 900000 | 6,215.686 μs | 106.9267 μs | 139.0349 μs | 6,238.000 μs | 1.00 | 0.00 |
| foreach | 900000 | 5,675.125 μs | 112.3905 μs | 184.6607 μs | 5,632.979 μs | 0.91 | 0.04 |
| 'LINQ where' | 900000 | 7,036.052 μs | 140.0268 μs | 186.9318 μs | 6,998.104 μs | 1.13 | 0.03 |
| | | | | | | | |
| for | 1000000 | 7,111.775 μs | 135.8003 μs | 181.2896 μs | 7,090.770 μs | 1.00 | 0.00 |
| foreach | 1000000 | 6,376.317 μs | 126.8754 μs | 146.1099 μs | 6,385.875 μs | 0.89 | 0.03 |
| 'LINQ where' | 1000000 | 7,866.300 μs | 152.4369 μs | 127.2917 μs | 7,840.729 μs | 1.11 | 0.03 |
| | | | | | | | |
| for | 2000000 | 14,027.476 μs | 246.9197 μs | 218.8879 μs | 13,987.396 μs | 1.00 | 0.00 |
| foreach | 2000000 | 13,022.047 μs | 238.5829 μs | 349.7117 μs | 13,023.208 μs | 0.92 | 0.03 |
| 'LINQ where' | 2000000 | 15,857.325 μs | 310.7761 μs | 305.2236 μs | 15,842.125 μs | 1.13 | 0.03 |
| | | | | | | | |
| for | 3000000 | 21,985.874 μs | 438.2834 μs | 767.6181 μs | 22,031.875 μs | 1.00 | 0.00 |
| foreach | 3000000 | 19,625.011 μs | 388.0937 μs | 518.0940 μs | 19,729.062 μs | 0.89 | 0.04 |
| 'LINQ where' | 3000000 | 23,947.016 μs | 475.9770 μs | 529.0472 μs | 23,845.729 μs | 1.09 | 0.05 |
Based on the filtering case results, LINQ performed as slowly as I expected. However, foreach outperformed for, surpassing expectations.
// * Summary *
BenchmarkDotNet=v0.13.2, OS=macOS 13.0 (22A380) [Darwin 22.1.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.402
[Host] : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
TestQuize : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
Job=TestQuize InvocationCount=1 UnrollFactor=1
| Method | size | Mean | Error | StdDev | Median |
|-------- |-------- |--------------:|------------:|------------:|--------------:|
| for | 1000 | 4.673 μs | 0.0952 μs | 0.1966 μs | 4.729 μs |
| foreach | 1000 | 4.153 μs | 0.0844 μs | 0.2007 μs | 4.166 μs |
| LINQ | 1000 | 14.282 μs | 0.2799 μs | 0.4676 μs | 14.291 μs |
| for | 10000 | 41.399 μs | 0.7559 μs | 0.5902 μs | 41.333 μs |
| foreach | 10000 | 38.342 μs | 0.5269 μs | 0.4400 μs | 38.332 μs |
| LINQ | 10000 | 108.015 μs | 2.1376 μs | 1.8949 μs | 107.979 μs |
| for | 100000 | 429.457 μs | 9.0368 μs | 25.9283 μs | 417.833 μs |
| foreach | 100000 | 400.570 μs | 7.4716 μs | 7.3381 μs | 400.354 μs |
| LINQ | 100000 | 1,068.414 μs | 7.6000 μs | 6.7372 μs | 1,067.875 μs |
| for | 200000 | 883.521 μs | 13.4280 μs | 11.9036 μs | 884.479 μs |
| foreach | 200000 | 864.611 μs | 14.3849 μs | 24.4266 μs | 863.542 μs |
| LINQ | 200000 | 2,224.179 μs | 30.9730 μs | 27.4568 μs | 2,224.479 μs |
| for | 300000 | 1,334.987 μs | 22.1656 μs | 21.7695 μs | 1,336.376 μs |
| foreach | 300000 | 1,313.348 μs | 25.0450 μs | 41.1497 μs | 1,302.625 μs |
| LINQ | 300000 | 3,357.893 μs | 54.9781 μs | 58.8259 μs | 3,335.500 μs |
| for | 400000 | 1,807.443 μs | 36.0743 μs | 53.9943 μs | 1,802.250 μs |
| foreach | 400000 | 1,757.504 μs | 34.6724 μs | 43.8495 μs | 1,752.125 μs |
| LINQ | 400000 | 4,306.752 μs | 85.2586 μs | 185.3453 μs | 4,256.167 μs |
| for | 500000 | 2,256.113 μs | 43.3074 μs | 74.7031 μs | 2,228.062 μs |
| foreach | 500000 | 2,150.252 μs | 42.1867 μs | 41.4329 μs | 2,139.166 μs |
| LINQ | 500000 | 5,629.488 μs | 109.0287 μs | 129.7909 μs | 5,649.438 μs |
| for | 600000 | 2,703.223 μs | 53.7349 μs | 78.7639 μs | 2,687.021 μs |
| foreach | 600000 | 2,702.998 μs | 57.7098 μs | 165.5802 μs | 2,629.688 μs |
| LINQ | 600000 | 6,725.791 μs | 129.6949 μs | 127.3777 μs | 6,731.583 μs |
| for | 700000 | 3,211.445 μs | 63.2919 μs | 133.5041 μs | 3,157.708 μs |
| foreach | 700000 | 3,027.453 μs | 44.6907 μs | 37.3187 μs | 3,030.646 μs |
| LINQ | 700000 | 7,823.911 μs | 99.7116 μs | 88.3917 μs | 7,806.146 μs |
| for | 800000 | 3,570.393 μs | 44.4061 μs | 41.5375 μs | 3,575.188 μs |
| foreach | 800000 | 3,483.000 μs | 68.0819 μs | 66.8656 μs | 3,479.209 μs |
| LINQ | 800000 | 8,985.376 μs | 178.2623 μs | 225.4447 μs | 8,932.583 μs |
| for | 900000 | 3,986.935 μs | 54.6625 μs | 45.6457 μs | 3,972.230 μs |
| foreach | 900000 | 3,920.466 μs | 63.2092 μs | 52.7825 μs | 3,936.688 μs |
| LINQ | 900000 | 9,914.489 μs | 86.5025 μs | 80.9145 μs | 9,922.167 μs |
| for | 1000000 | 4,489.122 μs | 82.2601 μs | 72.9214 μs | 4,478.438 μs |
| foreach | 1000000 | 4,355.026 μs | 85.2142 μs | 83.6918 μs | 4,363.458 μs |
| LINQ | 1000000 | 10,629.327 μs | 211.0438 μs | 517.6940 μs | 10,396.626 μs |
| for | 2000000 | 8,896.910 μs | 82.2718 μs | 72.9318 μs | 8,910.729 μs |
| foreach | 2000000 | 8,582.566 μs | 80.1089 μs | 71.0145 μs | 8,578.541 μs |
| LINQ | 2000000 | 22,281.354 μs | 246.3951 μs | 218.4229 μs | 22,351.312 μs |
| for | 3000000 | 13,546.536 μs | 267.8750 μs | 297.7424 μs | 13,481.292 μs |
| foreach | 3000000 | 13,505.183 μs | 264.5349 μs | 247.4461 μs | 13,482.791 μs |
| LINQ | 3000000 | 32,951.134 μs | 431.7526 μs | 382.7374 μs | 32,856.355 μs |
According to the iteration case results, for and foreach showed very efficient performances. Foreach narrowly outstripped for. LINQ, on the other hand, was quite slow.
// * Summary *
BenchmarkDotNet=v0.13.2, OS=macOS 13.0 (22A380) [Darwin 22.1.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.402
[Host] : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
TestQuize : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
Job=TestQuize InvocationCount=1 UnrollFactor=1
| Method | size | Mean | Error | StdDev | Median | Ratio | RatioSD |
|-------------- |-------- |-------------:|-------------:|--------------:|-------------:|------:|--------:|
| for | 1000 | 186.4 μs | 3.73 μs | 9.63 μs | 185.6 μs | 1.00 | 0.00 |
| foreach | 1000 | 358.0 μs | 7.12 μs | 12.08 μs | 360.2 μs | 1.95 | 0.17 |
| 'LINQ select' | 1000 | 238.6 μs | 27.59 μs | 81.35 μs | 176.7 μs | 1.39 | 0.48 |
| | | | | | | | |
| for | 10000 | 1,230.7 μs | 6.84 μs | 6.40 μs | 1,231.0 μs | 1.00 | 0.00 |
| foreach | 10000 | 1,101.8 μs | 21.69 μs | 42.31 μs | 1,086.2 μs | 0.91 | 0.05 |
| 'LINQ select' | 10000 | 1,093.3 μs | 14.86 μs | 18.25 μs | 1,085.1 μs | 0.89 | 0.02 |
| | | | | | | | |
| for | 100000 | 19,616.3 μs | 394.61 μs | 1,157.32 μs | 19,339.2 μs | 1.00 | 0.00 |
| foreach | 100000 | 19,151.5 μs | 374.81 μs | 757.13 μs | 19,346.4 μs | 0.98 | 0.07 |
| 'LINQ select' | 100000 | 19,563.5 μs | 444.96 μs | 1,305.00 μs | 19,216.3 μs | 1.00 | 0.09 |
| | | | | | | | |
| for | 200000 | 42,581.8 μs | 826.18 μs | 1,236.59 μs | 42,422.5 μs | 1.00 | 0.00 |
| foreach | 200000 | 43,205.4 μs | 862.50 μs | 2,226.40 μs | 43,119.1 μs | 1.04 | 0.08 |
| 'LINQ select' | 200000 | 42,555.0 μs | 848.24 μs | 1,345.41 μs | 42,394.7 μs | 1.00 | 0.04 |
| | | | | | | | |
| for | 300000 | 68,894.2 μs | 1,329.43 μs | 1,477.66 μs | 68,809.1 μs | 1.00 | 0.00 |
| foreach | 300000 | 69,128.5 μs | 1,099.22 μs | 1,028.21 μs | 69,227.3 μs | 1.00 | 0.03 |
| 'LINQ select' | 300000 | 68,098.9 μs | 1,188.56 μs | 1,503.14 μs | 67,699.5 μs | 0.99 | 0.04 |
| | | | | | | | |
| for | 400000 | 93,229.3 μs | 1,859.60 μs | 1,739.47 μs | 93,683.8 μs | 1.00 | 0.00 |
| foreach | 400000 | 89,551.9 μs | 1,790.68 μs | 1,990.34 μs | 90,615.7 μs | 0.96 | 0.03 |
| 'LINQ select' | 400000 | 92,667.5 μs | 937.26 μs | 876.72 μs | 92,906.5 μs | 0.99 | 0.02 |
| | | | | | | | |
| for | 500000 | 113,604.1 μs | 2,270.00 μs | 3,030.38 μs | 111,885.2 μs | 1.00 | 0.00 |
| foreach | 500000 | 114,814.7 μs | 2,013.13 μs | 1,784.59 μs | 114,390.6 μs | 1.02 | 0.02 |
| 'LINQ select' | 500000 | 115,807.9 μs | 2,272.83 μs | 2,705.64 μs | 115,469.5 μs | 1.02 | 0.03 |
| | | | | | | | |
| for | 600000 | 138,300.0 μs | 703.80 μs | 658.34 μs | 138,435.2 μs | 1.00 | 0.00 |
| foreach | 600000 | 141,649.6 μs | 1,851.61 μs | 1,732.00 μs | 141,811.5 μs | 1.02 | 0.01 |
| 'LINQ select' | 600000 | 138,510.8 μs | 1,184.60 μs | 989.20 μs | 138,807.2 μs | 1.00 | 0.01 |
| | | | | | | | |
| for | 700000 | 196,495.0 μs | 3,810.24 μs | 5,086.55 μs | 197,209.0 μs | 1.00 | 0.00 |
| foreach | 700000 | 201,671.9 μs | 3,939.84 μs | 5,650.40 μs | 203,067.4 μs | 1.03 | 0.04 |
| 'LINQ select' | 700000 | 197,219.0 μs | 3,898.34 μs | 5,834.85 μs | 198,643.8 μs | 1.01 | 0.04 |
| | | | | | | | |
| for | 800000 | 209,354.0 μs | 3,532.26 μs | 3,304.08 μs | 208,252.3 μs | 1.00 | 0.00 |
| foreach | 800000 | 209,050.6 μs | 2,450.00 μs | 2,723.17 μs | 207,971.4 μs | 1.00 | 0.02 |
| 'LINQ select' | 800000 | 215,563.5 μs | 4,193.36 μs | 5,598.02 μs | 215,312.8 μs | 1.03 | 0.03 |
| | | | | | | | |
| for | 900000 | 227,232.8 μs | 3,312.04 μs | 3,098.08 μs | 226,833.9 μs | 1.00 | 0.00 |
| foreach | 900000 | 222,250.3 μs | 4,325.60 μs | 4,807.89 μs | 221,545.5 μs | 0.98 | 0.03 |
| 'LINQ select' | 900000 | 220,078.2 μs | 1,687.53 μs | 1,409.16 μs | 219,890.0 μs | 0.97 | 0.01 |
| | | | | | | | |
| for | 1000000 | 240,397.8 μs | 1,470.56 μs | 1,375.56 μs | 240,312.8 μs | 1.00 | 0.00 |
| foreach | 1000000 | 234,540.0 μs | 3,196.72 μs | 2,990.21 μs | 233,486.8 μs | 0.98 | 0.01 |
| 'LINQ select' | 1000000 | 242,308.3 μs | 2,731.83 μs | 2,555.36 μs | 242,412.0 μs | 1.01 | 0.01 |
| | | | | | | | |
| for | 2000000 | 445,187.4 μs | 40,642.40 μs | 119,197.12 μs | 405,413.1 μs | 1.00 | 0.00 |
| foreach | 2000000 | 333,020.2 μs | 11,738.20 μs | 34,610.35 μs | 343,528.1 μs | 0.80 | 0.22 |
| 'LINQ select' | 2000000 | 308,329.0 μs | 11,740.80 μs | 34,618.02 μs | 320,886.7 μs | 0.74 | 0.21 |
| | | | | | | | |
| for | 3000000 | 476,453.4 μs | 9,462.91 μs | 22,672.53 μs | 484,952.5 μs | 1.00 | 0.00 |
| foreach | 3000000 | 485,748.1 μs | 9,699.31 μs | 21,693.91 μs | 485,431.8 μs | 1.03 | 0.07 |
| 'LINQ select' | 3000000 | 491,490.7 μs | 9,813.60 μs | 24,620.41 μs | 493,051.0 μs | 1.03 | 0.07 |
Transformation case results were quite complex. It was pretty hard to say for sure. They seem to have given almost the same performance, with a few exceptions.
Bonus
After these results, I decided to use a pointer in comparison. I wondered if the pointer could be more performant. I wanted to see this. However, I had to change the script in the cases to use pointers. In order to represent an array with a pointer, the array must have a simple data type. For this reason, I decided to have an integer in the array. I examined the performance of an array holding Integer type Points in a filtering case. For and foreach were champions of the benchmark. But I couldn’t use a pointer for foreach. That’s why I chose for to compare with the pointer. I decided to reduce the array sizes, because I expected too much in the previous comparison.
I designed two for loops, with and without a pointer, in the same scenario where the points of the user entity were filtered.
public class FiterTestQuize:BaseTestQuize
{
[Benchmark(Baseline = true, Description = "for int Array")]
public int[] ForLoopIntArray()
{
var result = new List<int>();
for (int i = 0; i < points.Length; i++)
{
if (points[i]!<200)
{
result.Add(points[i]);
}
}
return result.ToArray();
}
[Benchmark(Description = "for int Array Ptr")]
public int[] ForLoopIntArrayPtr()
{
var result = new List<int>();
unsafe
{
fixed (int* ptr = points)
{
for (var i = 0; i < size; i++)
{
if (*(ptr + i)!<200)
{
result.Add(*(ptr + i));
}
}
}
}
return result.ToArray();
}
}
Let’s see the results after doing the same build process.
BenchmarkDotNet=v0.13.2, OS=macOS 13.0 (22A380) [Darwin 22.1.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.402
[Host] : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
TestQuize : .NET 6.0.10 (6.0.1022.47605), Arm64 RyuJIT AdvSIMD
Job=TestQuize InvocationCount=1 UnrollFactor=1
| Method | size | Mean | Error | StdDev | Median | Ratio | RatioSD |
|-------------------- |------- |-------------:|-----------:|------------:|-------------:|------:|--------:|
| 'for int Array' | 1000 | 3.938 μs | 0.1014 μs | 0.2876 μs | 3.875 μs | 1.00 | 0.00 |
| 'for int Array Ptr' | 1000 | 3.333 μs | 0.0714 μs | 0.1980 μs | 3.292 μs | 0.85 | 0.07 |
| | | | | | | | |
| 'for int Array' | 100000 | 277.841 μs | 3.9947 μs | 3.1188 μs | 277.125 μs | 1.00 | 0.00 |
| 'for int Array Ptr' | 100000 | 240.987 μs | 5.0171 μs | 14.6351 μs | 232.958 μs | 0.88 | 0.06 |
| | | | | | | | |
| 'for int Array' | 800000 | 2,323.790 μs | 46.4424 μs | 135.4746 μs | 2,391.312 μs | 1.00 | 0.00 |
| 'for int Array Ptr' | 800000 | 1,265.292 μs | 41.2887 μs | 110.9193 μs | 1,280.478 μs | 0.55 | 0.06 |
According to the new results, the for loop using pointers seemed to be more efficient. Especially as the array size increased, the performance difference became more obvious.
Inferences
After all the benchmarks, we can say that for and foreach perform almost the same. Pointers can also be used for even greater performance. However, in my personal opinion, we should benchmark this way just to get an idea. Because performance is not the only criterion for a software. Code readability, ease of development, system requirements, and system resources should also be evaluated. According to these criteria (for, foreach, do-while, while, LINQ, pointer etc.), all loops can provide functionality to your code according to your needs.
Please contact me if you would like to add to or correct any parts of this article. All the best…