اشتراکها
بالاخره تفاوت کارآیی بین حلقههای 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(); }
نظرات نظرسنجیها
شما برای کار با دیتا در اندروید، کدامیک از روش های زیر را استفاده میکنید یا ترجیح می دهید؟
لینک زیر میتونه برای انتخاب ابزار مناسب برای کار با دادهها مناسب باشه!
Android-ORM-benchmark از آنجا که برای کار با جاوا اسکریپت نیاز به درک کاملی دربارهی مفهوم حوزه کارکرد متغیرها (Scope) میباشد و نحوه فراخوانی توابع نیز نقش اساسی در این مورد بازی میکند، در این قسمت با این موارد آشنا خواهیم شد:
جاوا اسکریپت از مفهومی به نام functional scope برای تعیین حوزه متغیرها استفاده میکند و به این معنی است که با تعریف توابع، حوزه عملکرد متغیر مشخص میشود. در واقع هر متغیری که در یک تابع تعریف میشود در کلیه قسمتهای آن تابع، از قبیل If statement – for loops و حتی nested function نیز در دسترس میباشد.
اجازه دهید با مثالی این موضوع را بررسی نماییم.
function testScope() { var myTest = true; if (true) { var myTest = "I am changed!" } alert(myTest); } testScope(); // will alert "I am changed!"
function testScope() { var myTest = true; if (true) { var myTest = "I am changed!" } alert(myTest); } testScope(); // will alert "I am changed!" alert(myTest); // will throw a reference error, because it doesn't exist outside of the function
1 – متغیر myTest را در بیرون بلاک testScope() تعریف کنیم
2 – هنگام تعریف متغیر myTest، کلمه کلیدی var را حذف کنیم که این موضوع باعث میشود این متغیر در کل window قابل دسترس باشد و یا به عبارتی متغیر global میشود.
قبل از پرداختن به ادامه بحث خواندن مقاله مربوط به Closure در جاوااسکریپت توصیه میگردد .
در پایان بحث Scopeها با یک مثال نسبتا جامع اکثر این حالات به همراه خروجی را نشان میدهیم :
<script type="text/javascript"> // a globally-scoped variable var a = 1; // global scope function one() { alert(a); } // local scope function two(a) { alert(a); } // local scope again function three() { var a = 3; alert(a); } // Intermediate: no such thing as block scope in javascript function four() { if (true) { var a = 4; } alert(a); // alerts '4', not the global value of '1' } // Intermediate: object properties function Five() { this.a = 5; } // Advanced: closure var six = function () { var foo = 6; return function () { // javascript "closure" means I have access to foo in here, // because it is defined in the function in which I was defined. alert(foo); } }() // Advanced: prototype-based scope resolution function Seven() { this.a = 7; } // [object].prototype.property loses to [object].property in the lookup chain Seven.prototype.a = -1; // won't get reached, because 'a' is set in the constructor above. Seven.prototype.b = 8; // Will get reached, even though 'b' is NOT set in the constructor. // These will print 1-8 one(); two(2); three(); four(); alert(new Five().a); six(); alert(new Seven().a); alert(new Seven().b); </Script>
Function Invocation Patterns In JavaScript :
از آنجا که توابع در جاوااسکریپت به منظور 1 – ساخت اشیاء و 2 – حوزه دسترسی متغیرها(Scope) نقش اساسی ایفا میکنند بهتر است کمی درباره استفاده و نحوه فراخوانی آنها (Function Invocation Patterns) در جاوااسکریپت بحث نماییم.
در جاوااسکریپت 4 مدل فراخوانی تابع داریم که به نامهای زیر مطرح هستند:
1. Method Invocation
2. Function Invocation
3. Constructor Invocation
4. Apply And Call Invocation
در فراخوانی توابع به هر یک از روشهای بالا باید به این نکته توجه داشت که حوزه دسترسی متغیرها در جاوااسکریپت ابتدا و انتهای توابع هستند و اگر به عنوان مثال از توابع تو در تو استفاده کردیم ،حوزه شی this برای توابع داخلی تغییر خواهد کرد .این موضوع را در طی مثالهایی نشان خواهیم داد.
Method Invocation :
وقتی یک تابع قسمتی از یک شی باشد به آن متد میگوییم به عنوان مثال :
var obj = { value: 0, increment: function() { this.value+=1; } }; obj.increment(); //Method invocation
Function Invocation:
در اینحالت که از () برای فراخوانی تابع استفاده میگردد ،This به شی سراسری (global object ) اشاره میکند؛ منظور اینکه this به اجزای تابعی که فراخوانی آن انجام شده اشاره نمیکند. اجازه دهید با مثالی این موضوع را روشن کنیم
<script type="text/javascript"> var value = 500; //Global variable var obj = { value: 0, increment: function() { this.value++; var innerFunction = function() { alert(this.value); } innerFunction(); //Function invocation pattern } } obj.increment(); //Method invocation pattern <script type="text/javascript"> Result : 500
برای حل این گونه مشکلات ساختار کد نویسی ما بایستی به شکل زیر باشد :
<script type="text/javascript"> var value = 500; //Global variable var obj = { value: 0, increment: function() { var that = this; that.value++; var innerFunction = function() { alert(that.value); } innerFunction(); //Function invocation pattern } } obj.increment(); <script type="text/javascript"> Result : 1
Constructor Invocation :
در این روش برای فراخوانی تابع از کلمه new استفاده میکنیم. در این حالت یک شیء مجزا ایجاد شده و به متغیر دلخواه ما اختصاص پیدا میکند. به عنوان مثال داریم :
var Dog = function(name) { //this == brand new object ({}); this.name = name; this.age = (Math.random() * 5) + 1; }; var myDog = new Dog('Spike'); //myDog.name == 'Spike' //myDog.age == 2 var yourDog = new Dog('Spot'); //yourDog.name == 'Spot' //yourDog.age == 4
اگر از new استفاده نشود متغیر myDog ،undefined میشود.
یک مثال دیگر :
var createCallBack = function(init) { //First function return new function() { //Second function by Constructor Invocation var that = this; this.message = init; return function() { //Third function alert(that.message); } } } window.addEventListener('load', createCallBack("First Message")); window.addEventListener('load', createCallBack("Second Message"));
Apply And Call Invocation:
تمامی توابع جاوااسکریپت دارای دو متد توکار apply() و call() هستند که توسط این متدها میتوان این توابع را با context دلخواه فراخوانی کرد.
نحوه فراخوانی به شکل مقابل است :
myFunction.apply(thisContext, arrArgs); myFunction.call(thisContext, arg1, arg2, arg3, ..., argN);
مثال :
var contextObject = { testContext: 10 } var otherContextObject = { testContext: "Hello World!" } var testContext = 15; // Global variable function testFunction() { alert(this.testContext); } testFunction(); // This will alert 15 testFunction.call(contextObject); // Will alert 10 testFunction.apply(otherContextObject); // Will alert "Hello World”
یک مثال کاملتر :
var o = { i : 0, F : function() { var a = function() { this.i = 42; }; a(); document.write(this.i); } }; o.F(); Result :0
برای حل این مشکل 2 راه وجود دارد
راه اول :
var p = { i : 0, F : function() { var a = function() { this.i = 42; }; a.apply(this); document.write(this.i); } }; p.F(); Result :42
یا اینکه همانند مثالهای قبلی :
var q = { i: 0, F: function F() { var that = this; var a = function () { that.i = 42; } a(); document.write(this.i); } } q.F();
منابع :
Javascript programmer,s refrence
این روزها جهت اندازهگیری کارآیی قطعات کدهای دات نتی، استفاده از فریم ورکهای مخصوصی که بسیاری از نکات ریز مرتبط با اینگونه اندازهگیریها را مانند warmup یا گرم کردن JIT (جهت عدم اندازه گیری زمان کامپایل پویای کدها، بجای زمان واقعی اجرای آنها)، اندازهگیری فشار بر روی Garbage collector و غیره را انجام میدهند، بجای استفادهی از Stop Watch، متداول است. یکی از معروفترینهای این گروه، که تقریبا حالت استانداردی را در جهت اندازه گیری کارآیی کدهای دات نتی پیدا کردهاست، فریم ورک سورس باز NBench است.
شروع به کار با NBench
برای شروع به کار با NBench، ابتدا نیاز است دو بستهی نیوگت ذیل را نصب کرد:
عملکرد این فریم ورک، شبیه به عملکرد فریم ورکهای آزمونهای واحد است. برای مثال فرض کنید که میخواهید فشار حافظه و فشار بر روی GC قطعه کدی را اندازه گیری کنید:
همانند نوشتن متدهای آزمونهای واحد، ابتدا یک یا چند متد public void را در اینجا اضافه میکنیم.
سپس هر متد تست به ویژگی PerfBenchmark مزین میشود. در اینجا RunMode.Iterations به این معنا است که خودمان قصد داریم در طی یک حلقه، تعداد بار انجام را مشخص کنیم.
ویژگی MemoryMeasurement برای اندازه گیری حافظهی مصرفی یک قطعه کد و GcMeasurement برای اندازه گیری فشار بر روی Garbage collector بکار میرود.
اجرای آزمونهای NBench
پس از تهیهی دو متد فوق، به پوشهی packages\NBench.Runner.0.3.4\lib\net45 مراجعه کنید. یک فایل exe در آن موجود است که کار یافتن و اجرای آزمونهای NBench را انجام میدهد. به عنوان پارامتر آن تنها کافی است مسیر اسمبلی برنامه (فایل exe و یا dll) را به آن ارسال کنیم:
پس از آن، کار اجرای آزمونهای NBench شروع شده و پس از مدتی ابتدا BEGIN WARMUP و END WARMUPها را میتوان مشاهده کرد و در آخر یک چنین خروجی ارائه میشود:
نکتهای در مورد اندازه گیری فشار حافظه
حافظه توسط سیستم عامل، به صورت صفحات تخصیص داده میشود. برای مثال اگر شما به 12 بایت نیاز داشته باشید، سیستم عامل ممکن است 8 کیلوبایت را جهت کاهش تعداد بار تخصیصهای حافظه و بالا بردن سرعت کار، در اختیار برنامه قرار دهد. بنابراین جهت رسیدن به بهترین نتیجه، در اینجا بهتر است تعداد زیادی شیء را مورد آزمایش قرار داد. برای مثال در آزمایش فوق بجای افزودن یک آیتم به دیکشنری، افزودن میلیونها شیء، نویز استراتژی تخصیص حافظهی توسط سیستم عامل را به حداقل میرساند.
شبیه به همین استراتژی، در پیاده سازی Dictionary نیز بکارگرفته شدهاست:
اگر اینبار این آزمون را انجام دهیم، به نتیجهی ذیل خواهیم رسید:
در اینجا زمانیکه شیء دیکشنری ایجاد شدهاست، اندازهی اولیهی آن نیز مشخص گردیدهاست. همین مساله سبب شدهاست تا مصرف حافظهی آن از نزدیک به 41 مگابایت (متد AddMemoryMeasurement ابتدای بحث) به نزدیک 24 مگابایت (متد AddMemoryMeasurement_With_initial_Size فوق) کاهش یابد.
علت اینجا است که دیکشنری در پشت صحنه، از یک متد ReSize استفاده میکند که شبیه به سیستم عامل، بیشتر از مقدار مورد نیاز جهت ذخیرهی اشیاء، برای کاهش تعداد بار تخصیصهای حافظه، حافظه به خود اختصاص میدهد. به همین جهت زمانیکه اندازهی اولیه را مشخص کردهایم، کار تخصیص حافظهی بیش از اندازهی این شیء، به شدت کاهش یافتهاست.
بررسی متد MeasureGarbageCollections
در متد MeasureGarbageCollections، مقدار زیادی شیء بر روی heap ایجاد شده و GC را وادار به عکس العمل شدید میکند.
حلقهی داخلی ایجاد شده نیز تعداد زیادی شیء را در جهت پاکسازی GC تخصیص میدهد. این پاکسازی در مرحلهای به نام generation 0 صورت میگیرد.
اشیاء اضافه شدهی به لیست، طول عمر بیشتری دارند (تا پایان حلقه). بنابراین از garbage collection at generation 0 جان سالم به در خواهند برد و در garbage collection at generation 1 به عمر آنها پایان داده خواهد شد. هرچند ممکن است تعدادی از آنها پاکسازی نشده و تا پایان full garbage collection (generation 2) باقی بمانند.
در آزمایش انجام شده، با ذکر GcGeneration.AllGc، هر سه مورد Gen0 تا Gen2 اندازه گیری خواهند شد. عموما اندازه گیری Gen0 و Gen1 مهم نیستند و اینها خیلی زود به پایان خواهند رسید. اگر تعداد بار رخدادن Gen2 زیاد بود (یا اصلا وجود داشت)، میتواند سبب بروز مشکلات کارآیی شدیدی گردد.
بنابراین میتوان بجای تنظیم GcGeneration.AllGc، صرفا از GcGeneration.Gen2 استفاده کرد.
اندازهگیری Throughput یا تعداد بار اجرای یک متد در ثانیه
روش دیگر کار با فریم ورک NBench، ایجاد یک کلاس مخصوص و سپس افزودن متدهای Setup مزین به PerfSetup، متد Cleanup مزین به PerfCleanup و سپس تعدادی متد اندازه گیری کارآیی توسط ویژگی PerfBenchmark است. در اینجا برای اندازهگیری سرعت اجرای متدها، از ویژگی CounterThroughputAssertion استفاده خواهد شد که پارامتر اول آن نام یک شمارشگر است. این شمارشگر در متد Setup ایجاد میشود (با یک نام دلخواه).
در این آزمایشها، RunMode.Throughput به معنای اجرای متد آزمایش به تعداد AverageOperationsPerSecond توسط فریم ورک NBench است. در حالت قید RunMode.Iterations، تعداد بار اجرا، توسط حلقهای که ما مشخص کردهایم، تعیین میگردد.
اگر دقت کنید، کارآیی اندازه گیری شدهی در حالت RunMode.Iterations بیشتر است از حالت RunMode.Throughput. چون در حالت RunMode.Throughput، فریم ورک کار اجرای متد را از طریق Reflection انجام میدهد. بنابراین بهتر است از حالت RunMode.Iterations، جهت رسیدن به نتایج دقیقتری استفاده کرد.
در اینجا برای گزارش دادن، عددهای Average و Average / s باید مورد استفاده قرار گیرند.
شروع به کار با NBench
برای شروع به کار با NBench، ابتدا نیاز است دو بستهی نیوگت ذیل را نصب کرد:
PM> Install-Package NBench PM> Install-Package NBench.Runner
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)] [MemoryMeasurement(MemoryMetric.TotalBytesAllocated)] public void AddMemoryMeasurement() { const int numberOfAdds = 1000000; var dictionary = new Dictionary<int, int>(); for (var i = 0; i < numberOfAdds; i++) { dictionary.Add(i, i); } } [PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)] [GcMeasurement(GcMetric.TotalCollections, GcGeneration.AllGc)] public void MeasureGarbageCollections() { var dataCache = new List<int[]>(); for (var i = 0; i < 500; i++) { for (var j = 0; j < 10000; j++) { var data = new int[100]; dataCache.Add(data.ToArray()); } dataCache.Clear(); } }
سپس هر متد تست به ویژگی PerfBenchmark مزین میشود. در اینجا RunMode.Iterations به این معنا است که خودمان قصد داریم در طی یک حلقه، تعداد بار انجام را مشخص کنیم.
ویژگی MemoryMeasurement برای اندازه گیری حافظهی مصرفی یک قطعه کد و GcMeasurement برای اندازه گیری فشار بر روی Garbage collector بکار میرود.
اجرای آزمونهای NBench
پس از تهیهی دو متد فوق، به پوشهی packages\NBench.Runner.0.3.4\lib\net45 مراجعه کنید. یک فایل exe در آن موجود است که کار یافتن و اجرای آزمونهای NBench را انجام میدهد. به عنوان پارامتر آن تنها کافی است مسیر اسمبلی برنامه (فایل exe و یا dll) را به آن ارسال کنیم:
D:\Prog\NBenchSample\packages\NBench.Runner.0.3.4\lib\net45\NBench.Runner.exe "D:\Prog\NBenchSample\NBenchSample\bin\Release\NBenchSample.exe"
--------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement --------------- TotalBytesAllocated: Max: 47,842,944.00 bytes, Average: 42,002,757.60 bytes, Min: 41,353,848.00 bytes, StdDev: 2,052,032.33 bytes TotalBytesAllocated: Max / s: 359,074,078.19 bytes, Average / s: 311,474,786.96 bytes, Min / s: 300,926,928.79 bytes, StdDev / s: 16,869,581.62 bytes --------------- RESULTS: NBenchSample.Program+MeasureGarbageCollections --------------- TotalCollections [Gen0]: Max: 708.00 collections, Average: 702.80 collections, Min: 697.00 collections, StdDev: 3.65 collections TotalCollections [Gen0]: Max / s: 111.55 collections, Average / s: 109.87 collections, Min / s: 107.88 collections, StdDev / s: 1.28 collections TotalCollections [Gen1]: Max: 338.00 collections, Average: 334.60 collections, Min: 330.00 collections, StdDev: 2.41 collections TotalCollections [Gen1]: Max / s: 53.61 collections, Average / s: 52.31 collections, Min / s: 51.10 collections, StdDev / s: 0.70 collections TotalCollections [Gen2]: Max: 32.00 collections, Average: 24.80 collections, Min: 18.00 collections, StdDev: 4.73 collections TotalCollections [Gen2]: Max / s: 4.91 collections, Average / s: 3.87 collections, Min / s: 2.86 collections, StdDev / s: 0.72 collections
نکتهای در مورد اندازه گیری فشار حافظه
حافظه توسط سیستم عامل، به صورت صفحات تخصیص داده میشود. برای مثال اگر شما به 12 بایت نیاز داشته باشید، سیستم عامل ممکن است 8 کیلوبایت را جهت کاهش تعداد بار تخصیصهای حافظه و بالا بردن سرعت کار، در اختیار برنامه قرار دهد. بنابراین جهت رسیدن به بهترین نتیجه، در اینجا بهتر است تعداد زیادی شیء را مورد آزمایش قرار داد. برای مثال در آزمایش فوق بجای افزودن یک آیتم به دیکشنری، افزودن میلیونها شیء، نویز استراتژی تخصیص حافظهی توسط سیستم عامل را به حداقل میرساند.
شبیه به همین استراتژی، در پیاده سازی Dictionary نیز بکارگرفته شدهاست:
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)] [MemoryMeasurement(MemoryMetric.TotalBytesAllocated)] public void AddMemoryMeasurement_With_initial_Size() { const int numberOfAdds = 1000000; var dictionary = new Dictionary<int, int>(numberOfAdds); for (var i = 0; i < numberOfAdds; i++) { dictionary.Add(i, i); } }
--------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement_With_initial_Size --------------- TotalBytesAllocated: Max: 23,245,912.00 bytes, Average: 23,245,912.00 bytes, Min: 23,245,912.00 bytes, StdDev: 0.00 bytes TotalBytesAllocated: Max / s: 394,032,435.34 bytes, Average / s: 389,108,363.43 bytes, Min / s: 378,502,981.34 bytes, StdDev / s: 5,575,519.09 bytes
علت اینجا است که دیکشنری در پشت صحنه، از یک متد ReSize استفاده میکند که شبیه به سیستم عامل، بیشتر از مقدار مورد نیاز جهت ذخیرهی اشیاء، برای کاهش تعداد بار تخصیصهای حافظه، حافظه به خود اختصاص میدهد. به همین جهت زمانیکه اندازهی اولیه را مشخص کردهایم، کار تخصیص حافظهی بیش از اندازهی این شیء، به شدت کاهش یافتهاست.
بررسی متد MeasureGarbageCollections
در متد MeasureGarbageCollections، مقدار زیادی شیء بر روی heap ایجاد شده و GC را وادار به عکس العمل شدید میکند.
حلقهی داخلی ایجاد شده نیز تعداد زیادی شیء را در جهت پاکسازی GC تخصیص میدهد. این پاکسازی در مرحلهای به نام generation 0 صورت میگیرد.
اشیاء اضافه شدهی به لیست، طول عمر بیشتری دارند (تا پایان حلقه). بنابراین از garbage collection at generation 0 جان سالم به در خواهند برد و در garbage collection at generation 1 به عمر آنها پایان داده خواهد شد. هرچند ممکن است تعدادی از آنها پاکسازی نشده و تا پایان full garbage collection (generation 2) باقی بمانند.
در آزمایش انجام شده، با ذکر GcGeneration.AllGc، هر سه مورد Gen0 تا Gen2 اندازه گیری خواهند شد. عموما اندازه گیری Gen0 و Gen1 مهم نیستند و اینها خیلی زود به پایان خواهند رسید. اگر تعداد بار رخدادن Gen2 زیاد بود (یا اصلا وجود داشت)، میتواند سبب بروز مشکلات کارآیی شدیدی گردد.
بنابراین میتوان بجای تنظیم GcGeneration.AllGc، صرفا از GcGeneration.Gen2 استفاده کرد.
اندازهگیری Throughput یا تعداد بار اجرای یک متد در ثانیه
روش دیگر کار با فریم ورک NBench، ایجاد یک کلاس مخصوص و سپس افزودن متدهای Setup مزین به PerfSetup، متد Cleanup مزین به PerfCleanup و سپس تعدادی متد اندازه گیری کارآیی توسط ویژگی PerfBenchmark است. در اینجا برای اندازهگیری سرعت اجرای متدها، از ویژگی CounterThroughputAssertion استفاده خواهد شد که پارامتر اول آن نام یک شمارشگر است. این شمارشگر در متد Setup ایجاد میشود (با یک نام دلخواه).
public class DictionaryThroughputTests { private readonly Dictionary<int, int> _dictionary = new Dictionary<int, int>(); private const string AddCounterName = "AddCounter"; private Counter _addCounter; private int _key; private const int AverageOperationsPerSecond = 20000000; [PerfSetup] public void Setup(BenchmarkContext context) { _addCounter = context.GetCounter(AddCounterName); _key = 0; } [PerfBenchmark(RunMode = RunMode.Throughput, TestMode = TestMode.Test)] [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)] public void AddThroughput_ThroughputMode(BenchmarkContext context) { _dictionary.Add(_key++, _key); _addCounter.Increment(); } [PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Test)] [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)] public void AddThroughput_IterationsMode(BenchmarkContext context) { for (var i = 0; i < AverageOperationsPerSecond; i++) { _dictionary.Add(i, i); _addCounter.Increment(); } } [PerfCleanup] public void Cleanup(BenchmarkContext context) { _dictionary.Clear(); } }
--------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_ThroughputMode --------------- [Counter] AddCounter: Max: 575,654.00 operations, Average: 575,654.00 operations, Min: 575,654.00 operations, StdDev: 0.00 operations [Counter] AddCounter: Max / s: 7,205,997.59 operations, Average / s: 7,163,894.30 operations, Min / s: 7,075,316.79 operations, StdDev / s: 42,518.20 operations --------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_IterationsMode --------------- [Counter] AddCounter: Max: 20,000,000.00 operations, Average: 20,000,000.00 operations, Min: 20,000,000.00 operations, StdDev: 0.00 operations [Counter] AddCounter: Max / s: 7,409,380.61 operations, Average / s: 7,250,991.24 operations, Min / s: 6,880,938.73 operations, StdDev / s: 148,085.19 operations
در اینجا برای گزارش دادن، عددهای Average و Average / s باید مورد استفاده قرار گیرند.
LINQ Extensions Library - Pivot Extensions
The pivot feature enables transforming a collection of objects into a new collection of objects flattening the original hierarchy
: In the following example The Pivot extension method is used to transform a collection as follow
اشتراکها
مثالی از کاربرد Azure DocumentDB
اشتراکها
کتابخانه 101
1) 101 will be maintained to minimize overlap with vanilla JS.
- 101 utils are made to work well with vanilla JS methods.
- 101 will only duplicate vanilla JS to provide functional programming paradigms or if the method is not available in a widely supported JS version (currently ES5).
- Underscore/lodash - duplicates a lot of ES5: forEach, map, reduce, filter, sort, and more.
2) No need for custom builds.
- With 101, import naturally, and what you use will be bundled.
- Each util method is a module that can be required require('101/<util>').
- Currently node/browserify is supported, I will add other module system support on request.
- Underscore/lodash is large, and you have to manually create custom builds when if you're trying to optimize for size.
اشتراکها