ביצוע אופטימיזציה ברמת דיוק נמוכה יותר

לפורמט המספרי של נתוני הגרפיקה ולחישובי ההצללה יכולה להיות השפעה משמעותית על ביצועי המשחק.

פורמטים אופטימליים:

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

פורמטים של נקודה צפה

רוב החישובים והנתונים בגרפיקה תלת-ממדית מודרנית מבוססים על מספרים עם נקודה צפה. ‫Vulkan ב-Android משתמש במספרים עם נקודה צפה בגודל 32 או 16 ביט. מספר בשיטת נקודה צפה של 32 ביט נקרא בדרך כלל דיוק יחיד או דיוק מלא, ומספר בשיטת נקודה צפה של 16 ביט נקרא חצי דיוק.

‫Vulkan מגדיר סוג של נקודה צפה (floating point) של 64 ביט, אבל הסוג הזה לא נתמך בדרך כלל במכשירי Vulkan ב-Android, ולא מומלץ להשתמש בו. מספר נקודה צפה (floating point) של 64 ביט נקרא בדרך כלל דיוק כפול (double precision).

פורמטים של מספרים שלמים

בנוסף, נעשה שימוש במספרים שלמים עם סימן ובלי סימן לנתונים ולחישובים. גודל המספר השלם הרגיל הוא 32 ביט. התמיכה בגדלים אחרים של סיביות תלויה במכשיר. במכשירי Vulkan עם Android יש בדרך כלל תמיכה במספרים שלמים של 16 ביט ו-8 ביט. ‫Vulkan מגדיר סוג של מספר שלם בן 64 ביט, אבל הסוג הזה לא נתמך בדרך כלל במכשירי Vulkan ב-Android, ולא מומלץ להשתמש בו.

התנהגות לא אופטימלית של דיוק חצי

ארכיטקטורות GPU מודרניות משלבות שני ערכים של 16 ביט בזוג של 32 ביט ומיישמות הוראות שפועלות על הזוג. לביצועים אופטימליים, מומלץ להימנע משימוש במשתנים סקלריים של נקודה צפה (float) בגודל 16 ביט, ולבצע וקטוריזציה של הנתונים לתוך וקטורים של שניים או ארבעה רכיבים. יכול להיות שהקומפיילר של ה-shader יוכל להשתמש בערכים סקלריים בפעולות וקטוריות. עם זאת, אם אתם מסתמכים על הקומפיילר כדי לבצע אופטימיזציה של סקלרים, כדאי לבדוק את הפלט של הקומפיילר כדי לוודא שבוצעה וקטוריזציה.

להמרה לנקודה צפה (floating-point) עם דיוק של 32 ביט ו-16 ביט וממנה יש עלות חישובית. כדי להפחית את התקורה, כדאי לצמצם את ההמרות המדויקות בקוד.

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

תמיכה בפורמט מספרי

כל מכשירי Vulkan ב-Android תומכים במספרים בנקודה צפה של 32 ביט ובמספרים שלמים של 32 ביט בחישובים של נתונים ותוכנות הצללה (shader). התמיכה בפורמטים אחרים לא מובטחת, ואם היא זמינה, היא לא מובטחת לכל תרחישי השימוש.

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

תמיכה באריתמטיקה

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

  • מספר שלם (חובה)
  • נקודה צפה ב-32 ביט (חובה)
  • מספר שלם 8 ביט (אופציונלי)
  • מספר שלם 16 ביט (אופציונלי)
  • נקודה צפה (floating-point) בחצי דיוק של 16 ביט (אופציונלי)

כדי לבדוק אם מכשיר Vulkan תומך במספרים שלמים של 16 ביט עבור פעולות אריתמטיות, צריך לאחזר את התכונות של המכשיר באמצעות קריאה לפונקציה vkGetPhysicalDeviceFeatures2()‎ ולבדוק אם השדה shaderInt16 במבנה התוצאה VkPhysicalDeviceFeatures2 הוא true.

כדי לבדוק אם מכשיר Vulkan תומך במספרים ממשיים של 16 ביט או במספרים שלמים של 8 ביט, מבצעים את השלבים הבאים:

  1. בודקים אם המכשיר תומך בתוסף Vulkan‏ VK_KHR_shader_float16_int8. התוסף נדרש לתמיכה בנקודה צפה של 16 ביט ובמספר שלם של 8 ביט.
  2. אם יש תמיכה ב-VK_KHR_shader_float16_int8, מוסיפים מצביע למבנה VkPhysicalDeviceShaderFloat16Int8Features לשרשרת VkPhysicalDeviceFeatures2.pNext.
  3. בודקים את השדות shaderFloat16 ו-shaderInt8 במבנה התוצאה VkPhysicalDeviceShaderFloat16Int8Features אחרי ההפעלה של vkGetPhysicalDeviceFeatures2(). אם ערך השדה הוא true, הפורמט נתמך עבור אריתמטיקה של תוכנית הצללה.

