כידוע, שימוש במשתנים בעלי נקודה עשרונית (floating-point) אינו מומלץ במיקרו-בקרים טיפוסיים, מכיוון שכל פעולה שנעשית איתם גוזלת זמן רב בהרבה מפעולה מקבילה על מספרים שלמים (integer) . מה שקצת פחות ידוע הוא שיש לעבודה איתם גם מחיר כבד בדרישות הזיכרון. הנה דוגמה מעשית קטנה.
לאחרונה התחלתי לעבוד על ספריה לתקשורת סריאלית עבור ATtiny85. בעבר כתבתי קוד לשידור סריאלי ב-9600 באוד, ורציתי להוסיף לזה יכולת קליטה של שידורים מבחוץ. על העקרון של הקליטה אדבר במקום אחר. מה שחשוב לענייננו כאן הוא שהחלטתי לשכלל את הספריה כך שתהיה גמישה ותתמוך גם בקצבי שידור שונים.
בקוד השידור הישן, כוונון הטיימר נעשה ידנית מראש. חישבתי במחשבון כמה זה 16MHz (קצב השעון שבחרתי למיקרו-בקר) חלקי 9600 (קצב השידור שנבחר). מכיוון שהתוצאה גדולה מדי בשביל מונה הטיימר שגודלו בייט אחד בלבד, חישבתי ידנית גם מה יהיו הערכים האופטימליים של prescaler לטיימר (שמקטין את הקצב שלו ביחס ידוע) ושל ערך עליון לספירה. מצאתי ש-prescaler של 8 וערך עליון של 208 נותנים את השגיאה המינימלית האפשרית.
אבל מה יקרה אם ארצה לעבוד דווקא ב-8MHz וב-14400 באוד? או, מסיבה מטורפת כלשהי, 2.5MHz ו-794 באוד? האם אצטרך לבצע חישוב ידני מחדש לכל יישום? לא עדיף לתת למיקרו-בקר את המספרים הגולמיים ושיחשב את הפרמטרים האופטימליים בעצמו?
הקוד שעושה את זה יצא קצר יחסית אבל קצת מורכב, ועוד לא בדקתי אותו ביסודיות הראויה כך שלא ניכנס לפרטיו בינתיים. אחד השלבים שבו הוא חישוב – עבור קצב שעון, ערך prescaler של טיימר וקצב שידור ידועים – מהו מספר ה"תקתוקים" של הטיימר שיהיה הכי קרוב לאורך השידור הרצוי של ביט יחיד. ניקח את הדוגמה מלמעלה, ונקבל:
Ticks = 16000000 / 8 / 9600 = 208.33333…
מכיוון שהטיימר עצמו לא מסוגל כמובן לקבל ערכים עם נקודה עשרונית, אנחנו צריכים לעגל את התוצאה. הנה שורת קוד שמבצעת את החישוב עם נקודה עשרונית ומעגלת את התוצאה לערך השלם הקרוב ביותר:
ticks = round((float)CPUFreq / prescaler / baud);
קימפלתי תוכנית ניסוי קטנה ב-Atmel Studio 6.1 – שוב, הספריה עוד לא הושלמה כך שאלה לא מספרים סופיים – וקיבלתי את המספרים הבאים: 10 בייטים זיכרון RAM, ו-1634 בייטים זיכרון תוכנית (Flash) שהם כמעט 20% מהנפח הזמין בטייני.
אם נעשה את אותו החישוב עם מספרים שלמים בלבד, ככה:
ticks = CPUFreq / prescaler / baud;
נקבל תוצאה מקוצצת (truncated) במקום מעוגלת, וזו לא בהכרח התשובה האופטימלית. בדיקת האופציה של עיגול כלפי מעלה בלי להשתמש במספרים עם נקודה עשרונית מחייבת שורות קוד נוספות של חישובים ובדיקות. אף על פי כן, אחרי קימפול, נשארתי עם 10 בייטים RAM ו-1032 בייטים ב-Flash, שהם 12.6% מהזיכרון הזמין. בהערכה גסה, ההפרש של 602 בייטים יספיק בהחלט כדי לאחסן את כל מה שעוד נשאר לי לכתוב בספריה הזו.
כמובן, לא כל חישוב עם float בקוד יעלה בדיוק 602 בייטים, ואם יש חישובים מרובים, הקומפיילר בוודאי ימחזר קוד ויצמצם מאד את העלויות הנלוות. עם זאת, ברור שכאשר יש מגבלות זיכרון או מהירות במערכת, כדאי לעשות כל מאמץ לעבוד עם מספרים שלמים בלבד.
אם אתה רוצה לחלק בקבוע, ניתן להשתמש בחילוק בעזרת "מספרי קסם" שהוא יותר יעיל מחילוק במשתנה.
האתר הזה מסביר את זה דיי טוב: http://ridiculousfish.com/blog/posts/labor-of-division-episode-i.html
על הטריק הזה עוד לא שמעתי. באמת יכול להיות יעיל כשמחלקים בקבועים (חוץ מחזקות של שתיים כמובן…), אבל חובה לבדוק קודם איך הקומפיילר הספציפי מתייחס לפעולות כאלה על הפלטפורמה הספציפית.
מעניין, נתקלתי בזה כשכתבתי תוכנית לATtiny13 שיש לו רק 1K פלאש, כמה שלא ניסיתי לצמצם את הקוד שלי לא יכלתי לרדת מתחת ל1K, עד שהורדתי את השימוש בfloat, חישבתי מתח, אז עברתי לעבוד במיליוולטים במקום וולטים. מה שיותר מעניין, מה יקרה אם במקום להשתמש בfloat תעבור לint, ובשביל לעגל את המספר תשתמש בmodulo כדי להסיק מסקנה מהשארית ולבדוק אם צריך לעגל כלפי מעלה או לא. משהו כזה : int round_division(int a, int b){ int whole = a/b; int mod = a%b; if(mod >= (b/2)) { whole+=1; } return whole; } טרם בדקתי נכונות, צריך לבדוק אם צריך להתיחס למנה שלא… לקרוא עוד »
כן, זו אחת הדרכים הפשוטות לעגל תוצאת חילוק בלי להשתמש ב-float. כמובן, עם עוד קצת מאמץ אפשר לוותר גם על פעולת המודולו 😉
תודה על המאמר! יצא לי לפגוש את הנושא דווקא במקום אחר, כשרציתי להעביר ערך float דרך תקשורת סריאלית. במקרה שלי הכפלתי את הערך ב-100 והעברתי אותו כ-byte (כמובן לאחר בדיקה שטווח הערכים מתאים).
אכן, שיטה מקובלת לייצג ערכים מדויקים יחסית בעזרת מספרים שלמים היא להכפיל אותם בקבוע משמעותי, ולבצע את החילוק רק ברגע האחרון ממש. אם בוחרים קבוע שהוא חזקה של 2, החילוק מתבצע כפעולה פשוטה ומהירה מאד של הזזת ביטים.
אני מעריך שהתוספת לקוד נובעת מהכללת הפונקציות שמשמשות לטיפול ב-float פעם אחת, כלומר, גם אם היו לך מאה שורות כאלו, ההפרש היה נשאר זהה. לא בטוח שזה חוסך לך כל כך הרבה לעומת שגיאות שמאוד תתקשה למצוא בקוד מורכב
לעניין ה"מחיר" של פעולות מרובות התייחסתי בפסקה האחרונה. העלות הנוספת מבחינת זיכרון כנראה תהיה מזערית, אבל העלות בזמן עיבוד תצטבר ועוד איך… 🙂
מעבר לזה – נכון, כל בחירה שאנחנו עושים בקוד צריכה להיבחן בהקשר הספציפי, ולפעמים הכי נכון דווקא לעבוד עם float. מצד שני, התחום שנקרא integer math הוא ותיק ומבוסס מאד, אנחנו לא ממציאים כאן את הגלגל ולכן הפוטנציאל לשגיאות לא אמור להיות גדול במיוחד.