ביצועים ב-.NET Core

שלום לכולם! מאמר זה הוא אוסף של שיטות עבודה מומלצות שאני ועמיתיי משתמשים בהן במשך זמן רב תוך כדי עבודה על פרויקטים שונים.
מידע על המכונה עליה בוצעו החישובים:BenchmarkDotNet=גרסה 0.11.5, מערכת הפעלה=Windows 10. 0.18362
מעבד Intel Core i5-8250U במהירות 1.60GHz (Kaby Lake R), מעבד אחד, 1 ליבות לוגיות ו-8 ליבות פיזיות
ערכת פיתוח תוכנה של .NET Core=3.0.100
[מארח]: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), RyuJIT 64 סיביות
ליבה: .NET Core 2.2.7 (CoreCLR 4.6.28008.02, CoreFX 4.6.28008.03), RyuJIT 64 סיביות
[מארח]: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), RyuJIT 64 סיביות
ליבה: .NET Core 3.0.0 (CoreCLR 4.700.19.46205, CoreFX 4.700.19.46214), RyuJIT 64 סיביות
עבודה=ליבה זמן ריצה=ליבה
ToList לעומת ToArray ו-Cycles
תכננתי להכין את המידע הזה עם יציאת .NET Core 3.0, אבל הקדימתי את זה, אני לא רוצה לגנוב את התהילה של מישהו אחר ולהעתיק מידע של מישהו אחר, אז אני רק אציין את זה. .
בשמי, אני רק רוצה להציג בפניכם את המדידות והתוצאות שלי; הוספתי להם מחזורים הפוכים למי שאוהב את "סגנון ++C" של כתיבת מחזורים.
קוד:
public class Bench
{
private List<int> _list;
private int[] _array;
[Params(100000, 10000000)] public int N;
[GlobalSetup]
public void Setup()
{
const int MIN = 1;
const int MAX = 10;
Random random = new Random();
_list = Enumerable.Repeat(0, N).Select(i => random.Next(MIN, MAX)).ToList();
_array = _list.ToArray();
}
[Benchmark]
public int ForList()
{
int total = 0;
for (int i = 0; i < _list.Count; i++)
{
total += _list[i];
}
return total;
}
[Benchmark]
public int ForListFromEnd()
{
int total = 0;t
for (int i = _list.Count-1; i > 0; i--)
{
total += _list[i];
}
return total;
}
[Benchmark]
public int ForeachList()
{
int total = 0;
foreach (int i in _list)
{
total += i;
}
return total;
}
[Benchmark]
public int ForeachArray()
{
int total = 0;
foreach (int i in _array)
{
total += i;
}
return total;
}
[Benchmark]
public int ForArray()
{
int total = 0;
for (int i = 0; i < _array.Length; i++)
{
total += _array[i];
}
return total;
}
[Benchmark]
public int ForArrayFromEnd()
{
int total = 0;
for (int i = _array.Length-1; i > 0; i--)
{
total += _array[i];
}
return total;
}
}
הביצועים ב-.NET Core 2.2 ו-3.0 כמעט זהים. הנה מה שהצלחתי להשיג ב-.NET Core 3.0:


