הדרך מקוד ביניים לקוד רץ¶
פונקציה קטנה יכולה לרוץ מהר יותר אחרי כמה אלפי קריאות, בלי שהקובץ על הדיסק השתנה. ההסבר אינו בהכרח cache של הנתונים: מנוע הריצה יכול לשנות את הקוד המבוצע במהלך חיי התהליך. נפריד בין IL ששמור ב־assembly, קוד שהוכן מראש וקוד שה־JIT יצר עבור מכונה וגרסת runtime מסוימות.
ניצור ניסוי שמחזיק פונקציה קטנה בגבול שנוח לצפות בו. השימוש ב־NoInlining הוא אמצעי אבחון, ולא המלצה ליישום אמיתי.
using System.Reflection;
using System.Runtime.CompilerServices;
long total = 0;
for (int run = 0; run < 200_000; run++) total += Kernel.Sum(run, 3);
Console.WriteLine(total);
MethodInfo method = typeof(Kernel).GetMethod(nameof(Kernel.Sum))!;
byte[] il = method.GetMethodBody()!.GetILAsByteArray()!;
Console.WriteLine(Convert.ToHexString(il));
Console.WriteLine(method.Module.ModuleVersionId);
static class Kernel
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static int Sum(int a, int b) => checked(a + b);
}
ה־IL מכיל הוראות של מכונה וירטואלית, ולא opcodes של x64. עבור חיבור checked נחפש הוראה שמבטאת בדיקת overflow, ולא נניח שהדפסת byte-ים היא disassembler. ה־metadata token שמופיע באופרנד של קריאה מפורש ביחס למודול שלו. אותו מספר במודול אחר יכול לייצג דבר שונה. ה־MVID עוזר לזהות את המודול שבדקנו, אך אינו חתימה קריפטוגרפית של מקור הקוד.
נוכל לבקש מן ה־JIT להדפיס את ה־assembly של הפונקציה, כאשר ה־runtime שבחרנו תומך במתגי האבחון האלה. הריצו ב־PowerShell את הפקודות הבאות בפרויקט הניסוי, ואפסו את המשתנים אחרי התצפית כדי שלא ישפיעו על ניסויים אחרים.
$env:DOTNET_JitDisasm = 'Kernel:Sum'
$env:DOTNET_TieredCompilation = '0'
dotnet run -c Release
Remove-Item Env:DOTNET_JitDisasm
Remove-Item Env:DOTNET_TieredCompilation
הביטול של tiering נותן ניסוי פשוט יותר, אבל משנה את המערכת שאנו מודדים. בניסוי שני נפעיל את ברירת המחדל ונבחן קוד ראשוני מול קוד חם, עם מספר קריאות וזמן שמאפשרים קידום. ה־tiered compilation יכול להתחיל בקוד שמטרתו עלות התחלה נמוכה, ולעבור לקוד מותאם יותר. מידע פרופיל דינמי יכול להשפיע על בחירות אופטימיזציה. אין הבטחה שכל שיטה תגיע לאותו שלב באותה קריאה.
ה־ReadyToRun מוסיף קוד שהוכן מראש כדי להפחית חלק מעבודת ה־JIT בזמן העלייה, אבל אינו מבטל בהכרח JIT לכל הפעולות. תאימות, instantiations של generics ו־tiering עדיין משנים את התמונה. לעומתו NativeAOT מפרסם אפליקציה למכונה ולמערכת יעד בלי להסתמך על JIT בזמן ריצה. הבחירה מצמצמת אפשרויות כמו יצירת קוד דינמי וטעינה דינמית של assemblies, ולכן משפיעה על הארכיטקטורה של plugin host.
ה־trimming מסיר קוד שהניתוח מזהה כבלתי נדרש. שימוש ב־reflection המבוסס על מחרוזת מקלט יכול להסתיר תלות מן הניתוח. אזהרת trimming אינה רעש שכדאי להשתיק: היא אומרת שהכלי אינו יכול להוכיח שקוד נחוץ נשמר. נעדיף תיאור מפורש של התלות, source generation או חוזה מוגבל, ונבדוק את ה־artifact שפורסם, לא רק dotnet run של הבנייה הרגילה.
במעבדת הפרסום נשתמש ב־dotnet publish עם RuntimeIdentifier מפורש, למשל win-x64, ונשווה startup, גודל ו־working set בתרחיש זהה. פרסום NativeAOT בווינדוס דורש גם כלי בנייה טבעיים מתאימים; הימצאות SDK מנוהל בלבד אינה מוכיחה שהסביבה יכולה לבצע את ה־link. נרשום בדוח מה נבנה בפועל ומה נותר ניסוי לביצוע במכונת המעבדה.
התיעוד על הגדרות קומפילציה בזמן ריצה ועל הפצה עם NativeAOT מגדיר את אפשרויות העבודה. ההשוואה שלנו תקפה לגרסה, למצב הבנייה ול־workload שנמדדו, ולא לכל קוד C# באשר הוא.