יום שישי 29 מאי 2009

מזל טוב, עברנו

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

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

אתם מוזמנים לבוא ולחנוך את הבית החדש .. רק אל תשכחו להביא איזו מתנת חנוכת בית קטנה, משהו סמלי. לא יותר.
http://blogs.microsoft.co.il/blogs/spiritus_asper

יום שישי 15 מאי 2009

Check This Out

שאלה: האם לתוכניות שרצות בדוט-נט יש בדיקה אוטומטית עבור Arithmetic Overflows?
קחו לדוגמה את התוכנית הבאה, מה לדעתכם יהיה הפלט? או שבכלל תזרק שגיאה?

class Program

{

static void Main(string[] args)

{

int foo = 1;

int bar = int.MaxValue + foo;

Console.WriteLine(foo);

}

}


אם הניחוש שלכם היה 2147483648-, צדקתם.
זאת טעות נפוצה לחשוב שכברירת מחדל הסביבה תבדוק כל הזמן האם קיימת אי התאמה בין גודל הערך שאותו אנחנו מנסים להכניס למשתנה. רוב המפתחים מצפים שעל פעולה כמו בדוגמה למעלה תזרק אוטומטית שגיאה. אבל, לא.
הסיבה שאנחנו מקבלים את הערך "המוזר" הזה, הוא שהתוצאה של החישוב שלנו חורגת מהגודל ש-int יכול להכיל. כלומר, בסביבה של 32 ביט, גודלו של int הוא 4 בתים. לכן, המספר החיובי הכי גדול שהוא יכול להכיל הוא 2147483647 (הכוונה כאן היא כאמור ל-signed int). ברגע שנרצה להחזיק ערך גדול יותר, נהיה חייב להקצות עוד בתים. במקרה שלנו נצטרך להשתמש ב-long (שמחזיק 8 בתים).
אז מה שקורה בפועל זה שהבתים הנוספים שאמורים להשתמש בהם כדי להחזיק את הערך החדש, למעשה "מקוצצים" (truncated), וכך אנחנו מקבלים ערך חדש ומזובל (הביט האחרון ב-signed types מייצג את סימון המינוס, ומכאן המספר השלילי).

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

איך ניתן להתמודד עם הבעיה?
בדיקות Arithmetic Overflows בדוט נט זקוקות להוראות ספציפיות דרך ה-CIL. כלומר, זהו לא תפקידו של ה-CLR לפקח כל הזמן על כל פעולה קטנה שאנחנו עושים ולבדוק האם אנחנו גולשים בטעות מהגודל המותר. במקום זה, בכל פעם שאנחנו רוצים לבצע בדיקת Overflow הקוד המקופל חייב להכיל הוראה "תבדוק מה הולך כאן". רק במצב כזה תבוצע בדיקה על הערך ובמידה וה-CLR רואה שהולכת לקרות כאן גלישה, הוא יזרוק שגיאת OverflowException.
כדי לבצע את בדיקת החוקיות הזאת, אפשר לנקוט באחת משתי דרכים. האחת, שימוש במילת המפתח Checked כדי לתחום אזורים בקוד בהם אנחנו מעוניינים לבצע בדיקות Overflow. השניה, עדכון סוויץ' שעובר לקומפיילר בזמן קימפול הפרוייקט ולמעשה גורם לו לבצע בדיקת Overflow בכל מקום בקוד. כברירת מחדל האפשרות הזאת מבוטלת, אפשר לאפשר אותה דרך אפשרויות הפרוייקט, תפריט Build, דרך Advanced ואז לסמן את האפשרות Check for arithmetic overflow/underflow.


לכל בדיקות ה-Overflows האלו יכולה להיות השפעה לרעה על ביצועי התוכנית שלכם. לכן צריך לחשוב פעמיים לפני שהולכים ומפעילים את הבדיקות אוטומטית על כל הפרוייקט. לכן אפשר גם לשקול לאפשר את הבדיקות המלאות רק בתצורת ה-Debug של התוכנית, אבל לא ב-Release. אבל במצב כזה שוב פעם חזרנו לבעיה ש-Overflows יכולים להתקיים בלי שנדע מזה בצורה ברורה.
כאן נכנסת לתמונה מילת המפתח checked. אנחנו יכולים לתחום בעזרתה קטעי קוד "שמועדים לפורענות". מקומות שאנחנו מזהים ויודעים להגיד ששם קיימת סבירות .. סבירה, שיגרם Overflow. בצורה הזאת אנחנו נמנעים מלעשות בדיקות על מקומות חסרי טעם שסתם יבזבזו Cycle'ים של המעבד.
הנה מה שקורה לאותו הקוד ממקודם, לאחר שתחמנו את הפעולה הלא חוקית ב-checked:


