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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • מספר שלם (חובה) ב-32 ביט
  • נקודה צפה ב-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 ביט במאגרי מידע אחידים, בבלוקים של קבועים מסוג push ובממשקי קלט/פלט של 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

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

  • נתוני מיקום בקואורדינטות גלובליות בעולם
  • קואורדינטות 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

כדי להשתמש בסוגי נתונים של נקודה צפה (floating point) של 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 (דיוק משוחרר). מילת מפתח מסוג דיוק מינימלי מציינת את הדיוק המינימלי, אבל יכול להיות שהקומפיילר יחליף אותה בדיוק גבוה יותר אם דיוק גבוה יותר הוא בחירה טובה יותר עבור חומרת היעד. מילת המפתח min16float מציינת מספר נקודה צפה של 16 ביט עם דיוק מינימלי. מספרים שלמים חתומים ולא חתומים של 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;