ניתן להסיק כי עיבוד מחזורי של אוסף מערכים מהיר יותר הודות לאופטימיזציות הפנימיות שלו ולהקצאה מפורשת של גודל האוסף. כמו כן, ראוי לזכור כי לאוסף רשימה יש יתרונות משלו ועליכם להשתמש באוסף הנכון בהתאם לחישובים הנדרשים. גם אם אתם כותבים את הלוגיקה לעבודה עם מחזורים, אל תשכחו שמדובר בלולאה רגילה והיא כפופה גם לאופטימיזציה אפשרית של מחזורים. מאמר פורסם ב-habr לפני זמן מה: זה עדיין רלוונטי ומומלץ לקרוא.
לזרוק
לפני שנה עבדתי על פרויקט Legacy בחברה, בפרויקט הזה היה מקובל לטפל באימות שדה באמצעות קונסטרוקציה של try-catch-throw. כבר אז הבנתי שזו היגיון עסקי לא בריא לפרויקט, אז ניסיתי לא להשתמש במבנה כזה ככל האפשר. אבל בואו נבין למה הגישה של טיפול בשגיאות עם קונסטרוקציה כזו היא גרועה. כתבתי קוד קטן כדי להשוות בין שתי הגישות ולקחתי "בנצ'מרק" לכל אפשרות.
קוד:
public bool ContainsHash()
{
bool result = false;
foreach (var file in _files)
{
var extension = Path.GetExtension(file);
if (_hash.Contains(extension))
result = true;
}
return result;
}
public bool ContainsHashTryCatch()
{
bool result = false;
try
{
foreach (var file in _files)
{
var extension = Path.GetExtension(file);
if (_hash.Contains(extension))
result = true;
}
if(!result)
throw new Exception("false");
}
catch (Exception e)
{
result = false;
}
return result;
}התוצאות ב-.NET Core 3.0 וב-Core 2.2 דומות (.NET Core 3.0):


הפונקציה Try catch מקשה על הבנת הקוד ומגדילה את זמן הביצוע של התוכנית. אבל אם אתם זקוקים למבנה הזה, אל תוסיפו שורות קוד שלא צפויות לטפל בשגיאות - זה יקל על הבנת הקוד. למעשה, לא טיפול בחריגים הוא זה שטוען את המערכת, אלא זריקת השגיאות עצמן דרך המבנה throw new Exception.
זריקת חריגים איטית יותר מאשר זריקת מחלקה כלשהי שתאסוף את השגיאה בפורמט הנכון. אם אתה מעבד טופס או נתונים ואתה יודע בבירור מה אמורה להיות השגיאה, למה לא לטפל בה?
אסור לכתוב את המבנה throw new Exception() אלא אם כן המצב חריג. טיפול וזריקת חריג זה יקר מאוד!!!
למטה, לאינבריאנט למטה, למעלה, לאינבריאנט למעלה
במהלך 5 שנות הניסיון שלי בעבודה על פלטפורמת .NET, נתקלתי בפרויקטים רבים שהשתמשו בהשוואת מחרוזות. ראיתי גם את התמונה הבאה: היה פתרון ארגוני אחד עם פרויקטים רבים, שכל אחד מהם ביצע השוואת מחרוזות בצורה שונה. אבל מה צריך להשתמש בו וכיצד לאחד את זה? בספרו של ריכטר CLR דרך C#, קראתי מידע לפיו שיטת ToUpperInvariant() עובדת מהר יותר משיטת ToLowerInvariant().
קטע מהספר:

כמובן, לא האמנתי לזה והחלטתי לערוך כמה בדיקות על .NET Framework באותה תקופה והתוצאה הדהימה אותי - עלייה של יותר מ-15% בפריון. לאחר מכן, כשהגעתי לעבודה למחרת בבוקר, הראיתי את המדידות הללו לבוסים שלי ונתתי להם גישה לקוד המקור. לאחר מכן, 2 מתוך 14 פרויקטים שונו כדי להתאים למידות החדשות, ובהתחשב בכך ששני הפרויקטים הללו נועדו לעבד טבלאות אקסל ענקיות, התוצאה הייתה משמעותית ביותר עבור המוצר.
אני גם מציג לכם מדידות עבור גרסאות שונות של .NET Core, כך שכל אחד מכם יוכל לבחור את הפתרון האופטימלי ביותר. ואני רק רוצה להוסיף שבחברה בה אני עובד, אנחנו משתמשים ב-ToUpper() להשוואת מחרוזות.
קוד:
public const string defaultString = "VXTDuob5YhummuDq1PPXOHE4PbrRjYfBjcHdFs8UcKSAHOCGievbUItWhU3ovCmRALgdZUG1CB0sQ4iMj8Z1ZfkML2owvfkOKxBCoFUAN4VLd4I8ietmlsS5PtdQEn6zEgy1uCVZXiXuubd0xM5ONVZBqDu6nOVq1GQloEjeRN8jXrj0MVUexB9aIECs7caKGddpuut3";
[Benchmark]
public bool ToLower()
{
return defaultString.ToLower() == defaultString.ToLower();
}
[Benchmark]
public bool ToLowerInvariant()
{
return defaultString.ToLowerInvariant() == defaultString.ToLowerInvariant();
}
[Benchmark]
public bool ToUpper()
{
return defaultString.ToUpper() == defaultString.ToUpper();
}
[Benchmark]
public bool ToUpperInvariant()
{
return defaultString.ToUpperInvariant() == defaultString.ToUpperInvariant();
}


