מה זה ‏.NET Native AOT — ההבדל מ-JIT ומ-trimming

· עודכן בתאריך: · · C#, .NET, Native AOT, פרסום, תכנון

כבר באיך קוראים ל-DLL של C# Native AOT מ-C/‏C++ כתבנו על שימוש ב-Native AOT כדי לקרוא מ-C/‏C++ ל-C#. אבל למען האמת, נכון היה יותר לשים לפני זה את השאלה מה זה בכלל Native AOT. הסדר התהפך קצת.

הסיפור על Native AOT הוא בעיקר סיפור שבו קל מאוד למונחים להתערבב כבר בהתחלה.

  • האם זה סיפור על ביטול JIT
  • מה ההבדל מ-self-contained ומ-single-file
  • האם זה מאותה משפחה כמו ReadyToRun
  • מה קורה שיוצאות המון אזהרות trimming
  • האם אפשר להשתמש בו באותה טמפרטורה גם ב-WPF /‏ WinForms /‏ ASP.NET Core

כשכל זה מתערבב יחד, Native AOT יכול להיראות כ’קסם שסתם מאיץ’, או ההפך — כ’משהו מלא מגבלות ומפחיד’. שני המבטים האלה קצת גסים.

אי-ההבנה שנוצרת כשהמונחים מתערבביםכשמונחים כמו JIT, self-contained, ReadyToRun ו-trimming מתערבבים יחד, Native AOT יכול להיראות כקסם שסתם מאיץ, או כמשהו מלא מגבלות ומפחיד, ושני המבטים האלה גסים למדי.המונחים מתערבבים יחדנראה כקסם שמאיץנראה מלא מגבלות ומפחידמפרידים בין המיליםהופך לבחירה עם מקום שימוש ברור

איור 1: מקור אי-ההבנה הוא בלבול המונחים. מתחילים מהפרדת המילים.

במאמר הזה, בהתבסס בעיקר על התחושה המעשית הנוכחית מ-‏.NET 8 ואילך, נסדר תחילה את ארבעת אלה.

  • מהותו של Native AOT
  • מה משמח, ואיפה זה נהיה קשה
  • מה ההבדל מ-ReadyToRun ומ-trimming
  • מאיזו אפליקציה נעים להתחיל לנסות

תוכן העניינים

  1. תחילה המסקנה (בקצרה)
  2. תחילה, טבלת סידור
    • 2.1. המילים סביב Native AOT
    • 2.2. ההבדל בין JIT /‏ ReadyToRun /‏ Native AOT
  3. התמונה הכללית של Native AOT (תרשים)
  4. מה משמח ב-Native AOT
    • 4.1. ההפעלה נוטה להיות קלה יותר
    • 4.2. לא חייבים להניח התקנה מראש של סביבת ריצה
    • 4.3. מתאים לסביבת הרצה מוגבלת
  5. מה נהיה קשה ב-Native AOT
    • 5.1. רפלקציה ויצירת קוד דינמית
    • 5.2. צריך לחשוב במונחי trimming כברירת מחדל
    • 5.3. מפרסמים לכל פלטפורמה בנפרד
    • 5.4. הקשר שולחן העבודה של Windows /‏ COM דורש זהירות רבה
  6. הצעדים המינימליים
    • 6.1. csproj
    • 6.2. publish
    • 6.3. איך כותבים JSON
  7. מקרים שמתאימים
  8. מקרים שלא מתאימים
  9. מוקשים
  10. סיכום
  11. מקורות

מפת הידע של המאמר

‏.NET Native AOT היא שיטת פרסום שמקמפלת מראש לקוד נייטיבי בזמן ה-publish בלי להשתמש ב-JIT בזמן ריצה, וקל לה לשפר את זמן ההפעלה ואת תפוסת הזיכרון, וגם קל להפיץ אותה לסביבות שבהן אין התקנה מראש של סביבת ריצה של ‏.NET. בעוד ש-ReadyToRun נוטה לכיוון שבו ה-IL נשאר וחלק מה-JIT מוקדם, Native AOT לא מניח JIT בזמן ריצה כלל, ולכן trimming הופך כמעט לברירת מחדל, וההתאמה נחלשת עם רפלקציה חופשית, יצירת קוד דינמית ו-built-in COM. ל-Native AOT של Windows אין built-in COM, ‏WPF לא מתיישב עם trimming, ו-WinForms תלוי חזק ב-built-in COM marshalling — בשתיהן תמיכת ה-trimming מושבתת בצד ה-‏.NET SDK, ולכן מומלץ להסתכל בזהירות רבה עליהן כמועמד ראשון ל-Native AOT. כשדרוש COM, תכנון מחדש סביב ComWrappers הוא אפשרות חזקה.

מפת הידע של ‏.NET Native AOTתרשים שמראה איך Native AOT שונה מ-JIT ומ-ReadyToRun, איך הוא מתנגש עם trimming, עם רפלקציה ועם built-in COM, ולמה WPF ו-WinForms דורשים זהירות רבה.אינו מתיישב עםמחייבמחייבאינו מתיישב עםאינו מתיישב עםאינו מתיישב עםמענה מומלץ לאינו מתיישב עםמחייבשימוש לא מומלץ לשימוש לא מומלץ למשתמש במצמצםמחייבמשתמש במחייבNative AOTקומפילציית JIT‏ (Just-In-Time)ReadyToRun‏trimming (גיזום)רפלקציהיצירת קוד דינמית (Reflection.Emit וכדומה)built-in COM interopComWrappers / source-generated COMWPFWindows Formssource generator‏source generation של System.Text.Jsonפרסום self-containedפרסום single-file‏.NET (מ-Core ואילך)

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 16, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

