העלות הסמויה של struct

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

מימין לאט, משמאל מהר – לפרטים, קיראו למטה

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

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

עשיתי כמה חישובים זריזים על הנייר: 12 לדים, כפול נניח 16 רמות בהירות לכל אחד, וכדי למנוע הבהוב נראה לעין, כל זה צריך לקרות לפחות 50 פעמים בשנייה. סה"כ 9600 פעולות-לד בשנייה, ואם המיקרו-בקר רץ ב-0.5MHz (תדר שעון הפעולות שמופעל כברירת המחדל ב-PIC הזה), המשמעות היא שכל "פעולת לד" צריכה להסתיים תוך כ-52 מחזורי שעון ברוטו. קצת לחוץ, בהינתן הקומפיילר החינמי שלא אוהב אופטימיזציות כמוני וה-Instruction Set של ה-PIC, אבל עדיין סביר.

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

זו לא הייתה טעות בקוד שלי. כל פעולת לד פשוט לקחה הרבה יותר מחזורי שעון ממה שהערכתי. למה? מה קרה? אם נתעלם מהדברים הפחות-קריטיים שמסביב, בשורה התחתונה מדובר בסך הכול על שלוש פקודות C: אחת שהופכת את כל הפינים הרלוונטיים בפורט מסוים לקלט (כדי לכבות את כל הלדים ולמנוע הארה לא-רצויה בצעד הבא), אחת שמגדירה כ-HIGH או LOW את שני הפינים הרלוונטיים ללד שמנוהל כרגע, ואחת שמחזירה את שני הפינים האלה למצב פלט כדי שהלד יאיר בפועל. המידע על ערכי הפינים הרלוונטיים לכל לד שמור במערך של struct מינימליסטי. איך הקומפיילר הצליח למתוח את שלוש הפקודות הדי-בסיסיות האלה על פני עשרות רבות של פקודות אסמבלי? פתחתי את פלט האסמבלי וחשכו עיניי:

פלט האסמבלי המזעזע
פלט האסמבלי המזעזע (לחצו להגדלה… אם יש לכם אומץ)

השורות שמתחילות בתשעים-ומשהו-נקודתיים הן הפקודות המקוריות, ומה שאחריהן הוא התרגום שלהן לאסמבלי. גם בלי להבין מה בדיוק האסמבלי עושה, ברור שפקודה 95 מנופחת מאד, ומה שהרבה יותר גרוע – שימו לב לשורה 079D, זוהי פקודת CALL, שקוראת לעוד קוד אסמבלי שנמצא במקום אחר! הלכתי לברר מה יש שם, והסתבר (הודות להערה אינפורמטיבית) שזהו קוד לביצוע פעולת כפל. כן, המיקרו-בקר הזה כל כך בסיסי שאין לו אפילו יכולת לבצע כפל בחומרה. אבל מה פתאום כפל?

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

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

האסמבלי אחרי המעבר למערכי בייטים פשוטים
האסמבלי אחרי המעבר למערכי בייטים פשוטים (לחצו להגדלה)

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

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

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

אחחחחח, קומפיילרים למיקרובקרים חלשים… תמיד יש להם הפתעות 🙂
ועוד עם הארכיטקטורה הקסומה של בקרי PIC…
אבל יש פה שיעור חשוב מאוד לכל מי שמתיימר לעסוק באמבדד (פרט למתכנתי אמבדד) – תמיד תבחן את הפלט של הקומפיילר שלך.

מרתק!