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

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

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 13 Mar 2026)
פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173432)

מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.

Go Komura (2026). מה זה .NET Native AOT — ההבדל מ-JIT ומ-trimming. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173432 https://comcomponent.com/he/blog/dotnet-native-aot-what-is/

DOI (הגרסה האחרונה)
10.5281/zenodo.22173432
DOI (הגרסה הזו)
10.5281/zenodo.22173433

כבר באיך קוראים ל-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. לא חייבים להניח התקנה מראש של runtime
    • 4.3. מתאים לסביבת הרצה מוגבלת
  5. מה נהיה קשה ב-Native AOT
    • 5.1. reflection ויצירת קוד דינמית
    • 5.2. צריך לחשוב במונחי trimming כברירת מחדל
    • 5.3. מפרסמים לכל פלטפורמה בנפרד
    • 5.4. הקשר desktop של Windows / COM דורש זהירות רבה
  6. הצעדים המינימליים
    • 6.1. csproj
    • 6.2. publish
    • 6.3. איך כותבים JSON
  7. מקרים שמתאימים
  8. מקרים שלא מתאימים
  9. מוקשים
  10. סיכום
  11. מקורות

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 16, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

1. קודם כל, המסקנות

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

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

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

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

2. טבלת מונחים

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

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

מונח מה הוא עושה הקשר ל-Native AOT
JIT יוצר קוד native מ-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 הוא לא פיצ’ר אחד בודד, אלא מודל publish שפועל בשיתוף פעולה עם self-contained, trimming, source generation, publish עם RID קבוע ועוד.

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

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

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

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

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

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

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

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

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

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

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

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

3. הסקירה של Native AOT (תרשים)

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

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

איור 6: בהרצה רגילה עושים JIT בזמן ריצה, אבל Native AOT מקדים את הניתוח, ההורדה והיצירה ה-native לזמן ה-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
  • processes קצרי-חיים
  • הפעלה בסגנון serverless
  • הפעלה והחלפה של containers
  • כלי ניטור או process רקע קטן

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

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

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

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

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

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

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

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

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

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

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

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

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

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

5.1. reflection ויצירת קוד דינמית

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

סדר טיפול מתי משתמשים בו
1 להפסיק להשתמש ב-reflection כשאפשר להחליף Activator.CreateInstance(Type) בארגומנט generic, או לעבור ל-source generator
2 להוסיף DynamicallyAccessedMembers כש-reflection עדיין נדרשת, אבל הטיפוס היעד ידוע כבר בזמן הקומפילציה
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 לא מבטלת את האזהרה, בודקים מהמקום שבו נעשה שימוש ב-reflection חזרה עד הצד הקורא, שהתוספת קיימת לאורך כל המסלול. אם חוליה אחת בדרך חסרה, הדרישה נקטעת שם.

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

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

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

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

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

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

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

5.4. הקשר desktop של 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 תלוי חזק ב-reflection ובבדיקת קוד בזמן ריצה וכמעט לא פועל אחרי trimming, ומכיוון ש-WinForms תלוי חזק ב-built-in COM marshalling, בשתיהן תמיכת ה-trimming מושבתת בצד ה-.NET SDK. כלומר, זה לא “מומלץ להסתכל בזהירות” אלא מצב שבו ה-SDK בפועל עוצר את זה כרגע. יש אפשרות שהניסוח הזה ישתנה בעתיד, אז מומלץ לוודא באותו עמוד בזמן הקריאה.

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

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

בקיצור:

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

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

ולהפך:

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

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

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

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

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

סביבה מה נדרש
Windows Visual Studio 2022 ומעלה. להתקין את עומס העבודה “Desktop development with 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 ואילך)

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

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