1. תחילה המסקנה (בקצרה)

  • Native AOT היא שיטה שמקמפלת מראש אפליקציית ‏.NET לקוד נייטיבי בזמן ה-publish, ומפיצה כך.
  • מכיוון שלא נעשה שימוש ב-JIT בזמן ריצה, זמן ההפעלה ותפוסת הזיכרון נוטים להשתפר, וקל יותר להפיץ גם לסביבות שבהן אין התקנה מראש של סביבת ריצה של ‏.NET.
  • עם זאת, ההתאמה נחלשת עם רפלקציה חופשית, יצירת קוד דינמית, ‏built-in COM וספריות שלא תומכות ב-trimming.
  • כלומר, זה לא קסם שמאיץ, אלא מודל פרסום שמוותר במקצת על העולם הדינמי ונוטה לעבר עולם סטטי, לטובת צרכי ההפעלה, ההפצה וסביבת הריצה.

Native AOT הוא “מנגנון להפצת ‏.NET בצורה שדומה לנייטיבי”, ולא סתם תיבת סימון להאצת קומפילציה.

מה מקבלים ומה מוותרים עליוNative AOT הוא מודל פרסום שבזכות קומפילציה מראש בזמן ה-publish מקבל זמן הפעלה, תפוסת זיכרון והפצה בלי צורך בסביבת ריצה, ובתמורה מוותר על רפלקציה חופשית, יצירת קוד דינמית וההתאמה עם built-in COM.Native AOTמה שמקבליםמה שמוותרים עליוקלות בהפעלה, בזיכרון ובהפצהרפלקציה חופשיתיצירת קוד דינמית ו-built-in COM

איור 2: לא קסם שמאיץ, אלא עסקת חליפין — מוותרים במקצת על העולם הדינמי לטובת עולם סטטי.

2. תחילה, טבלת סידור

2.1. המילים סביב Native AOT

תחילה, אם מפרידים בין אלה, ההמשך קל יותר.

מונח מה הוא עושה הקשר ל-Native AOT
JIT יוצר קוד נייטיבי מ-IL בזמן ריצה Native AOT מסיים את זה מראש
self-contained מפיץ יחד את כל חבילת ‏.NET הדרושה להרצה Native AOT נחשב מהמשפחה הזו
single-file מרכז את חומר ההפצה לקובץ אחד לא זהה למהות Native AOT, אבל המראה בפועל נוטה להידמות
trimming מוריד קוד שלא בשימוש ב-Native AOT זה כמעט תמיד ברירת מחדל
ReadyToRun משאיר את ה-IL, ומקדים במקצת חלק מעבודת ה-JIT דומה ל-Native AOT אבל שונה בבסיסו
source generator מעביר עיבוד דינמי בזמן ריצה ליצירת קוד בזמן build מתאים היטב ל-Native AOT

מה שקל להתערבב הוא ש-Native AOT הוא לא פיצ’ר אחד בודד, אלא מודל פרסום שפועל בשיתוף פעולה עם self-contained, ‏trimming, ‏source generation, ‏publish עם RID קבוע ועוד.

המנגנונים שפועלים יחד עם Native AOTNative AOT הוא לא פיצ'ר בודד, אלא מודל פרסום שפועל בשילוב עם self-contained, trimming, source generation ו-publish עם RID קבוע.Native AOT〔מודל פרסום〕מהמשפחה של self-containedtrimming כמעט תמיד ברירת מחדלהתאמה טובה עם source generatorpublish עם RID קבוע

איור 3: Native AOT הוא לא פיצ’ר עצמאי, אלא מודל פרסום שפועל יחד עם מנגנונים סובבים.

2.2. ההבדל בין JIT /‏ ReadyToRun /‏ Native AOT

גם כאן הכי מהיר לראות תחילה בטבלה אחת.

היבט הרצת JIT רגילה ReadyToRun Native AOT
JIT בזמן ריצה בשימוש עדיין יש מצבים בשימוש לא בשימוש
תוכן חומר ההפצה מתמקד ב-IL IL + קוד שנוצר מראש מתמקד בקובץ הרצה נייטיבי
הפעלה קו הבסיס נוטה להשתפר נוטה להשתפר משמעותית
התאמה הרחבה ביותר רחבה מגבלות חזקות
יכולות דינמיות נוח לשימוש נוח לשימוש ברובו הרבה מגבלות
מטרה מתאימה פיתוח ‏.NET רגיל בכללותו רוצים קודם לשפר הפעלה לוקחים בכוח הפעלה, הפצה וסביבה מוגבלת

אם ReadyToRun הוא בכיוון ‘להקל קצת על ה-JIT’, Native AOT הוא בכיוון ‘לא להניח JIT בזמן ריצה מלכתחילה’. גם אם שתיהן נושאות את אותה מילה AOT, ההבדל בטמפרטורה משמעותי.

ההבדל בכיוון בין ReadyToRun ל-Native AOTReadyToRun משאיר את ה-IL ומקדים במקצת עבודת JIT כדי להקל עליה, ואילו Native AOT לא מניח JIT בזמן ריצה מלכתחילה, וגם אם שתיהן נושאות את המילה AOT ההבדל בטמפרטורה משמעותי.להקל קצתלא להניח מלכתחילהמה רוצים לעשות עם ה-JITReadyToRun〔ה-IL נשאר〕Native AOT〔מתמקד בנייטיבי〕ההתאמה נשארת רחבהההפעלה מהירה אבל המגבלות חזקות

