DCSIMG
Write More Debuggable Code - Liran Chen's Blog

Liran Chen's Blog

.Net Internals, Debugging, Multithreading - and More!

Write More Debuggable Code

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

למה הכוונה? הנה דוגמה קלאסית:

static void Main()

{

if (calculate(2, 4) > 4)

Console.WriteLine("Greater");

}

static double calculate(double x, double y)

{

return Math.Pow(y, x);

}


במבט מהיר אפשר להבין כבר מה הקוד הזה עושה, אין שום בעיה עם הקריאות שלו.
אבל, מה יקרה כשנרצה לדבג אותו? נניח ואנו רוצים לדעת מהו ערך ההחזר של calculate? כדי לדעת בדיוק, נהיה חייבים לקרוא לה שנית דרך פאנל ה-Immediate, אבל מן הסתם זה לא פתרון נוח כל כך, ולא פרקטי אם מדובר בפונקציה מורכבת יותר.
הצורה הכי פשוטה ונכונה למנוע מצב כזה מגיעה משני כיוונים:
  • לא לקרוא לפונקציות בתוך משפטים מורכבים (קריאות לפונקציה, משפטי if, הכרזות על for ... למעשה כל מה שכולל בתוכו סוגריים).
  • לא לבצע חישוב מתמטי/קריאה לפונקציה בתוך קריאה ל-return.
בשני המקרים, כל קריאה לפונקציה/חישוב צריכה להיעשת לפני השימוש בערך ההחזר שלה. צריך ליצור משתנה "מאוד זמני" שיחזיק אותו ובו אפשר להשתמש בתוך הקריאה לפונקציה/return. בצורה הזאת, כשנדבג את הקוד, נוכל לבדוק את ערך ההחזר על ידי מבט מהיר ב-Watch.
 
מבחינת ביצועים? ההבדל זניח, אם בכלל קיים. הביטוי היחיד שלו יהיה בתוספת של מספר הוראות IL שתפקידן להעלות ולהוריד ערכים מה-Evaluation Stack. בפועל, לתוספת הקוד הזאת אין משמעות. גם אם תריצו את אותה הפונקציה, פעם בגרסה עם משתנים זמנים ופעם בגרסה בלי, גם אחרי עשרות מיליוני קריאות, לא תראו הבדל כלשהו בביצועים. זה יכול לנבוע מ-2 סיבות. האחת: ההבדל כל כך (אבל כל כך!) זניח עד שכדי שבאמת נראה הבדל "כלשהו" נצטרך לבצע כמה מילארדי קריאות לפונקציות, ורק אז, אולי, נוכל לקבל סט נתונים שיצביע על איזשהו הבדל בין 2 הגרסאות של הפונקציה. הסיבה השניה היא שלמרות שה-IL שחולל כאן שונה, יתכן וה-JIT מבצע אופטימיזציות נוספת על אותו הקוד ולמעשה מבטל את התוספת שקיימת בקוד ה-IL. כך שלמעשה, בפועל, אנחנו מריצים את אותו הקוד בשני המקרים.

תוכן התגובה

Alex Golesh כתב/ה:

Hi,

With all respect I have to disagree with you. The fact, that tool like reflector shows you the same code in original case and in case you creating the temp variables doesn't mean that compiler produced equal IL. Actually, the IL is different, and it is exactly like the code you created. I do agree, that in simple case like your sample the impact is less than zero, but in real case it does matters. Think about reference type variables which you are not creating...

IL produced from sample without local vars:

.method private hidebysig static float64

       calculate(float64 x,

                 float64 y) cil managed

{

 // Code size       13 (0xd)

 .maxstack  2

 .locals init ([0] float64 CS$1$0000)

 IL_0000:  nop

 IL_0001:  ldarg.1

 IL_0002:  ldarg.0

 IL_0003:  call       float64 [mscorlib]System.Math::Pow(float64,

                                                         float64)

 IL_0008:  stloc.0

 IL_0009:  br.s       IL_000b

 IL_000b:  ldloc.0

 IL_000c:  ret

} // end of method Program::calculate

IL from code with local vars:

.method private hidebysig static float64

       calculate(float64 x,

                 float64 y) cil managed

{

 // Code size       15 (0xf)

 .maxstack  2

 .locals init ([0] float64 temp,

          [1] float64 CS$1$0000)

 IL_0000:  nop

 IL_0001:  ldarg.1

 IL_0002:  ldarg.0

 IL_0003:  call       float64 [mscorlib]System.Math::Pow(float64,

                                                         float64)

 IL_0008:  stloc.0

 IL_0009:  ldloc.0

 IL_000a:  stloc.1

 IL_000b:  br.s       IL_000d

 IL_000d:  ldloc.1

 IL_000e:  ret

} // end of method Program::calculate

See the difference? When you will do the same trick with reference type the diff will be even more obvious...

Take care,

Alex

# May 22, 2009 4:30 AM

spiritus asper כתב/ה:

אכן, קוד ה-IL שיחולל בסופו של דבר יהיה מעט שונה. כמה זה "מעט שונה"? עוד משחק קטן עם ה-evaluation stack. אבל במציאות, אין משמעות לשורת הקוד וחצי הזאת. גם במידה ומדובר ב-ref type, אז ההשפעה כאן תהיה זהה (למעשה זהה לעבודה עם int, הרי אתה מאתחל אך ורק את המצביע, ולא מקצה מחדש את אותו אובייקט פעמים).

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

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

התירוץ של "חסכתי 2 שורות ב-IL ... לא קונה אותי :-)

# May 22, 2009 11:11 AM

spiritus asper כתב/ה:

דבר נוסף, הוא ויתכן שאותו קוד IL כבר עובר אופטימיזציות על ידי ה-JIT שגורמות בסופו של דבר לקוד להיות זהה לזה שעובד ללא משתנים זמניים.

אני יכול רק להגיד שאחרי 100 מיליון קריאות לפונקציית calculate על 2 גרסאותיה, לא ראיתי שום הבדל מבחינת הביצועים. מה שרק מוסיף ומראה על חוסר המשמעות ואי הכדאיות של "החסכון" הזה.

# May 22, 2009 11:42 AM
שלח תגובה

(שדה חובה)  

(שדה חובה)  

(אופציונלי)

(שדה חובה) 

Please add 8 and 7 and type the answer here:


Enter the numbers above: