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

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

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

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

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

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

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

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

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

התמיכה בתוסף VK_KHR_shader_float16_int8 נפוצה מאוד במכשירי Android, למרות שהיא לא נדרשת ב-Vulkan 1.1 או בפרופיל ה-Baseline של 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

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