התמיכה בתוסף VK_KHR_shader_float16_int8 נפוצה מאוד במכשירי Android, למרות שהיא לא נדרשת ב-Vulkan 1.1 או בפרופיל הבסיסי של Android משנת 2022.

תמיכה באחסון

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

סוגי האחסון הם:

  • אובייקטים של מאגר אחסון
  • אובייקטים של מאגרים זמניים אחידים
  • בלוקים של קבועים מסוג Push
  • ממשקי קלט ופלט של Shader

ברוב המכשירים עם Vulkan 1.1 ב-Android יש תמיכה בפורמטים של 16 ביט באובייקטים של מאגר אחסון, אבל לא בכולם. אל תניחו שיש תמיכה על סמך דגם ה-GPU. יכול להיות שמכשירים עם מנהלי התקנים ישנים יותר ל-GPU מסוים לא יתמכו באובייקטים של מאגר אחסון, בעוד שמכשירים עם מנהלי התקנים חדשים יותר כן יתמכו בהם.

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

פונקציה לדוגמה שבודקת אם יש תמיכה בפורמט אריתמטי ובפורמט אחסון של Vulkan:

struct ReducedPrecisionSupportInfo {
  // Arithmetic support
  bool has_8_bit_int_ = false;
  bool has_16_bit_int_ = false;
  bool has_16_bit_float_ = false;
  // Storage support
  bool has_16_bit_SSBO_ = false;
  bool has_16_bit_UBO_ = false;
  bool has_16_bit_push_ = false;
  bool has_16_bit_input_output_ = false;
  // Use 16-bit floats if we have arithmetic
  // support and at least SSBO storage support.
  bool use_16bit_floats_ = false;
};

void CheckFormatSupport(VkPhysicalDevice physical_device,
    ReducedPrecisionSupportInfo &info) {

  // Retrieve the device extension list so we
  // can check for our desired extensions.
  uint32_t device_extension_count;
  vkEnumerateDeviceExtensionProperties(physical_device, nullptr,
      &device_extension_count, nullptr);
  std::vector<VkExtensionProperties> device_extensions(device_extension_count);
  vkEnumerateDeviceExtensionProperties(physical_device, nullptr,
      &device_extension_count, device_extensions.data());

  bool has_16_8_extension = HasDeviceExtension("VK_KHR_shader_float16_int8",
      device_extensions);

  // Initialize the device features structure and
  // chain the storage features structure and 8/16-bit
  // support structure if applicable.
  VkPhysicalDeviceFeatures2 device_features;
  memset(&device_features, 0, sizeof(device_features));
  device_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;

  VkPhysicalDeviceShaderFloat16Int8Features f16_int8_features;
  memset(&f16_int8_features, 0, sizeof(f16_int8_features));
  f16_int8_features.sType =
      VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT16_INT8_FEATURES_KHR;

  VkPhysicalDevice16BitStorageFeatures storage_features;
  memset(&storage_features, 0, sizeof(storage_features));
  storage_features.sType =
      VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES;
  device_features.pNext = &storage_features;

  if (has_16_8_extension) {
    storage_features.pNext = &f16_int8_features;
  }

  vkGetPhysicalDeviceFeatures2(physical_device, &device_features);

  // Parse the storage features and determine
  // what kinds of 16-bit storage access are available.
  if (storage_features.storageBuffer16BitAccess ||
      storage_features.uniformAndStorageBuffer16BitAccess) {
    info.has_16_bit_SSBO_ = true;
  }
  info.has_16_bit_UBO_ = storage_features.uniformAndStorageBuffer16BitAccess;
  info.has_16_bit_push_ = storage_features.storagePushConstant16;
  info.has_16_bit_input_output_ = storage_features.storageInputOutput16;

  info.has_16_bit_int_ = device_features.features.shaderInt16;
  if (has_16_8_extension) {
    info.has_16_bit_float_ = f16_int8_features.shaderFloat16;
    info.has_8_bit_int_ = f16_int8_features.shaderInt8;
  }

  // Get arithmetic and at least some form of storage
  // support before enabling 16-bit float usage.
  if (info.has_16_bit_float_ && info.has_16_bit_SSBO_) {
    info.use_16bit_floats_ = true;
  }
}

רמת הדיוק של הנתונים

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