איור 4: גם אם שתיהן AOT, ‘להקל על ה-JIT’ ו’לא להניח JIT’ הם שני דברים שונים.

לאחר מכן, אם מרכזים לתרשים אחד את השאלה “אז איזו צורת הפצה בוחרים בסוף”, מתקבל זה:

בחירת צורת ההפצהתרשים החלטה הבוחן אם אפשר להתקין סביבת ריצה ביעד, אם חשוב לצמצם את זמן ההפעלה, ואם יש תלות ברפלקציה, ביצירת קוד דינמית או ב-COM המובנה, ומוביל משם ל-framework-dependent,‏ ReadyToRun,‏ self-contained או Native AOT.אפשרלא עד כדי כךרוצים לצמצםלא רוצים / אי אפשר להתקיןיש תלותאין תלותכןלארוצים להחליט צורת הפצהאפשר להתקין סביבת ריצה של ‏.NET ביעד ההפצה?רוצים לצמצם את זמן ההפעלה?framework-dependent‏פרסום רגילReadyToRun‏שיפור הפעלה תוך שמירה על ההתאמההאם יש תלות ברפלקציה, יצירת קוד דינמית או built-in COM?self-contained‏מרכזים ל-single-file אם צריךהיעד הוא console / ‏worker / ‏API קטן?Native AOTקודם self-contained‏לצמצם תלות דינמית ואז לבחון מחדש

איור 5: איך בוחרים צורת הפצה. מסתעפים לפי הסדר — האם אפשר להניח סביבת ריצה, והאם אפשר לצמצם מנגנונים דינמיים.

ההסתעפות הראשונה היא האם אפשר להניח סביבת ריצה ביעד ההפצה, וההסתעפות השנייה היא עד כמה אפשר לצמצם את המנגנונים הדינמיים. ‏self-contained ו-single-file הם סיפור של “איך מפיצים”, ואילו ReadyToRun ו-Native AOT הם סיפור של “מתי יוצרים את הקוד הנייטיבי”, כך שבפועל צריך לחשוב עליהם בשילוב.

3. התמונה הכללית של Native AOT (תרשים)

אם מציירים תרשים גס ל-Native AOT, זה כך:

המסלול מהקוד אל ההרצהתרשים המשווה בין המסלול הרגיל, שבו ה-IL מתקמפל ב-JIT בזמן ריצה, לבין המסלול של PublishAot, שבו ניתוח AOT ו-trim מורידים קוד מיותר ואז נוצר קובץ הרצה נייטיבי ספציפי ל-RID.הרצה רגילהdotnet publish + PublishAotקוד מקור C# / ‏.NETה-assembly של ה-ILJIT בזמן ריצההרצת האפליקציהניתוח AOT / ‏trimהורדת קוד מיותריצירת קוד נייטיביקובץ הרצה ספציפי ל-RID

איור 6: בהרצה רגילה עושים JIT בזמן ריצה, אבל Native AOT מקדים את הניתוח, ההורדה והיצירה הנייטיבית לזמן ה-publish.

ב-‏.NET רגיל, קודם יוצרים IL, ובזמן ריצה עושים JIT רק לחלק שדרוש. Native AOT מקדים חלק גדול מהשלב שאחרי זה לזמן ה-publish.

מה שחשוב כאן הוא ש-בזמן ה-publish כמעט צריך לדעת מראש את כל הקוד שיידרש בזמן ריצה. כאן משתנות ההנחות של מה מותר לכתוב.

  • מציאת טיפוס בזמן ריצה
  • יצירת קוד בזמן ריצה
  • טעינת Assembly בזמן ריצה
  • פתרון מושהה (lazy) בגישת “יסתדר איכשהו” בזמן ריצה

צורות כתיבה כאלה מתחילות פתאום להתנגש עם Native AOT.

צריך לדעת הכול כבר בזמן ה-publishב-Native AOT צריך לדעת כמעט את כל הקוד שיידרש בזמן ריצה כבר בזמן ה-publish, ולכן צורות כתיבה כמו מציאת טיפוס בזמן ריצה, יצירת קוד בזמן ריצה, טעינת Assembly בזמן ריצה ופתרון מושהה מתנגשות איתו.הקוד הדרוש נקבע בזמן ה-publishמציאת טיפוס בזמן ריצהיצירת קוד בזמן ריצהקריאת Assembly בזמן ריצהפתרון מושהה כדי להסתדרכל אלה מתנגשים עם AOT

איור 7: מהות שינוי ההנחות. ככל שהכתיבה מתבססת על ‘קביעה בזמן ריצה’, ההתנגשות עם Native AOT חזקה יותר.

4. מה משמח ב-Native AOT

4.1. ההפעלה נוטה להיות קלה יותר

ההשפעה הכי ברורה של Native AOT היא בכל זאת ההפעלה.

  • כלי CLI
  • תהליכים קצרי-חיים
  • הפעלה בסגנון serverless
  • הפעלה והחלפה של קונטיינרים
  • כלי ניטור או תהליך רקע קטן

בתחומים האלה עלות ה-JIT בולטת, ומכיוון ש-Native AOT מקדים אותה, ההפעלה הראשונה נעשית קלה יותר.

