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

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

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

אך מהי באמת ביצועים?

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

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

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

HMI Qt QMLoptimisation performance

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

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

מלכודות ביצועים נפוצות

שימוש בפתרונות מורכבים

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

HMI Qt QMLoptimisation performance

אופטימיזציה מוקדמת מדי

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

אי-שימוש בפרופיילר

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

לניתוח זיכרון CPU ו-RAM של קוד QML / JavaScript, עליך להשתמש ב-QML profiler המסופק עם Qt Creator. עם זאת, הוא אינו מנתח קוד C++. אולם כל profiler לשימוש כללי אמור להספיק לניתוח קוד C++ (למשל Valgrind Callgrind).

לניתוח GPU / renderer, ניתן להגדיר משתני סביבה ספציפיים כדי לאפשר סטטיסטיקות רינדור Quick Scene Graph – אותם תלמדו בהמשך מאמר זה.

קישורים עודפים

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

HMI Qt QMLoptimisation performance

קישורים (Bindings) שתלויים בקישורים אחרים יכולים לגרום לתגובת שרשרת שבה שינוי במאפיין אחד מפעיל עדכונים במספר קישורים. הדבר יכול לגרום לחישובים מחדש מדורגים (cascading recomputations) גם כאשר השינוי במאפיין הבסיסי אינו משפיע על התוצאה.

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

ציור יתר

הדרך הקלה ביותר להיות מהיר יותר היא לצייר פחות. באופן אידיאלי, היינו רוצים ש-Qt Quick יצייר את כל האלמנטים שהמשתמש יכול לראות, ורק אותם. יש לזכור ש-Qt Quick חייב לצייר מחדש כל פריט שערך המאפיין visible שלו הוא true. יש להסתיר פריטים מוסתרים – ניתן לעשות זאת באופן חלק עם תצוגות (למשל StackView).

עובד היטב במחשב שולחני, אך קצב הפריימים יורד במערכת משובצת

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

אי-הבנה של אופן הפעולה של renderers

בין אם מדובר ב-OpenGL, Vulkan או Qt Quick Renderer – אי-היכרות עם המושג הכללי של רינדור צינור גרפי תחזור אליך במוקדם או במאוחר. אין צורך להיות מומחה ברינדור, אך הבנה כיצד הוא פועל מתחת למכסה המנוע תחסוך ממך עוגמות נפש רבות בהמשך ותאפשר לך להפיק את המקסימום מהיעילות של שבב הגרפיקה.

HMI Qt QMLoptimisation performance

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

זכרו: GPUs מצוינים בציור מספר גדול של פרימיטיבים בבת אחת.

לכן שימוש במספר רב ככל האפשר של פרימיטיבים הוא יעיל.

Qt Quick Renderer

HMI Qt QMLoptimisation performance

גרף סצנה

ה-Scene Graph הוא מבנה הנתונים המרכזי המשמש את Qt Quick Renderer לניהול ורינדור של אלמנטים חזותיים. הוא מארגן פריטי QML במבנה עץ, שבו כל צומת מייצג אלמנט חזותי, כגון מלבן או תמונה. הוא מאפשר דרכים לייעול הרינדור.

עיבוד באצוות

Batching מקבץ פקודות רינדור מרובות לפעולה אחת כדי למזער את העומס על ה-GPU. לא ניתן לקבץ את כל פקודות הרינדור – רק אלו שחולקות את אותו מצב pipeline. תארו לעצמכם שכל שינוי באטימות, ב-shader, בטקסטורה, ב-clipping או ב-render target מביא ל-batch חדש (מה שמוביל לביצועים גרועים יותר).

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

ביצועים טובים נובעים מאצווה יעילה, עם כמה שפחות מהגיאומטריה שמועלית שוב ושוב.

Opaque primitives

המרנדר מפריד בין פרימיטיבים אטומים לפרימיטיבים הדורשים מיזוג אלפא. באמצעות בחינת מצב החומר של כל פרימיטיב, המרנדר ייצור אצוות אטומות. מערך הפריטים הבסיסי של Qt Quick כולל פריטים מלבניים עם צבעים אטומים ותמונות אטומות לחלוטין, כגון JPEG או BMP (לא PNG). יתרון נוסף הוא שפרימיטיבים אטומים אינם דורשים הפעלה של GL_BLEND, שעלולה להיות יקרה למדי, במיוחד ב-GPU לנייד ולמערכות משובצות.

ניפוי שגיאות

על ידי הגדרת משתנה הסביבה QSG_RENDERER_DEBUG=render, המרנדר יפיק סטטיסטיקות על מידת היעילות של ה-batching, כמה batches בשימוש, אילו batches נשמרים ואילו אטומים ולא. כאשר שואפים לביצועים מיטביים, העלאות צריכות להתרחש רק כשהן באמת נחוצות, מספר ה-batches צריך להיות קטן מ-10, ולפחות 3-4 מהם צריכים להיות אטומים.

הגדרת QSG_VISUALIZE ל-batches מציגה אצוות ב-renderer, clip מצייר אזורים אדומים לציון חיתוך, changes מציג שינויים באמצעות הבהוב שכבת-על בצבע אקראי, overdraw מדגיש overdraws בתלת-ממד.

מעוניינים לעבוד ב-Qt/QML?

הצטרפו אלינו

מקרה בוחן

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

עיצובי ממשק המשתמש של הפרויקט נעשו ב-Figma, כלי פופולרי ליצירה, שיתוף ובדיקה של עיצובים עבור אפליקציות רבות. הוא מגיע עם שפע של מאפיינים לעבודה. אחד מהם הוא אפקטי צל. צללים חיצוניים או פנימיים אלה מתורגמים למאפיין ה-CSS box-shadow.

HMI Qt QMLoptimisation performance

היינו צריכים להשיג את אותו אפקט ב-QML – והצלחנו.

הגישה הראשונה שלנו הייתה להשתמש ברכיב DropShadow ממודול Graphical Effects של Qt. התוצאה הוויזואלית הייתה מצוינת, אך הפגיעה בביצועים במכשיר המוטמע הייתה עצומה.

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

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

מכיוון שהיה לי ניסיון בפיתוח משחקים, הכרתי היטב את הקונספט של 9-slice scaling.
זוהי טכניקה ליצירת תמונות הניתנות להרחבה שניתן לשנות את גודלן מבלי לעוות את החלקים החשובים של התמונה (הפינות). למרבה המזל Qt Quick מספק את רכיב BorderImage שמיישם טכניקה זו.

HMI Qt QMLoptimisation performance

אכן ביקשתי מהמעצבים שלנו תמונות 9-slice של צללים מרונדרים מראש, מה שלא היווה מטרד גדול עבורם מאחר ש-Adobe Photoshop (וכלים רבים אחרים) מציעים תוספים לעריכת 9-slice ליצירת נכסים כאלה בקלות.

החלפתי במהירות את גישת ה-shader בגישת border image ביישום ה-box shadow שלנו וקרה נס: מספר הפריימים לשנייה שולש, מ-20 fps ל-60 fps. אפילו צריכת הזיכרון חצתה, מ-1200 mb ל-600 mb. מכיוון שכל פריט שהשתמש בצל עובד לשכבה/מאגר נפרד, טביעת הזיכרון של יישום כזה הייתה עצומה למדי.

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

לעתים קרובות, השלמות היא האויב של הטוב.

תקציר

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

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

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