המחיר של הנקודה

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

קוד ב-Atmel Studio
קוד ב-Atmel Studio (סתם בשביל להוסיף קצת צבע לפוסט – אל תנסו לקרוא את זה…)

לאחרונה התחלתי לעבוד על ספריה לתקשורת סריאלית עבור 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 בייטים, ואם יש חישובים מרובים, הקומפיילר בוודאי ימחזר קוד ויצמצם מאד את העלויות הנלוות. עם זאת, ברור שכאשר יש מגבלות זיכרון או מהירות במערכת, כדאי לעשות כל מאמץ לעבוד עם מספרים שלמים בלבד.

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

אם אתה רוצה לחלק בקבוע, ניתן להשתמש בחילוק בעזרת "מספרי קסם" שהוא יותר יעיל מחילוק במשתנה.
האתר הזה מסביר את זה דיי טוב: 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 דרך תקשורת סריאלית. במקרה שלי הכפלתי את הערך ב-100 והעברתי אותו כ-byte (כמובן לאחר בדיקה שטווח הערכים מתאים).

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