גם תפוסת הזיכרון נוטה להשתפר, כך שיש יתרון גם כשרוצים לדחוס יותר מופעים באותה כמות משאבים. בייחוד בצד ענן שבו אותו תהליך עולה בכמויות גדולות, ההבדל הזה מצטבר לאט לאט.

4.2. לא חייבים להניח התקנה מראש של סביבת ריצה

אפליקציה שפורסמה עם Native AOT קל יותר להריץ גם בסביבה שבה אין התקנה מראש של סביבת ריצה של ‏.NET.

זו נקודה שקטה אבל משמעותית.

  • לא רוצים לומר ליעד ההפצה “קודם תתקינו .NET 9 Runtime”
  • רוצים להקטין את תמונת הקונטיינר
  • רוצים למקם כלי קטן אחד ולהריץ אותו
  • לא רוצים / אי אפשר לאפשר JIT בסביבת הריצה

במצבים כאלה, עצם ההיעדר של “הנחה שצריך להכין סביבת ריצה בנפרד” משקיט את הסיפור הרבה.

“ללא צורך בסביבת ריצה” כאן פירושו שלא צריך להתקין ‏.NET בנפרד ביעד ההפצה. זה לא אומר שהחלק המקביל ל-runtime נעלם לגמרי מתוך האפליקציה.

המשמעות הנכונה של "ללא צורך בסביבת ריצה""ללא צורך בסביבת ריצה" ב-Native AOT פירושו שלא צריך להתקין .NET בנפרד ביעד ההפצה, ולא שהחלק המקביל ל-runtime נעלם לגמרי מתוך האפליקציה.המילים 'ללא צורך בסביבת ריצה'לא צריך להתקין ‏.NET ביעד ההפצההחלק המקביל ל-runtime לא נעלםפחות הנחות בהפצה ובהפעלה

איור 8: “ללא צורך בסביבת ריצה” הוא סיפור על יעד ההפצה. החלק המקביל ל-runtime כלול בתוך האפליקציה.

4.3. מתאים לסביבת הרצה מוגבלת

מכיוון ש-Native AOT לא משתמש ב-JIT בזמן ריצה, קל יותר להריץ אותו גם בסביבה שבה JIT אסור.

זה נקודה שבולטת יותר בכיוון ענן, קונטיינרים ומובייל מאשר בכיוון desktop. עם זאת, גם בהקשר של פיתוח Windows, במובן של “רוצים להקטין הנחות מיותרות ביעד ההפצה” — זו נקודה משמחת דיה.

5. מה נהיה קשה ב-Native AOT

5.1. רפלקציה ויצירת קוד דינמית

המגבלה המרכזית של Native AOT היא כאן.

  • טעינה דינמית כמו Assembly.LoadFile
  • יצירת קוד בזמן ריצה כמו System.Reflection.Emit
  • רפלקציה שעוקבת אחר טיפוסים ללא הגבלה בזמן ריצה
  • כתיבה שמרכיבה generic באופן חופשי בזמן ריצה

קשה לקבוע מראש בזמן ה-publish את הקוד הדרוש עבור אלה, ולכן הם הופכים לחממה לאזהרות AOT.

כמובן, זה לא פשוט עד כדי כך שאם יש שורת רפלקציה אחת בלבד — זה כישלון מיידי. עם זאת, המגמה ש-כתיבה שנוטה לכיוון “קובעים ורואים בזמן ריצה” נעשית קשה יותר — לא זזה.

השם שנפוץ להופיע בשמות האזהרות הוא משפחת RequiresDynamicCode. משמעותו “הקריאה הזו עלולה להישבר תחת AOT”, כך שבטוח יותר לא לדכא אותה (suppress) בקלות דעת.

כשעושים Native AOT, מועיל לחשוב על זה כניסיון להקטין את “החוכמה בזמן ריצה” ולהגדיל את “ההצהרה בזמן build”.

מ"חוכמה בזמן ריצה" ל"הצהרה בזמן build"טעינה דינמית, יצירת קוד בזמן ריצה ורפלקציה ללא הגבלה הופכים לחממה לאזהרות AOT, ולכן מכוונים את התכנון להקטין את החוכמה בזמן ריצה ולהגדיל את ההצהרה בזמן build.תכנון שנשען על חוכמה בזמן ריצהאזהרות ממשפחת RequiresDynamicCodeמגדילים הצהרה בזמן buildהקוד הדרוש נקבע בזמן ה-publishלא לדכא בקלות דעת

איור 9: הדרך היחידה להתמודד עם המגבלה המרכזית. הופכים “קובעים בזמן ריצה” ל”מצהירים בזמן build”.

5.2. צריך לחשוב במונחי trimming כברירת מחדל

Native AOT קשור בחוזקה ל-trimming. מה שקל לפספס כאן הוא ש-זה משפיע לא רק על הקוד שלכם, אלא גם על אופן הכתיבה של ספריות התלות.

הנקודות שדורשות זהירות הן בערך אלה:

  • מסדר טורי (serializer) מבוסס רפלקציה
  • מבנה DI / plugin שאוסף טיפוסים בסריקה בזמן ריצה
  • מנגנון שמוצא טיפוס לפי שם מחרוזת ומייצר אותו
  • ספרייה שנשענת על proxy דינמי או על יצירת IL

אם יש אזהרה כאן, וקובעים “ה-publish עבר אז זה בסדר” — זה יכול לגרום לצרות אמיתיות בהמשך. ב-Native AOT, לרוב כדאי לקרוא את האזהרות ברצינות.

trimming שלא נקבע רק לפי הקוד שלכםהצלחת ה-trimming תלויה לא רק באופן הכתיבה של הקוד שלכם אלא גם באופן הכתיבה של ספריות התלות, ומסדר טורי מבוסס רפלקציה, DI עם סריקה בזמן ריצה, פתרון טיפוס לפי מחרוזת וספריות עם proxy דינמי דורשים זהירות.הצלחת ה-trimmingאופן הכתיבה של הקוד שלכםאופן הכתיבה של ספריות התלותמסוג רפלקציה או סריקה בזמן ריצהקוראים את האזהרות ברצינות ומוודאים

איור 10: מה שקל לפספס הוא צד ספריות התלות. “ה-publish עבר אז זה בסדר” יכול להתגלגל לצרה בהמשך.

ובכל זאת, סתם “תקראו ברצינות” לא מספיק כדי לדעת איך לפעול, אז נכתוב את סדר הטיפול העדיף. גם התיעוד הרשמי ממליץ לנסות בסדר הזה.

סדר טיפול מתי משתמשים בו
1 להפסיק להשתמש ברפלקציה כשאפשר להחליף Activator.CreateInstance(Type) בארגומנט generic, או לעבור ל-source generator
2 להוסיף DynamicallyAccessedMembers כשרפלקציה עדיין נדרשת, אבל הטיפוס היעד ידוע כבר בזמן הקומפילציה
3 להוסיף RequiresUnreferencedCode כשקובעים שם טיפוס לפי מחרוזת בזמן ריצה וכדומה, ולא ניתן לבצע ניתוח סטטי מהותית
4 לדכא (suppress) עם UnconditionalSuppressMessage האמצעי האחרון, כשכל האמור לעיל בלתי אפשרי ואישרו שזה בטוח

הדפוס הנפוץ ביותר כצעד ראשון הוא מספר 2 — “הטיפוס ידוע, אבל בכל זאת יוצאת אזהרה”. לדוגמה, הקוד הבא מייצר IL2070.

// IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
void PrintMethodNames(Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

אם מצהירים באמצעות attribute שרוצים לשמר את השיטות ה-public כשקוראים ל-GetMethods(), האזהרה נעלמת.

using System.Diagnostics.CodeAnalysis;

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// אם הצד הקורא מעביר עם typeof, הדרישה מתמלאת אוטומטית
PrintMethodNames(typeof(DateTime));

ה-API שקוראים לו וההצהרה הדרושה בדרך כלל תואמים זה לזה. עבור GetMethod / ‏GetMethods זה PublicMethods, עבור GetProperty / ‏GetProperties זה PublicProperties, ועבור Activator.CreateInstance זה PublicParameterlessConstructor או PublicConstructors. ‏DynamicallyAccessedMemberTypes.All נוח, אבל שומר את כל חברי הטיפוס היעד וזה מנפח את הגודל, וגם החברים שנשמרו מביאים אזהרות נוספות — לכן העיקרון הוא לציין את המינימום ההכרחי.

יצוין שאם הוספת ה-attribute לא מבטלת את האזהרה, בודקים מהמקום שבו נעשה שימוש ברפלקציה חזרה עד הצד הקורא, שהתוספת קיימת לאורך כל המסלול. אם חוליה אחת בדרך חסרה, הדרישה נקטעת שם.

כשהוספת attribute לא מבטלת אזהרהמציינים ב-DynamicallyAccessedMembers את המינימום ההכרחי, וכשהאזהרה לא נעלמת, בודקים מהמקום שבו נעשה שימוש ברפלקציה חזרה עד הצד הקורא שהתוספת קיימת לאורך כל המסלול. חוליה חסרה אחת קוטעת את הדרישה שם.הוספת attribute והאזהרה עדיין נשארתבודקים חזרה עד הצד הקוראמוודאים שהתוספת קיימת לאורך כל המסלולחוליה חסרה אחת קוטעת את הדרישההעיקרון הוא לציין מינימום הכרחי

איור 11: מציינים את ה-attribute על המסלול ולא על נקודה בודדת. חוליה חסרה אחת קוטעת את הדרישה שם.

5.3. מפרסמים לכל פלטפורמה בנפרד

Native AOT מפרסם עם RID (‏Runtime Identifier) קבוע. כלומר, זה לא עולם שבו מריצים כמו שהוא ב-linux-x64 משהו שנבנה עבור win-x64.

  • Windows x64
  • Windows Arm64
  • Linux x64
  • Linux Arm64
  • macOS Arm64

וכן הלאה — ההנחה היא ליצור חומר הפצה נפרד לכל יעד.

כאן התחושה נעשית הרבה יותר “דומה לאפליקציה נייטיבית” בהשוואה ל-.NET רגיל מסוג framework-dependent.

5.4. הקשר שולחן העבודה של Windows /‏ COM דורש זהירות רבה

בהקשר של KomuraSoft, זו נקודה חשובה במיוחד.

ל-Native AOT של Windows אין built-in COM. בנוסף, ‏WPF לא מתיישב טוב עם trimming, ו-WinForms תלוי חזק ב-built-in COM marshalling, ולכן לפחות בשלב הזה, מומלץ להסתכל בזהירות רבה על שתיהן כ”מועמד ראשון ל-Native AOT”.

נכתוב כאן את הבסיס ל”בשלב הזה”. הכתוב במאמר הזה מבוסס על התיעוד הרשמי של דורות ‏.NET 8 /‏ 9 /‏ 10 (נכון ליולי 2026). לגבי WPF ו-WinForms, ב-“Known trimming incompatibilities” של Microsoft נכתב במפורש שמכיוון ש-WPF תלוי חזק ברפלקציה ובבדיקת קוד בזמן ריצה וכמעט לא פועל אחרי trimming, ומכיוון ש-WinForms תלוי חזק ב-built-in COM marshalling, בשתיהן תמיכת ה-trimming מושבתת בצד ה-‏.NET SDK. כלומר, זה לא “מומלץ להסתכל בזהירות” אלא מצב שבו ה-SDK בפועל עוצר את זה כרגע. יש אפשרות שהניסוח הזה ישתנה בעתיד, אז מומלץ לוודא באותו עמוד בזמן הקריאה.

הסיבה ש-WPF ו-WinForms נעצריםל-Native AOT של Windows אין built-in COM, WPF תלוי חזק ברפלקציה ובבדיקת קוד בזמן ריצה וכמעט לא פועל אחרי trimming, ו-WinForms תלוי חזק ב-built-in COM marshalling, ולכן בשתיהן תמיכת ה-trimming מושבתת בצד ה-.NET SDK.Native AOT של Windowsאין built-in COMWPFתלות חזקה ברפלקציהWinFormsתלות חזקה ב-COM marshallingתמיכת trimming מושבתת בצד ה-SDK

איור 12: זה לא “בזהירות”, אלא מצב שבו ה-SDK עוצר בפועל כרגע. גוף שולחן העבודה לא מועמד ראשון.

בקיצור:

  • להפוך את גוף WPF / ‏WinForms ל-Native AOT מיד
  • להביא COM interop כמו שהוא, באותה תחושה רגילה

אלה נוטים לגרום לזינוק פתאומי באזהרות ה-publish ובמגבלות זמן הריצה, וגם עלות הטיפול קופצת מהר.

ולהפך:

  • קונסולה
  • worker
  • Web API קטן
  • רכיב שגם בתוך אינטגרציה נייטיבית קל להוריד לגבול פונקציית C

אלה טבעיים יותר כנקודת כניסה.

אם דרוש COM, יש מקרים שבהם עדיף להישאר עם JIT, או לתכנן מחדש סביב ComWrappers / ‏source-generated COM.

6. הצעדים המינימליים

לפני זה, ל-publish של Native AOT דרושה שרשרת כלים נייטיבית. אם מריצים dotnet publish רק עם PublishAot, הכישלון לא קורה בקומפילציה, אלא בשלב הקישור הנייטיבי האחרון. זה המכשול הראשון, אז מומלץ להתקין מראש.

סביבה מה נדרש
Windows ‏Visual Studio 2022 ומעלה. להתקין את עומס העבודה ‘פיתוח שולחן עבודה עם C++’ עם כל הרכיבים המוגדרים כברירת מחדל
Ubuntu 18.04 ומעלה sudo apt-get install clang zlib1g-dev
Alpine 3.15 ומעלה sudo apk add clang build-base zlib-dev
Fedora 39 ומעלה / RHEL 8 ומעלה sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel
macOS ‏Xcode Command Line Tools (נתמך מ-‏.NET 8 ואילך)

בקיצור, מדובר בשרשרת כלי הקומפילציה ובחבילות הפיתוח של הספריות שסביבת הריצה של ‏.NET תלויה בהן. אם מריצים ב-CI, בדוגמאות ה-Native AOT של dotnet/samples יש Dockerfile גם ל-Linux וגם ל-Windows, ולכן הכי מהיר להביא משם את שלבי ההתקנה ההתחלתיים.

יצוין שקובץ בינארי שנבנה ב-Linux פועל רק ב-Linux זהה או חדש יותר. משהו שנבנה ב-Ubuntu 20.04 פועל מ-20.04 ואילך, אבל לא ב-18.04. בחירת סביבת ה-build הופכת ישירות לטווח ההפצה האפשרי.

המכשול לפני ה-publish וטווח ההפצה האפשריל-publish של Native AOT דרושה שרשרת כלים נייטיבית, ובלעדיה נכשל בשלב הקישור הנייטיבי האחרון. בנוסף, קובץ בינארי שנבנה ב-Linux פועל רק ב-Linux זהה או חדש יותר, ובחירת סביבת ה-build הופכת לטווח ההפצה האפשרי.לאכןהאם קיימת שרשרת כליםנכשל בשלב הקישור הנייטיבייוצא בינארי לכל RIDב-Linux פועל רק בסביבה זהה או חדשה יותרבחירת סביבת ה-build קובעת את טווח ההפצה

איור 13: המכשול הראשון הוא שרשרת הכלים. ב-Linux, ותק סביבת ה-build קובע את טווח ההפצה האפשרי.

6.1. csproj

קודם כול מכניסים PublishAot לקובץ הפרויקט.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

הדוגמה מספיקה עם net8.0. הרעיון עצמו כמעט זהה גם ב-‏.NET 9 / ‏10.

מה שחשוב הוא לא להוסיף זמנית רק בשורת הפקודה של dotnet publish, אלא להשאיר את זה קבוע בפרויקט מההתחלה, ולעקוב יומיומית אחר הניתוח בזמן ה-build / ‏publish.

יצוין שגם אם מכניסים <PublishAot>true</PublishAot>, ההרצה המקומית הרגילה לא הופכת פתאום ל-Native AOT. ‏dotnet run היומיומי וההרצה הרגילה עדיין עם JIT — מופע ה-Native AOT מתרחש בזמן ה-publish.

החיים היומיומיים אחרי הוספת PublishAotגם אם PublishAot מוגדר בפרויקט, dotnet run היומיומי נשאר עם JIT, וקומפילציית Native AOT מתרחשת בזמן ה-publish. חשוב להשאיר את ההגדרה קבועה בפרויקט ולעקוב יומיומית אחר ניתוח ה-build וה-publish.מכניסים PublishAot ל-csprojה-dotnet run היומיומי נשאר עם JITקומפילציית AOT בזמן ה-publishעוקבים יומיומית אחר ניתוח ואזהרות

איור 14: היומיום עם JIT, המופע עם publish. לכן מכניסים את ההגדרה קבוע ולא זמנית, ועוקבים אחר הניתוח.

6.2. publish

לדוגמה, עבור Windows x64, זו הצורה:

dotnet publish -c Release -r win-x64

עבור Linux x64, זה כך:

dotnet publish -c Release -r linux-x64

הפלט הוא עם RID קבוע. במקום מבט של “DLL אחד של ‏.NET שפועל בכל מקום”, המבט משתנה ל”קובץ הרצה שנבנה עבור מערכת ההפעלה / הארכיטקטורה הזו”.

אם רוצים לגעת מצד Web API, נוח יותר להתחיל מתבנית מבוססת Native AOT.

dotnet new webapiaot -o MyFirstAotWebApi

עבור worker, זו הדרך:

dotnet new worker -o WorkerWithAot --aot

6.3. איך כותבים JSON

מה שנוטים להיתקל בו לעיתים קרובות ב-Native AOT הוא JSON. ‏System.Text.Json נוטה לגלוש לכיוון רפלקציה כשמשתמשים בו בתחושה הרגילה, כך שבטוח יותר לעבור ל-source generation.

using System.Text.Json;
using System.Text.Json.Serialization;

[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}

public sealed class AppConfig
{
    public string? Name { get; init; }
    public int RetryCount { get; init; }
}

var config = new AppConfig
{
    Name = "sample",
    RetryCount = 3
};

string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);

בפועל, כדאי לזכור את זה לא כ”התאמה ל-Native AOT” אלא כמגמה של “לא לתת לו לחפש טיפוסים בזמן ריצה” — כך לא טועים בכיוון.

7. מקרים שמתאימים

Native AOT מתאים בנוחות במקרים כמו אלה:

  • כלי CLI / כלי שבו ההפעלה היא הכוכב
  • API קטנים שמפיצים בכמות גדולה בקונטיינרים
  • worker / שירות רקע
  • תהליכים חסרי שרת (serverless) או קצרי-חיים
  • רכיב ‏.NET קטן שמוחדר לתוך אפליקציה נייטיבית
  • מצב שבו לא רוצים לדרוש התקנה מראש של סביבת ריצה של ‏.NET בסביבת ההרצה

המשותף הוא ש-הגבול ברור יחסית, וקל להקטין את המנגנונים הדינמיים.

8. מקרים שלא מתאימים

ולהפך, יש גם מקרים שבהם ברור שלא כדאי לעשות מ-Native AOT את הזירה הראשית כבר מההתחלה.

  • גוף אפליקציה גדולה קיימת של WPF / ‏WinForms
  • מבנה שמניח built-in COM interop
  • אפליקציה שבה טעינת plugin בזמן ריצה היא הכוכב
  • מבנה עם תלות חזקה במסגרת עבודה שמחפשת טיפוסים דרך רפלקציה
  • ספרייה שמשתמשת כדבר מובן מאליו ב-System.Reflection.Emit או ב-proxy דינמי
  • תכנון שמשלב C++/CLI

במקרים כאלה עדיף .NET רגיל מבוסס JIT, או ReadyToRun, או שבחינה מחדש של חלוקת התכנון.

9. מוקשים

לסיום, סיכום של נקודות שקל להיתקל בהן בצעד הראשון עם Native AOT.

  • להתייחס בקלות ראש לאזהרות ה-publish
    • כאמור, אזהרות Native AOT הן יעד שצריך לקרוא ברצינות.
  • ה-build עובר, אבל ה-publish נשבר
    • בזמן ה-publish רץ ניתוח רציני שכולל גם את ספריות התלות, ולכן יש דברים שנראים לראשונה רק כאן.
  • להתייחס ל-ReadyToRun ול-Native AOT באותה תחושה
    • המילים דומות, אבל חוזק המגבלות שונה מאוד.
  • להתחיל ישר מגוף אפליקציית desktop
    • נעים יותר להתחיל מ-console / ‏worker / ‏API קטן.
  • לכתוב JSON וקישור הגדרות בגישה הרגילה
    • כתיבה שמניחה רפלקציה תתפוס בהמשך.
  • להפיץ מתוך כוונה שזה בלתי תלוי בפלטפורמה
    • חומר ההפצה של Native AOT הוא עם RID קבוע.
  • לחשוב ש”Native AOT = הכול מואץ”
    • הכוכבים הם ההפעלה, ההפצה וסביבת הריצה. אם מפספסים את זה, הציפיות מתעוותות.

ב-Native AOT, מה שקובע בסוף אם אפשר או לא הוא לא dotnet build אלא dotnet publish. אם מתחילים להריץ את זה מוקדם, פחות סביר להיתקע בהמשך.