רק לרקורד, קיימת גם מילת המפתח unchecked שעושה בדיוק ההפך. במידה והגדרתם שבדיקות Overflow יבוצעו אוטומטית עבור הפרוייקט, תוכלו לתחום אזורי קוד בהם אתם לא רוצים שהבדיקה תבוצע (יכול להיות שימוש למשל אם דורסים את GetHashCode ולא רוצים שה-XOR'ים יזרקו שגיאה).

נקודות נוספות
  • בדיקות Overflow מבוצעת אך ורק למספרים אינטגרלים. כלומר, אם למשל תנסו להכניס ערך גדול מדי ל-int - תקבלו שגיאה. אבל, אם תנסו להכניס ערך גדול מדי ל-double, לא תקבלו שום שגיאה, אלא רק ערכים שגויים.
  • לכל הבדיקות האלה קיימת השלכה מסויימת על הביצועים. כדאי לחשוב היטב איפה ומתי אנחנו באמת רוצים לעשות את הבדיקה הזאת, אם בכלל.
  • הקומפיילר ידע להזהיר אתכם על Overflows במידה והם ממש Hardcoded ו-"נראים לעין". זאת הסיבה שבדוגמה שנתנתי הייתי צריך להקצות את foo שיחזיק את הערך "1". במידה וממש הייתי רושם בקוד int32.MaxValue + 1 הקומפיילר היה מוציא שגיאת קומפילציה.
  • במידה ואתם כותבים קוד שאתם יודעים מראש שיכול לגרום ל-Overflow במידה והוא מקבל כקלט ערכים "גדולים מדי", אז אפשרות נוספות היא לעשות קצת Defensive Programming ולבדוק את חוקיות הקלט שמגיע אליכם. במידה והוא לא חוקי, תזרקו כבר שגיאה משלכם שתתריעו על הבעיה.
  • קידוד נעים :)

שבת 28 מרץ 2009

Writing Conditional Code

לא פעם, אנחנו כמפתחים, נתקלים במצבים בהם אנו צריכים לכתוב קוד שיעבוד "לפעמים". הכוונה היא לא למשפטי if, אלא לקוד שירוץ רק על פי מצבים שהגדרנו מראש. למשל, קוד שיעבוד רק כשהתוכנית תרוץ במצב Debug, או אולי פונקציה שתופעל רק כשהתוכנית מוגדרת לרוץ עם MY_FOO_SYMBOL.
הצורה "הקלאסית" להשיג את זה, היא להגדיר בעצמנו בלוקים של define/#if# לפני כל קטע קוד שאנו רוצים להפוך אותו ל-"Conditional". אנחנו למעשה אומרים ישירות ל Preprocessor שאנו מעוניינים לבצע את קטע הקוד הבא רק במידה ומוגדר Symbol כלשהו. צורת העבודה הזאת בעייתית כי היא מסרבלת לא מעט את הקוד. עכשיו אנו נצטרך לכתוב 2 שורות קוד בכל פעם שנרצה להציב תנאי לפני קטע קוד מסויים. ועוד עם הגדרות העימוד ב-VS.Net הקוד הזה רק יראה יותר מסורבל ולא קריא. מה גם שבמקרה הזה האחרויות היא על המשתמש. כלומר, אם כתבנו פונקציית שמיועדת לדיבאג בלבד, כל אדם שישתמש בה יצטרך לזכור להציב אותה תחת מרחב של #if. לפעמים ... גם האחריות הזאת יכולה להיות גדולה מדי.

אבל, יש אלטרנטיבה.
במקום לכתוב קוד מסורבל ולעבוד מול ה-Preprocessor, אנו יכולים להשתמש ב- ConditionalAttribute. את ה-attribute הזה אנו יכולים להעניק עבור פונקציות ומחלקות (הקאטץ': צריך לרשת מהמחלקה Attribute). בעת ההוספה שלו, כל מה שעלינו לעשות הוא לומר לו "מתי" אנחנו רוצים שהפונקציה תקרא. כלומר, אנו צריכים להעביר לו את ה-Symbol'ים המתאימים.
אם למשל נרצה שפונקציה כלשהיא תקרא אך ורק כאשר התוכנית קומפלה במצב Debug, נצטרך להוסיף את ה- ConditionalAttribute ולהעביר לו כפרמטר את המחרוזת "DEBUG". וזהו, סיימנו. בלי לכתוב פקודה אחת ל-Preprocessor. מה שיפה כאן זה שאין איזשהיא השפעה על הביצועים מאחר וה-attribute למעשה עושה את העבודה מול ה-preprocessor במקומנו. ולאחר קימפול הקוד פשוט נקבל קוד ש"קורא" או "לא קורא" לפונקציה.
כאמור, המשמעות של זה שלפונקציה שאנו כותבים יש הגבלות על צורת ההשפעה שלה על הסביבה בה קוראים לה. למשל, לפונקציה אסור להחזיר ערך (אלא רק void).

בצורה הזאת למשל מימשו את המחלקה Debug (תחת System.Diagnostics). פשוט הוסיפו לפונקציות שלה את ה-ConditionalAttribute עם ה-Symbol של DEBUG, ובצורה הזאת למשל כל פעם שאתה קוראים ל Debug.Assert, אפשר להיות בטוחים שהקוד הזה יבוצע אך ורק כשהתוכנית מקומפלת במצב Debug (ולמעשה, הקריאות לפונקציה בכלל לא יהיו קיימות כשה-symbol לא מוגדר. כך ששוב, אין פגיעה בביצועים כשהתוכנית "ב-production").

יום חמישי 26 פברואר 2009

Debugging Multithreaded Code

כל מי שאי פעם ניסה לדבג קוד שרץ תחת כמה ת'רדים בו זמנית, נתקל כנראה בתופעה המציקה שברגע שמבצעים Step Inside וכו', בדיוק כשאתם באמצע הדיבאגינג .. הדיבאגר לוקח אתכם בחזרה 200 שורות קוד לאחור, בפרוייקט אחר בכלל, חותך את חוט המחשבה שלכם בדרך, ובעצם מחזיר אתכם בחזרה ל breakpoint המקורי שהגדרתם. למה? כי פתאום ת'רד אחר בכלל הגיע לאותו ה breakpoint.

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

בסיטואציות כאלה, אני עובד בצורה מעט שונה.
ברגע שאני נכנס ל breakpoint, כל מה שאני עושה זה לגשת לחלון ה Threads, ומסמן את כל הת'רדים הקיימים, מלבד הת'רד שאני נמצא בו כרגע (מסומן ע"י החץ הצהוב). לאחר מכן כל מה שנשאר הוא להקליק על הלחצן הימני ולהעביר את כל הת'רדים הנבחרים למצב Freeze. וסיימנו.
כעת, ניתן להמשיך לדבג את התוכנית כאילו היינו מדבגים ת'רד אחד בלבד. היתרון הנוסף הוא שאנחנו יודעים שאף אחד מהת'רדים האחרים לא עובד בזמן שאנחנו מדבגים, כך שאין לנו מה לדאוג שעצם זה שאנחנו מבזבזים זמן בלדבג קוד, שאר החלקים בתוכנית ישפעו מכך (או לפחות מצמצמים את ההשפעה במידה ועובדים מול מספר תהליכים נפרדים).
הדבר היחיד שצריך לזכור, הוא לפני שמסיימים לדבג ורוצים להמשיך להריץ את התוכנית (ע"ע F5), אנחנו צריכים לזכור להעיר את כל הת'רדים. נעשה זאת בדיוק כמו שהקפאנו אותם, רק שבמקום Freeze נקליק על Thaw. כעת, ניתן ללחוץ על F5 ולהמשיך בריצה כרגיל.

יום ראשון 4 ינואר 2009

Coding Horror

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

m_fooDictionary[m_fooList[p_fooIndex].BarTypeEnum][oldState]--;


מילא... מילא וזה היה חד פעמי. אולי "התפלק" איכשהו, וקרה. ניחא. אבל, השורה הזאת חוזרת על עצמה מספר לא מבוטל של פעמים בקובץ (שעולה על 1500 שורות קוד ... גם כן נהדר), שבכל פעם יש הבדל מינורי, פעם אחת היא מסתיימת ב --, פעם אחרת ב ++ ... בקיצור, תאווה לעיניים.

יום שני 29 דצמבר 2008

Give Meaningful Names to Locking Objects

הרבה פעם קורה שרוצים להפוך טיפוס מסויים ל-Thread Safe, ובשביל לעשות זאת יוצרים data member חדש מסוג object, כך שבכל פעם שרוצים לשנות את ה-state של הטיפוס, זוכרים קודם לבצע נעילה על ה-object וכך להשיג סנכרון מספק.
אבל, אחד ההרגלים הרעים שכדאי להיפטר מהם הוא נתינת השם "m_locker" עבור כל מופע של אובייקט כזה. מה לעשות שמתוך הרגל הרבה אנשים נוטים להעניק את השם הגנרי הזה אוטומטית עבור כל אובייקט "נעילה" שיוצרים בקוד.
ברוב הזמן זה אפילו יהיה בסדר ולא תרגישו בכלל בהבדל אם תקראו לו m_locker או m_myClassLocker, אבל ברגע שתצוף בעיה כלשהיא הקשורה לניהול הת'רדים בתוכנית, כפילות השמות הזאת יכולה לעשות את העבודה להרבה פחות נוחה.
לדוגמה, רק לפני מספר ימים מצאתי את עצמי מדבג deadlock בקוד שנעשו בו נעילות על ימין ועל שמאל, כשכולם תפסו משתנה בשם m_locker. בזמן שאני עובר על ה-stack trace'ים של מספר ת'רדים תקועים אני שם לב שכמה ת'רדים שונים הצליחו לנעול את m_locker. לקח לי לא פחות מרבע שעה (שכלל אטרקציות ב-native, וניתוח מעברים בין appDomain'ים ..) רק להבין שמדובר בשני מופעים ששייכים למחלקות שונות. בסך הכל שני מופעים נפרדים של object ש"במקרה" קוראים לשניהם m_locker.
הלקח? עדיף להיות מעט יותר יצירתיים, ולתת שם מעט יותר משמעותי מ "m_locker", כל דבר שיקשר אותו מעט לקונטקסט בו משתמשים בו יכול לעשות את החיים שלכם להרבה יותר קלים ממה שאתם חושבים.

שבת 6 דצמבר 2008

Measuring Code Execution Time

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

אז איך אפשר למדוד את הזמן הזה? התשובה האינסטנקטיבית הראשונה של מרב המפתחים היא המחלקה Stopwatch שמציעה מנגנון למדידת זמנים. למרות זאת, שימוש ב Stopwatch במקרה הזה הוא לאו דווקא נכון.
הבעיה בשימוש בה נובעת מכך שהיא מודדת זמני שעון לוקאלי, ולא באמת כמה זמן המעבד העביר בביצוע הפקודות של הפונקציה שלנו. כלומר, אם היה איזשהו context switch לת'רד אחר (שבכלל נמצא בתוכנית אחרת...), או שחל בדיוק באמצע המדידה GC, או שבכלל הת'רד שלנו חיכה לסיגנל מת'רד אחר ובכלל לא ביזבז זמן עיבוד ... על הזמן הזה ימדד על ידי ה Stopwatch.
כלומר גם אם נכתוב Thread.Sleep(5000), הוא יחזיר לנו תשובה שעברו 5 שניות (למרות שבפועל הת'רד לא בזבז שניה מהמעבד).

אז מה הדרך הנכונה? ניתן לגשת לפונקצייה GetThreadTimes הנמצאת ב Win32API. שימוש בה יחזיר לנו את ה-kernel time וה-user time של הת'רד עליו הפעלנו את הפונקציה. מכאן נוכל לקבל פרמטר הרבה יותר מדוייק ונכון לצרכים שלנו.
למתעניינים, אפשר לעיין במאמר שפרסמתי לאחרונה באתר The Code Project
http://www.codeproject.com/KB/dotnet/ExecutionStopwatch.aspx
רק שימו לב שדוגמאות הקוד מיועדות להדגים את הרעיון הכללי בלבד, ואפשר לממש את המחלקה שמובאת שם בצורה הרבה יותר נורמלית.