סוגי נתונים שמתאימים לייצוג בנקודה צפה בחצי דיוק כוללים:

  • נתוני מיקום במערכת קואורדינטות מקומית
  • ערכי UV של טקסטורות קטנות יותר עם עטיפת UV מוגבלת שאפשר להגביל לטווח קואורדינטות של ‎-1.0 עד 1.0
  • נתונים של נורמלים, טנגנטים וביטנגנטים
  • נתוני צבע של קודקוד
  • נתונים עם דרישות דיוק נמוכות שמתרכזים סביב 0.0

סוגי נתונים שלא מומלצים לייצוג בערך float עם חצי דיוק כוללים:

  • נתוני מיקום בקואורדינטות גלובליות
  • קואורדינטות UV של טקסטורה לתרחישי שימוש ברמת דיוק גבוהה, כמו קואורדינטות של רכיבי ממשק משתמש בגיליון אטלס

דיוק בקוד של Shader

שפות התכנות של הצללות OpenGL Shading Language (GLSL) ו-High-level Shader Language (HLSL) תומכות בהגדרה של דיוק מופחת או דיוק מפורש לסוגים מספריים. הדיוק המופחת נחשב להמלצה עבור מהדר ה-Shader. הדיוק המפורש הוא דרישה של הדיוק שצוין. במכשירי Vulkan ב-Android, בדרך כלל נעשה שימוש בפורמטים של 16 ביט כשמומלץ להשתמש בדיוק לא מחמיר. יכול להיות שבמכשירי Vulkan אחרים, במיוחד במחשבים שולחניים עם חומרה גרפית שלא תומכת בפורמטים של 16 ביט, המערכת תתעלם מהדיוק המופחת ותמשיך להשתמש בפורמטים של 32 ביט.

תוספי אחסון ב-GLSL

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

// Enable 16-bit formats in storage and uniform buffers.
#extension GL_EXT_shader_16bit_storage : require
// Enable 8-bit formats in storage and uniform buffers.
#extension GL_EXT_shader_8bit_storage : require

התוספים האלה ספציפיים ל-GLSL ואין להם מקבילה ב-HLSL.

דיוק מופחת ב-GLSL

משתמשים במאפיין highp לפני סוג של מספר נקודה צפה כדי להציע מספר נקודה צפה עם דיוק יחיד, ובמאפיין mediump כדי להציע מספר נקודה צפה עם דיוק חצי. קומפיילרים של GLSL ל-Vulkan מפרשים את המאפיין lowp מדור קודם כ-mediump. דוגמאות לדיוק משופר:

mediump vec4 my_vector; // Suggest 16-bit half precision
highp mat4 my_matrix;   // Suggest 32-bit single precision

דיוק מפורש ב-GLSL

כדי לאפשר שימוש בסוגים של נקודות צפות של 16 ביט, צריך לכלול את התוסף GL_EXT_shader_explicit_arithmetic_types_float16 בקוד GLSL:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

מצהירים על סוגים של סקלר, וקטור ומטריצה בנקודה צפה של 16 ביט ב-GLSL באמצעות מילות המפתח הבאות:

float16_t   f16vec2     f16vec3    f16vec4
f16mat2     f16mat3     f16mat4
f16mat2x2   f16mat2x3   f16mat2x4
f16mat3x2   f16mat3x3   f16mat3x4
f16mat4x2   f16mat4x3   f16mat4x4

מצהירים על סוגים של מספרים שלמים סקלריים ווקטוריים של 16 ביט ב-GLSL באמצעות מילות המפתח הבאות:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

דיוק מופחת ב-HLSL

ב-HLSL משתמשים במונח minimal precision (דיוק מינימלי) במקום במונח relaxed precision (דיוק משוחרר). מילת מפתח מסוג דיוק מינימלי מציינת את הדיוק המינימלי, אבל יכול להיות שהקומפיילר יחליף אותה בדיוק גבוה יותר אם הוא יתאים יותר לחומרה של היעד. מספר נקודה צפה של 16 ביט עם דיוק מינימלי מצוין על ידי מילת המפתח min16float. מספרים שלמים חתומים ולא חתומים של 16 ביט עם דיוק מינימלי מצוינים על ידי מילות המפתח min16int ו-min16uint בהתאמה. דוגמאות נוספות להצהרות על דיוק מינימלי:

// Four element vector and four-by-four matrix types
min16float4 my_vector4;
min16float4x4 my_matrix4x4;

דיוק מפורש ב-HLSL

נקודה צפה בחצי דיוק מצוינת על ידי מילות המפתח half או float16_t. מספרים שלמים חתומים ולא חתומים בני 16 ביט מצוינים באמצעות מילות המפתח int16_t ו-uint16_t, בהתאמה. דוגמאות נוספות להצהרות מפורשות על דיוק:

// Four element vector and four-by-four matrix types
half4 my_vector4;
half4x4 my_matrix4x4;