ב-.NET Core 3.0, הרווח עבור כל אחת מהשיטות הללו הוא ~x2 ומאזן את המימושים בינם לבין עצמם.


אוסף שכבות
במאמר הקודם שלי, תיארתי בקצרה פונקציונליות זו, ברצוני לתקן ולהשלים את דבריי. קומפילציה מדורגת מאיצה את זמן ההפעלה של הפתרון שלך, אך אתה מקריב את העובדה שחלקים מהקוד שלך יקומפלו לגרסה אופטימלית יותר ברקע, מה שיכול להוביל לעלויות תקורה קטנות. עם הופעת NET Core 3.0, זמן הבנייה של פרויקטים עם קומפילציה מדורגת הופחת ובאגים הקשורים לטכנולוגיה זו תוקנו. בעבר, טכנולוגיה זו הובילה לשגיאות בבקשות הראשונות ב-ASP.NET Core ולקיפאון במהלך הבנייה הראשונה במצב קומפילציה מדורגת. כרגע, ב-.NET Core 3.0, היא מופעלת כברירת מחדל, אך תוכל להשבית אותה אם תרצה. אם אתה ראש צוות, בכיר, בדרג ביניים או מנהל מחלקה, עליך להבין שפיתוח פרויקטים מהיר מגדיל את ערך הצוות וטכנולוגיה זו תאפשר לך לחסוך הן את זמן המפתחים והן את זמן פיתוח הפרויקט עצמו.
עלייה ברמה של .NET
שדרגו את גרסת .NET Framework / .NET Core שלכם. לעתים קרובות, כל גרסה חדשה מספקת שיפורי ביצועים נוספים ומוסיפה תכונות חדשות.
אבל מהם בדיוק היתרונות? בואו נבחן כמה מהם:
- .NET Core 3.0 מציג תמונות R2R, אשר יסייעו בהפחתת זמן ההפעלה של יישומי .NET Core.
- מאז גרסה 2.2, הופיע Tier Compilation, שבזכותו מתכנתים יבלו פחות זמן בהשקת פרויקט.
- תמיכה בתקני .NET חדשים.
- תמיכה בגרסה החדשה של שפת התכנות.
- אופטימיזציה, עם כל גרסה חדשה, משתפרת האופטימיזציה של ספריות הבסיס Collection/Struct/Stream/String/Regex ועוד הרבה יותר. אם תעברו מ-.NET Framework ל-.NET Core, תקבלו שיפור משמעותי בביצועים כבר מהקופסה. כדוגמה, אני מצרף קישור לכמה מהאופטימיזציות שנוספו ל-.NET Core 3.0:

מסקנה
כשכותבים קוד, כדאי לשים לב להיבטים שונים של הפרויקט שלכם ולהשתמש בתכונות של שפת התכנות והפלטפורמה שלכם כדי להשיג את התוצאה הטובה ביותר. אשמח אם תשתפו את הידע שלכם בנוגע לאופטימיזציה ב-.NET.
מקור: www.habr.com