המכשול לפני ה-publish וטווח ההפצה האפשריל-publish של Native AOT דרושה שרשרת כלים native, ובלעדיה נכשל בשלב הקישור ה-native האחרון. בנוסף, קובץ בינארי שנבנה ב-Linux פועל רק ב-Linux זהה או חדש יותר, ובחירת סביבת ה-build הופכת לטווח ההפצה האפשרי.לאכןהאם קיימת שרשרת כליםנכשל בשלב הקישור ה-nativeיוצא בינארי לכל 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 נוטה לגלוש לכיוון reflection כשמשתמשים בו בתחושה הרגילה, כך שבטוח יותר לעבור ל-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 קטנים שמפיצים בכמות גדולה ב-containers
  • worker / שירות רקע
  • processes serverless או קצרי-חיים
  • רכיב .NET קטן שמוחדר לתוך אפליקציה native
  • מצב שבו לא רוצים לדרוש התקנה מראש של runtime של .NET בסביבת ההרצה

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

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

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

  • גוף אפליקציה גדולה קיימת של WPF / WinForms
  • מבנה שמניח built-in COM interop
  • אפליקציה שבה טעינת plugin בזמן ריצה היא העיקר
  • מבנה עם תלות חזקה ב-framework שמחפש טיפוסים דרך reflection
  • ספרייה שמשתמשת כדבר מובן מאליו ב-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 וקישור הגדרות בגישה הרגילה
    • כתיבה שמניחה reflection תתפוס בהמשך.
  • להפיץ מתוך כוונה שזה בלתי תלוי בפלטפורמה
    • חומר ההפצה של 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 מקמפל מראש לקוד native בזמן ה-publish
  2. משפיע היטב על צרכי ההפעלה, הזיכרון וההפצה
  3. בתמורה, קשה עם reflection, יצירת קוד דינמית, built-in COM וקוד שלא תומך ב-trimming
  4. היעד הראשון נעים יותר כ-console / worker / API קטן מאשר כגוף desktop
  5. חשוב לבטל אזהרות תוך כדי, ולוודא מוקדם ולעיתים קרובות מבוסס publish

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

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

11. מקורות

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

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

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

שאלות נפוצות

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

מה זה Native AOT?
שיטת publish שמקמפלת מראש אפליקציית .NET לקוד native בזמן ה-publish, ומפיצה אותה כך. מכיוון שלא נעשה שימוש ב-JIT בזמן ריצה, זמן ההפעלה ותפוסת הזיכרון נוטים להשתפר, וקל יותר להפיץ גם לסביבות שבהן אין התקנה מראש של runtime של .NET. בתמורה, ההתאמה נחלשת עם reflection חופשית, יצירת קוד דינמית, built-in COM וספריות שלא תומכות ב-trimming. זו לא קסם שמאיץ הכול, אלא מודל publish שמוותר במקצת על העולם הדינמי ונוטה לעבר עולם סטטי, לטובת צרכי ההפעלה, ההפצה וסביבת הריצה.
מה ההבדל בין Native AOT ל-ReadyToRun?
ReadyToRun הוא שיטה שמשאירה את ה-IL ומקדימה במקצת חלק מעבודת ה-JIT, ועדיין יש מצבים שבהם נעשה שימוש ב-JIT בזמן ריצה. ההתאמה רחבה, וגם היכולות הדינמיות נשארות בדרך כלל נוחות לשימוש. לעומת זאת, Native AOT לא מניח מלכתחילה JIT בזמן ריצה, החומר המופץ מתמקד בקובץ הרצה native, וההפעלה נוטה להשתפר משמעותית בתמורה למגבלות חזקות יותר. גם אם שתיהן נושאות את המילה 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 קטנים שמפיצים בכמות גדולה ב-containers, ל-worker או שירות רקע, ל-processes serverless או קצרי-חיים, לרכיב .NET קטן שמוחדר לתוך אפליקציה native, ולמצב שבו לא רוצים לדרוש התקנה מראש של runtime של .NET בסביבת ההרצה. המשותף הוא שהגבול ברור יחסית וקל להקטין את המנגנונים הדינמיים. ולהפך, זה לא מתאים לאפליקציה שהעיקר שלה הוא טעינת plugin בזמן ריצה, או למבנה עם תלות חזקה ב-framework שמחפש טיפוסים דרך reflection.

פרופיל הכותב

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

Go Komura

מנהל KomuraSoft LLC

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

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

חזרה לבלוג