اندازهی قلم متن
تخمین مدت زمان مطالعهی مطلب:
شش دقیقه
بالاخره تفاوت کارآیی بین حلقههای for و foreach در دات نت 7 برطرف شدهاست که این مورد نیز یکی دیگر از دلایل بهبود کارآیی LINQ در دات نت 7 است. در این مطلب به همراه آزمایشی، این مورد را بررسی خواهیم کرد.
تدارک یک آزمایش برای بررسی کارآیی حلقههای for و foreach در دات نت 7
یک برنامهی کنسول جدید را ایجاد کرده و سپس کتابخانهی BenchmarkDotNet را با TargetFramework دات نت 7 به صورت زیر به پروژه اضافه میکنیم:
در ادامه به این پروژه، کلاس زیر را اضافه میکنیم:
که توسط دستورات زیر در حالت release اجرا شده و نتایج نهایی را نمایش میدهد:
توضیحات:
- میتوان یک پروژه را یکبار بر اساس دات نت 7 و یکبار هم بر اساس دات نت 6 با تغییر target framework آنها کامپایل و اجرا کرد تا بتوان نتایج این دو را با هم مقایسه کرد و یا میتوان با ذکر [SimpleJob(RuntimeMoniker.Net60)] و همچنین [SimpleJob(RuntimeMoniker.Net70)]، این مورد را به صورت خودکار به BenchmarkDotNet دات نت واگذار کرد.
- در این آزمایش، ابتدا یک آرایه و یک لیست را تهیه میکنیم.
- سپس یکبار حلقههای for و foreach را بر روی آرایه و همین عملیات را بر روی لیست تهیه شده، تکرار میکنیم.
نتایج حاصل به صورت زیر هستند:
همانطور که در نتایج فوق هم مشاهده میکنید:
در دات نت 6
- تفاوتی بین کارآیی حلقههای for و foreach، زمانیکه بر روی یک آرایه اجرا میشوند، وجود ندارد.
- اما کارآیی حلقهی foreach نسبت به حلقهی for، زمانیکه بر روی یک لیست اجرا میشوند، تقریبا 50 درصد کمتر است.
در دات نت 7
- تفاوتی بین کارآیی حلقههای for و forach، زمانیکه بر روی یک آرایه اجرا میشوند، وجود ندارد. بنابراین از این لحاظ با دات نت 6 تفاوتی ندارد.
- اما کارآیی حلقهی foreach نسبت به حلقهی for، زمانیکه بر روی یک لیست اجرا میشود، تقریبا یکسان و قابل چشمپوشی است. یعنی در دات نت 7، کارآیی این دو حلقه یکی شدهاست. اما چرا؟
روشی در جهت یافتن یکی بودن سرعت حلقههای for و foreach بر اساس خروجی کامپایلر
با مشاهدهی نتایج حاصل از BenchmarkDotNet میتوان به بهبود کارآیی حاصل پیبرد؛ اما برای مثال چرا زمانیکه از آرایه استفاده میشود، حتی در دات نت 6، تفاوتی بین دو حلقهی for و foreach وجود ندارد، اما زمانیکه از لیستها استفاده میشود، این کارآیی 50 درصد افت میکند؟
در این خروجی بهتر میتوان مشاهده کرد که چرا در حالت استفادهی از آرایهها، تفاوتی بین حلقههای for و foreach نیست؛ چون هر دو به صورت حلقهی for تفسیر میشوند:
اما زمانیکه به لیستها میرسیم، حلقهی foreach به صورت زیر تفسیر میشود که بدیهی است نسبت به حلقهی for، کندتر اجرا خواهد شد:
اگر این خروجی را برای دات نت 6 و دات نت 7 تهیه کنیم، به یک جواب خواهیم رسید. یعنی از دیدگاه #Low-level C، تفاوتی بین IL دات نت 6 و 7 از این لحاظ وجود ندارد. تفاوتی اصلی در بهبودهای JIT دات نت 7 است که سبب شده، خروجی نهایی حلقههای foreach با for یکی باشد.
تدارک یک آزمایش برای بررسی کارآیی حلقههای for و foreach در دات نت 7
یک برنامهی کنسول جدید را ایجاد کرده و سپس کتابخانهی BenchmarkDotNet را با TargetFramework دات نت 7 به صورت زیر به پروژه اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include="BenchmarkDotNet" Version="0.13.4" /> </ItemGroup> </Project>
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; namespace NET7Loops; [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net70)] [MemoryDiagnoser(false)] public class Benchmarks { private int[] ItemsArray; private List<int> ItemsList; [GlobalSetup] public void Setup() { var random = new Random(420); var randomItems = Enumerable.Range(0, 1000).Select(_ => random.Next()); ItemsArray = randomItems.ToArray(); ItemsList = randomItems.ToList(); } [Benchmark] public void For_Array() { for (var i = 0; i < ItemsArray.Length; i++) { var item = ItemsArray[i]; } } [Benchmark] public void For_List() { for (var i = 0; i < ItemsList.Count; i++) { var item = ItemsList[i]; } } [Benchmark] public void ForEach_Array() { foreach (var item in ItemsArray) { } } [Benchmark] public void ForEach_List() { foreach (var item in ItemsList) { } } }
using BenchmarkDotNet.Running; using NET7Loops; BenchmarkRunner.Run<Benchmarks>();
- میتوان یک پروژه را یکبار بر اساس دات نت 7 و یکبار هم بر اساس دات نت 6 با تغییر target framework آنها کامپایل و اجرا کرد تا بتوان نتایج این دو را با هم مقایسه کرد و یا میتوان با ذکر [SimpleJob(RuntimeMoniker.Net60)] و همچنین [SimpleJob(RuntimeMoniker.Net70)]، این مورد را به صورت خودکار به BenchmarkDotNet دات نت واگذار کرد.
- در این آزمایش، ابتدا یک آرایه و یک لیست را تهیه میکنیم.
- سپس یکبار حلقههای for و foreach را بر روی آرایه و همین عملیات را بر روی لیست تهیه شده، تکرار میکنیم.
نتایج حاصل به صورت زیر هستند:
همانطور که در نتایج فوق هم مشاهده میکنید:
در دات نت 6
- تفاوتی بین کارآیی حلقههای for و foreach، زمانیکه بر روی یک آرایه اجرا میشوند، وجود ندارد.
- اما کارآیی حلقهی foreach نسبت به حلقهی for، زمانیکه بر روی یک لیست اجرا میشوند، تقریبا 50 درصد کمتر است.
در دات نت 7
- تفاوتی بین کارآیی حلقههای for و forach، زمانیکه بر روی یک آرایه اجرا میشوند، وجود ندارد. بنابراین از این لحاظ با دات نت 6 تفاوتی ندارد.
- اما کارآیی حلقهی foreach نسبت به حلقهی for، زمانیکه بر روی یک لیست اجرا میشود، تقریبا یکسان و قابل چشمپوشی است. یعنی در دات نت 7، کارآیی این دو حلقه یکی شدهاست. اما چرا؟
روشی در جهت یافتن یکی بودن سرعت حلقههای for و foreach بر اساس خروجی کامپایلر
با مشاهدهی نتایج حاصل از BenchmarkDotNet میتوان به بهبود کارآیی حاصل پیبرد؛ اما برای مثال چرا زمانیکه از آرایه استفاده میشود، حتی در دات نت 6، تفاوتی بین دو حلقهی for و foreach وجود ندارد، اما زمانیکه از لیستها استفاده میشود، این کارآیی 50 درصد افت میکند؟
برای پاسخ به این سؤال میتوان از IL Viewer موجود در Rider استفاده کرد که آخرین نگارش آن به همراه نمایش #Low-level C هم هست:
این همان خروجی است که توسط کامپایلر، پیش از تولید کدهای باینری نهایی، تهیه میشود. یعنی اگر قصد داشته باشیم تا درک کامپایلر را نسبت به قطعه کدی مشاهده کنیم، میتوان به این خروجی مراجعه کرد که به صورت زیر است:
// Decompiled with JetBrains decompiler // Type: NET7Loops.Benchmarks // Assembly: NET7Loops, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null // MVID: E398BEE7-8123-4C55-AF9A-F7D83DDA73F1 // Assembly location: C:\Prog\1401\Net7Tests\NET7Loops\bin\Debug\net7.0\NET7Loops.dll // Compiler-generated code is shown using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; namespace NET7Loops { [NullableContext(1)] [Nullable(0)] [SimpleJob(RuntimeMoniker.Net60, -1, -1, -1, -1, null, false)] [SimpleJob(RuntimeMoniker.Net70, -1, -1, -1, -1, null, false)] [MemoryDiagnoser(false)] public class Benchmarks { private int[] ItemsArray; private List<int> ItemsList; [GlobalSetup] public void Setup() { Benchmarks.<>c__DisplayClass2_0 cDisplayClass20 = new Benchmarks.<>c__DisplayClass2_0(); cDisplayClass20.random = new Random(420); IEnumerable<int> source = Enumerable.Range(0, 1000).Select<int, int>(new Func<int, int>((object) cDisplayClass20, __methodptr(<Setup>b__0))); this.ItemsArray = source.ToArray<int>(); this.ItemsList = source.ToList<int>(); } [Benchmark(23, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")] public void For_Array() { for (int index = 0; index < this.ItemsArray.Length; ++index) { int items = this.ItemsArray[index]; } } [Benchmark(32, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")] public void For_List() { for (int index = 0; index < this.ItemsList.Count; ++index) { int items = this.ItemsList[index]; } } [Benchmark(41, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")] public void ForEach_Array() { int[] itemsArray = this.ItemsArray; for (int index = 0; index < itemsArray.Length; ++index) { int num = itemsArray[index]; } } [Benchmark(49, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")] public void ForEach_List() { List<int>.Enumerator enumerator = this.ItemsList.GetEnumerator(); try { while (enumerator.MoveNext()) { int current = enumerator.Current; } } finally { enumerator.Dispose(); } } public Benchmarks() { base..ctor(); } [CompilerGenerated] private sealed class <>c__DisplayClass2_0 { [Nullable(0)] public Random random; public <>c__DisplayClass2_0() { base..ctor(); } internal int <Setup>b__0(int _) { return this.random.Next(); } } } }
for (int index = 0; index < this.ItemsArray.Length; ++index) { int items = this.ItemsArray[index]; }
List<int>.Enumerator enumerator = this.ItemsList.GetEnumerator(); try { while (enumerator.MoveNext()) { int current = enumerator.Current; } } finally { enumerator.Dispose(); }