DCSIMG
DateTime.Now Causes Boxing - Liran Chen's Blog

Liran Chen's Blog

.Net Internals, Debugging, Multithreading - and More!

DateTime.Now Causes Boxing

אולי לא הייתם מיודעים לכך, אבל בכל פעם שאתם פונים ל-DateTime.Now, אתם גורמים בעקיפין הקצאת זכרון דינאמית על ידי Boxing של Int32.
הסיבה לכך טמונה עמוק בתוך המימוש של Now, שאם נסתכל מקרוב, נוכל לראות שהוא למעשה עוטף קריאה ל-UtcNow והמרתו לזמן מקומי על ידי קריאה לפונקציה ToLocalTime.
עם הקריאה ל-UtcNow אין בעיה, מאחר ובסך הכל נעשית קריאה פנימית ל-GetSystemTimeAsFileTime, שמחזיר את הזמן הנוכחי בפורמט UTC.
הבעיה האמיתית טמונה במימוש של ToLocalTime, או למעשה במחלקה CurrentSystemTimeZone בה היא נעזרת. כחלק מרוטינת ההמרה, מפעילים את הפונקציה GetDaylightChanges שגורמת ל-Boxing עצמו. לא מאמינים? תראו בעצמכם:

public override DaylightTime GetDaylightChanges(int year)
{

object key = year;

if (!this.
m_CachedDaylightChanges.Contains(key))
{
// ..lots of code

}

return (
DaylightTime) this.m_CachedDaylightChanges[key];
 
זאת דוגמה טובה לכך שגם בגרסאות האחרונות של הפריימוורק, עדיין לא עודכנו קטעי קוד שמשתמשים במבני נתונים מגרסאות 1.0/1.1, לפני שהוצגו היכולות הגנריות בדוט-נט (במקרה הזה, m_CachedDaylightChanges הוא למעשה Hashtable שיודע לעבוד רק עם Object'ים).
המשמעות היא שהדוגמה הזאת עם DateTime.Now היא רק טיפה אחת בים, ואני לא אתפלא אם מבצעים עוד אינספור פעולות Boxing/Unboxing בלי שאנחנו בכלל מודעים לכך בתוך ה-BCL. עם זאת, מה שהופך את המקרה עם DateTime.Now לחמור במיוחד, הוא שמדובר במאפיין שיכולים לקרוא לו בתדירות גבוהה במיוחד (למשל, השארת חותמת זמן בכתיבה ללוג), כך שלכל הקצאות הזכרון הדינאמיות האלה יש מחיר, והוא לא זול.

לצורך האילוסטרציה, נשתמש בבנצ'מרק הבא:

while (true)

{

    Stopwatch sw = Stopwatch.StartNew();

 

    for (int i = 0; i < 1000000; i++)

    {

        DateTime now = DateTime.Now;

    }

 

    Console.WriteLine(sw.ElapsedMilliseconds);

}


מהרצה של הקוד הזה, אפשר ללמוד 2 דברים. הראשון, הוא שלוקח לנו בממוצע 456ms להשלים כל לולאה (נתייחס לזה בהמשך). והשני, הוא שניתן לקבל סדר גודל על השפעת הקריאות ל-Now על כלל ההקצאות הדינאמיות (שכביכול בכלל לא אמורות להתקיים בדוגמה הזאת). כך שאם נשתמש ב-Performance Counter כדי לנתר את הקצאות הזכרון, נקבל את התוצאה "המפתיעה" הזאת:



אם כן, למרות שכל ההקצאות האלה ישוחררו על ידי ה-GC במסגרת Generation 0, עדיין אי אפשר לקחת אותם יותר מדי בקלות, במיוחד אם אתם בונים אפליקציה בה אתם מנסים לצמצם ככל האפשר הקצאות דינאמיות, למען מתן שיפור בביצועים ובתגובתיות התוכנית (על ידי חסכון של Collection'ים מיותרים).

איך להמנע מ-Boxing?
אחרי שראינו למה לא באמת כדאי לקרוא ל-DateTime.Now, עולה השאלה איך אפשר להמנע מההקצאות היקרות והמיותרת האלה.
מהצד של מיקרוסופט, הם רק צריכים לעדכן את הקוד ולהחליף את השימוש ב-Hashtable ל-Dictionary. ברגע שהם יעשו את זה, נוכל לחזור להשתמש ב-Now בלי לחשוש שאנחנו מפגיזים את ה-Heap בהקצאות זכרון לא נגמרות.
עד שזה יקרה, אנחנו צריכים לקחת את העיניים לידיים שלנו, ולהשתמש בתחליף ל-DateTime.Now. מה שאני מציע לעשות הוא פשוט לכתוב פונקציה שכמעט מקבילה במימוש ל-UtcNow (שכזכור פונה ל-Win32 בשביל לקבל את הזמן). כך שכל מה שאנחנו צריכים לעשות זה לפנות ל-GetLocalTime שתחזיר לנו את הזמן בצורה של SYSTEMTIME, ואותו רק נצטרך להמיר ל-DateTime שיוחזר למשתמש.
בקוד זה נראה כך:

public static class TimeUtil

{

    [DllImport("kernel32.dll")]

    static extern void GetLocalTime(out SYSTEMTIME time);

 

    [StructLayout(LayoutKind.Sequential)]

    private struct SYSTEMTIME

    {

        public ushort Year;

        public ushort Month;

        public ushort DayOfWeek;

        public ushort Day;

        public ushort Hour;

        public ushort Minute;

        public ushort Second;

        public ushort Milliseconds;

    }

 

    public static DateTime LocalTime

    {

        get

        {

            SYSTEMTIME nativeTime;

            GetLocalTime(out nativeTime);

 

            return new DateTime(nativeTime.Year, nativeTime.Month, nativeTime.Day,

                    nativeTime.Hour, nativeTime.Minute, nativeTime.Second,

                    nativeTime.Milliseconds, DateTimeKind.Local);

        }

    }

}


עכשיו לצורך העניין, נשווה את ביצועי הקוד הזה מול השימוש ב-Now. אז לאחר הרצת הבנצ'מרק ממקודם, אפשר לראות שנפטרנו לגמרי מההקצאות הדינאמיות המטרידות. ודבר שני, אנחנו עכשיו מקבלים תוצאה ממוצעת של 370ms. כלומר, שיפור של 18% בהשוואה לשימוש ב-Now.

תוכן התגובה

Shlomo כתב/ה:

תודה

# August 29, 2009 9:18 PM

itai כתב/ה:

יפה מאוד..

# August 30, 2009 8:51 AM
שלח תגובה

(שדה חובה)  

(שדה חובה)  

(אופציונלי)

(שדה חובה) 

Please add 5 and 6 and type the answer here:


Enter the numbers above: