ה-Seed האסור: תעלומה אקראית

בתוך אחת מהפונקציות הצנועות של ארדואינו מסתתר if לא הגיוני. מה הוא עושה שם? מה יקרה (אם בכלל) אם ניפטר ממנו? הבדיקה של הנושא תיקח אותנו למסע בזמן עד שנת 1988.

בין ההכרזה על הרכישה של יצרנית המיקרו-בקרים Atmel על ידי חברת Dialog (מהלך שאף אחד לא הצליח עדיין להבין את המשמעות שלו), לבין ההכרזה על גרסה 7 של סביבת הפיתוח Atmel Studio, ניצלתי כמה שניות פנויות כדי לבדוק את קוד המקור של פונקציות ארדואינו נוספות שאני מתעתד להכניס לוויקי. אחת מהן היא randomSeed, והקוד הקצרצר שלה הוא:

void randomSeed(unsigned long seed)
{
  if (seed != 0) {
    srandom(seed);
  }
}

קל לראות שזוהי בעצם קליפה דקה לפונקציה srandom, שעושה את הפעולה החשובה באמת – אתחול של מחולל המספרים הפסודו-אקראיים לפי הערך של seed. הדבר הנוסף היחיד הוא הווידוא ש-seed שונה מאפס, וזה נראה לי חשוד. מה הבעיה עם 0? לא היתה שום הערה שתסביר את העניין, וגם בתיעוד הרשמי של הפונקציה לא נאמר על כך כלום. מה שמטריד במיוחד הוא לא עצם קיומה של בדיקה מיותרת, אלא ההשלכות של הבדיקה הזו: כאשר משתמש "נאיבי" שולח ל-randomSeed את הערך 0, הוא מצפה שיקרה משהו ספציפי – והמשהו הזה פשוט לא יקרה.

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

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

איך יודעים אם 0 באמת בעייתי ל-srandom? יצאתי לחפש את קוד המקור של הפונקציה, ומצאתי אותו בקובץ random.c של AVR-Libc, אחד מהמרכיבים של קומפיילר הארדואינו – ושל Atmel Studio, אפרופו. לא ניכנס כאן לקוד עצמו – מי שרוצה יכול לראות אותו בקישור. נגיד רק שכן, 0 הוא אכן ערך בעייתי – ושהפונקציה srandom בעצמה כבר מטפלת בו, באמצעות החלפתו בערך הקבוע 123459876! לא ברור למה מי שכתב את הקוד לארדואינו לא שם לב לזה, או חשב שהבדיקה ב-randomSeed נחוצה בכל זאת – בינתיים נראה שהיא מיותרת לחלוטין. בקרוב אדווח על כך למפתחים, ונראה מה יהיה.

עוד נושא אחד שעלה בזמן הבדיקה הוא אלגוריתם החישוב הספציפי. אפשר לומר די בביטחון שלפחות 99% מהפרויקטים שנעשו ב-AVR ושהשתמשו במספרים אקראיים, השתמשו באלגוריתם הזה. מאיפה הוא בא? מי בדק אותו? ובכן, לשמחתנו, הכותבים של AVR-Libc עשו שיעורי בית וגם היו נדיבים יותר בהערות: הם טרחו לציין את המקור המדויק של האלגוריתם שלהם – המאמר "Random number generators: good ones are hard to find" מכתב העת Communications of the ACM, גליון אוקטובר 1988. עוד יותר לשמחתנו, מישהו טרח והעלה את המאמר לרשת.

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

להרשמה
הודע לי על
5 תגובות
מהכי חדשה
מהכי ישנה לפי הצבעות
Inline Feedbacks
הראה את כל התגובות

בהסבר שלך על seed, עדיף להשתמש במילה ״התחלתי״ במקום ראשוני, למספר ראשוני במטמטיקה יש משמעות אחרת

זאת הזדמנות להודות על הבלוג הנפלא, שמעשיר לי את הfeed

הכל נכון חוץ מהפסקה האחרונה 🙂
PRNG מהסוג הזה נקראים Linear congruential generator והם עדיין בשימוש במרבית הקומפיילרים.
את יכול לראות את הקבועים שכל קומפיילר משתמש בהם בויקיפדיה:
https://en.wikipedia.org/wiki/Linear_congruential_generator

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

אם הנושא מעניין אותך, אני ממליץ בחום על ההרצאה הזו של הפיראט:
https://channel9.msdn.com/Events/GoingNative/2013/rand-Considered-Harmful