מי שקובע אפשר/לא-אפשר הוא ה-publishזה שה-build עובר אבל ה-publish נשבר קורה כי בזמן ה-publish רץ ניתוח רציני שכולל גם את ספריות התלות, ולכן מי שקובע בסוף אם אפשר או לא ב-Native AOT הוא לא dotnet build אלא dotnet publish.dotnet build עוברהאפשרות עוד לא נקבעהdotnet publishרץ ניתוח רציני שכולל תלויותרק כאן מתבררת לראשונה האפשרותלכן מריצים publish מוקדם

איור 15: המשותף למוקשים. לא להסתפק ב”ה-build עבר” — מריצים publish מוקדם.

10. סיכום

אם אומרים את Native AOT במשפט אחד, זה מנגנון שמזיז את אפליקציית ‏.NET ממודל הרצה דינמי למודל הפצה שקל יותר לקבוע באופן סטטי.

הנקודות ששווה לזכור מסתכמות בחמש אלה.

  1. Native AOT מקמפל מראש לקוד נייטיבי בזמן ה-publish
  2. משפיע היטב על צרכי ההפעלה, הזיכרון וההפצה
  3. בתמורה, קשה עם רפלקציה, יצירת קוד דינמית, built-in COM וקוד שלא תומך ב-trimming
  4. היעד הראשון נעים יותר כ-console / ‏worker / ‏API קטן מאשר כגוף desktop
  5. חשוב לבטל אזהרות תוך כדי, ולוודא מוקדם ולעיתים קרובות מבוסס publish

Native AOT הוא לא מתג סטנדרטי שמצרפים לכל אפליקציית ‏.NET. אבל במצב שבו ההפעלה חשובה, רוצים להקל את ההפצה, ורוצים להקטין הנחות בסביבת הריצה — זה כלי חזק למדי.

ולהפך, בעולם הצפוף של WPF / ‏WinForms / ‏COM, יש עדיין מקרים רבים שבהם ‏.NET רגיל עדיף. כשמבחינים בין אלה, Native AOT הופך מ”פיצ’ר חדש ומסובך” לבחירה עם מקום שימוש ברור.

11. מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מה זה Native AOT?
שיטת פרסום שמקמפלת מראש אפליקציית ‏.NET לקוד נייטיבי בזמן ה-publish, ומפיצה אותה כך. מכיוון שלא נעשה שימוש ב-JIT בזמן ריצה, זמן ההפעלה ותפוסת הזיכרון נוטים להשתפר, וקל יותר להפיץ גם לסביבות שבהן אין התקנה מראש של סביבת ריצה של ‏.NET. בתמורה, ההתאמה נחלשת עם רפלקציה חופשית, יצירת קוד דינמית, ‏built-in COM וספריות שלא תומכות ב-trimming. זו לא קסם שמאיץ הכול, אלא מודל פרסום שמוותר במקצת על העולם הדינמי ונוטה לעבר עולם סטטי, לטובת צרכי ההפעלה, ההפצה וסביבת הריצה.
מה ההבדל בין Native AOT ל-ReadyToRun?
‏ReadyToRun הוא שיטה שמשאירה את ה-IL ומקדימה במקצת חלק מעבודת ה-JIT, ועדיין יש מצבים שבהם נעשה שימוש ב-JIT בזמן ריצה. ההתאמה רחבה, וגם היכולות הדינמיות נשארות בדרך כלל נוחות לשימוש. לעומת זאת, Native AOT לא מניח מלכתחילה JIT בזמן ריצה, החומר המופץ מתמקד בקובץ הרצה נייטיבי, וההפעלה נוטה להשתפר משמעותית בתמורה למגבלות חזקות יותר. גם אם שתיהן נושאות את המילה AOT, ‏ReadyToRun נוטה לכיוון 'להקל קצת על ה-JIT', ואילו Native AOT נוטה לכיוון 'לא להניח JIT מלכתחילה' — ההבדל בטמפרטורה משמעותי.
האם אפשר להשתמש ב-Native AOT באפליקציות WPF או WinForms?
בשלב הזה מומלץ להסתכל על זה בזהירות רבה. ל-Native AOT של Windows אין built-in COM, ‏WPF לא מתיישב טוב עם trimming, ו-WinForms תלוי חזק ב-built-in COM marshalling, ולכן שתיהן לא מתאימות כמועמד ראשון ל-Native AOT. אם דרוש COM, יש מקרים שבהם עדיף להישאר עם JIT, או לתכנן מחדש סביב ComWrappers / source-generated COM. כנקודת כניסה, קונסולה, worker או Web API קטן טבעיים יותר.
לאילו אפליקציות Native AOT מתאים?
מתאים לכלי CLI שההפעלה שלהם היא הכוכב, ל-API קטנים שמפיצים בכמות גדולה בקונטיינרים, ל-worker או שירות רקע, לתהליכים חסרי שרת (serverless) או קצרי-חיים, לרכיב .NET קטן שמוחדר לתוך אפליקציה נייטיבית, ולמצב שבו לא רוצים לדרוש התקנה מראש של סביבת ריצה של ‏.NET בסביבת ההרצה. המשותף הוא שהגבול ברור יחסית וקל להקטין את המנגנונים הדינמיים. ולהפך, זה לא מתאים לאפליקציה שהכוכב שלה הוא טעינת plugin בזמן ריצה, או למבנה עם תלות חזקה במסגרת עבודה שמחפשת טיפוסים דרך רפלקציה.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג