بهینه سازی با دقت کمتر

قالب عددی داده‌های گرافیکی و محاسبات سایه‌زن می‌تواند تأثیر قابل‌توجهی بر عملکرد بازی شما داشته باشد.

قالب‌های بهینه موارد زیر را انجام می‌دهند:

  • افزایش بهره‌وری در استفاده از حافظه نهان پردازنده گرافیکی (GPU)
  • کاهش مصرف پهنای باند حافظه، صرفه‌جویی در مصرف برق و افزایش کارایی
  • به حداکثر رساندن توان محاسباتی در برنامه‌های سایه‌زن
  • استفاده از رم و پردازنده توسط بازی خود را به حداقل برسانید

قالب‌های ممیز شناور

اکثر محاسبات و داده‌ها در گرافیک‌های سه‌بعدی مدرن از اعداد ممیز شناور استفاده می‌کنند. Vulkan در اندروید از اعداد ممیز شناور با اندازه ۳۲ یا ۱۶ بیت استفاده می‌کند. یک عدد ممیز شناور ۳۲ بیتی معمولاً با عنوان دقت تکی یا دقت کامل شناخته می‌شود؛ یک عدد ممیز شناور ۱۶ بیتی، با دقت نیم بیتی.

ولکان یک نوع ممیز شناور ۶۴ بیتی تعریف می‌کند، اما این نوع معمولاً توسط دستگاه‌های ولکان در اندروید پشتیبانی نمی‌شود و استفاده از آن توصیه نمی‌شود. یک عدد ممیز شناور ۶۴ بیتی معمولاً به عنوان دقت مضاعف شناخته می‌شود.

قالب‌های عدد صحیح

اعداد صحیح علامت‌دار و بدون علامت نیز برای داده‌ها و محاسبات استفاده می‌شوند. اندازه استاندارد عدد صحیح ۳۲ بیت است. پشتیبانی از سایر اندازه‌های بیت به دستگاه بستگی دارد. دستگاه‌های Vulkan که اندروید را اجرا می‌کنند معمولاً از اعداد صحیح ۱۶ بیتی و ۸ بیتی پشتیبانی می‌کنند. Vulkan یک نوع عدد صحیح ۶۴ بیتی تعریف می‌کند، اما این نوع معمولاً توسط دستگاه‌های Vulkan در اندروید پشتیبانی نمی‌شود و استفاده از آن توصیه نمی‌شود.

رفتار نیمه‌دقتی غیربهینه

معماری‌های مدرن GPU دو مقدار ۱۶ بیتی را در یک جفت ۳۲ بیتی با هم ترکیب می‌کنند و دستورالعمل‌هایی را پیاده‌سازی می‌کنند که روی این جفت عمل می‌کنند. برای عملکرد بهینه، از استفاده از متغیرهای اعشاری ۱۶ بیتی اسکالر خودداری کنید؛ داده‌ها را به بردارهای دو یا چهار عنصری تبدیل کنید. کامپایلر سایه‌زن ممکن است بتواند از مقادیر اسکالر در عملیات برداری استفاده کند. با این حال، اگر برای بهینه‌سازی اسکالر به کامپایلر تکیه می‌کنید، خروجی کامپایلر را برای تأیید برداری‌سازی بررسی کنید.

تبدیل به/از دقت اعشاری ۳۲ بیتی و ۱۶ بیتی هزینه محاسباتی دارد. با به حداقل رساندن تبدیل‌های دقیق در کد خود، سربار را کاهش دهید.

تفاوت‌های عملکرد بین نسخه‌های ۱۶ بیتی و ۳۲ بیتی الگوریتم‌های خود را محک بزنید. دقت نصف همیشه منجر به بهبود عملکرد نمی‌شود، به خصوص برای محاسبات پیچیده. الگوریتم‌هایی که از دستورالعمل‌های ضرب-جمع ترکیبی (FMA) روی داده‌های برداری استفاده زیادی می‌کنند، کاندیداهای خوبی برای بهبود عملکرد با دقت نصف هستند.

پشتیبانی از قالب عددی

همه دستگاه‌های Vulkan در اندروید از اعداد اعشاری ۳۲ بیتی با دقت تکی و اعداد صحیح ۳۲ بیتی در محاسبات داده‌ها و سایه‌زن پشتیبانی می‌کنند. پشتیبانی از سایر فرمت‌ها تضمین نمی‌شود و در صورت وجود، برای همه موارد استفاده تضمین نمی‌شود.

ولکان دو دسته پشتیبانی برای قالب‌های عددی اختیاری دارد: حسابی و ذخیره‌سازی. قبل از استفاده از یک قالب خاص، مطمئن شوید که دستگاه از هر دو دسته پشتیبانی می‌کند.

پشتیبانی محاسباتی

یک دستگاه Vulkan باید پشتیبانی محاسباتی را برای یک قالب عددی اعلام کند تا در برنامه‌های shader قابل استفاده باشد. دستگاه‌های Vulkan در اندروید معمولاً از قالب‌های محاسباتی زیر پشتیبانی می‌کنند:

  • عدد صحیح ۳۲ بیتی (اجباری)
  • ممیز شناور ۳۲ بیتی (اجباری)
  • عدد صحیح ۸ بیتی (اختیاری)
  • عدد صحیح ۱۶ بیتی (اختیاری)
  • ممیز شناور ۱۶ بیتی با دقت نیم‌دقیقه (اختیاری)

برای تعیین اینکه آیا یک دستگاه Vulkan از اعداد صحیح ۱۶ بیتی برای محاسبات پشتیبانی می‌کند، ویژگی‌های دستگاه را با فراخوانی تابع vkGetPhysicalDeviceFeatures2() بازیابی کنید و بررسی کنید که آیا فیلد shaderInt16 در ساختار نتیجه VkPhysicalDeviceFeatures2 درست است یا خیر.

برای تعیین اینکه آیا یک دستگاه Vulkan از اعداد اعشاری ۱۶ بیتی یا اعداد صحیح ۸ بیتی پشتیبانی می‌کند، مراحل زیر را انجام دهید:

  1. بررسی کنید که آیا دستگاه از افزونه VK_KHR_shader_float16_int8 Vulkan پشتیبانی می‌کند یا خیر. این افزونه برای پشتیبانی از اعداد اعشاری ۱۶ بیتی و اعداد صحیح ۸ بیتی مورد نیاز است.
  2. اگر VK_KHR_shader_float16_int8 پشتیبانی می‌شود، یک اشاره‌گر ساختار VkPhysicalDeviceShaderFloat16Int8Features را به زنجیره VkPhysicalDeviceFeatures2.pNext اضافه کنید.
  3. فیلدهای shaderFloat16 و shaderInt8 از ساختار نتیجه VkPhysicalDeviceShaderFloat16Int8Features را پس از فراخوانی vkGetPhysicalDeviceFeatures2() بررسی کنید. اگر مقدار فیلد true باشد، فرمت برای محاسبات برنامه shader پشتیبانی می‌شود.

اگرچه در Vulkan 1.1 یا پروفایل Android Baseline 2022 الزامی نیست، پشتیبانی از افزونه VK_KHR_shader_float16_int8 در دستگاه‌های اندروید بسیار رایج است.

پشتیبانی از ذخیره‌سازی

یک دستگاه Vulkan باید پشتیبانی از یک قالب عددی اختیاری را برای انواع ذخیره‌سازی خاص اعلام کند. افزونه VK_KHR_16bit_storage پشتیبانی از قالب‌های عدد صحیح ۱۶ بیتی و ممیز شناور ۱۶ بیتی را اعلام می‌کند. چهار نوع ذخیره‌سازی توسط این افزونه تعریف می‌شوند. یک دستگاه می‌تواند از اعداد ۱۶ بیتی برای هیچ، برخی یا همه انواع ذخیره‌سازی پشتیبانی کند.

انواع ذخیره‌سازی عبارتند از:

  • اشیاء بافر ذخیره‌سازی
  • اشیاء بافر یکنواخت
  • بلوک‌های ثابت را فشار دهید
  • رابط‌های ورودی و خروجی سایه‌زن

بیشتر دستگاه‌های Vulkan 1.1 در اندروید، اما نه همه آنها، از فرمت‌های ۱۶ بیتی در اشیاء بافر ذخیره‌سازی پشتیبانی می‌کنند. پشتیبانی را بر اساس مدل GPU فرض نکنید. دستگاه‌هایی که درایورهای قدیمی‌تری برای یک GPU خاص دارند، ممکن است از اشیاء بافر ذخیره‌سازی پشتیبانی نکنند، در حالی که دستگاه‌هایی که درایورهای جدیدتری دارند، این کار را انجام می‌دهند.

پشتیبانی از فرمت‌های ۱۶ بیتی در بافرهای یکنواخت، بلوک‌های ثابت فشار و رابط‌های ورودی/خروجی سایه‌زن عموماً به سازنده‌ی پردازنده‌ی گرافیکی (GPU) بستگی دارد. در اندروید، یک پردازنده‌ی گرافیکی (GPU) معمولاً یا از هر سه نوع پشتیبانی می‌کند یا هیچ‌کدام را پشتیبانی نمی‌کند.

یک تابع نمونه که پشتیبانی از محاسبات و قالب ذخیره‌سازی 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;
  }
}

سطح دقت برای داده‌ها

یک عدد ممیز شناور با دقت نیم‌دقت می‌تواند محدوده‌ی کوچک‌تری از مقادیر را با دقت پایین‌تری نسبت به یک عدد ممیز شناور با دقت تک‌دقتی نشان دهد. دقت نیم‌دقت اغلب یک انتخاب ساده و از نظر ادراکی بدون اتلاف نسبت به دقت تک‌دقتی است. با این حال، دقت نیم‌دقت ممکن است در همه موارد استفاده عملی نباشد. برای برخی از انواع داده‌ها، محدوده و دقت کاهش‌یافته می‌تواند منجر به مصنوعات گرافیکی یا رندر نادرست شود.

انواع داده‌هایی که کاندیداهای خوبی برای نمایش در اعداد اعشاری با دقت نیم هستند عبارتند از:

  • داده‌های موقعیت در مختصات فضایی محلی
  • UV های بافت برای بافت های کوچکتر با پوشش UV محدود که می توانند به محدوده مختصات -1.0 تا 1.0 محدود شوند
  • داده‌های نرمال، تانژانت و بیتژانت
  • داده‌های رنگ رأس
  • داده‌هایی با دقت پایین که روی 0.0 متمرکز شده‌اند

انواع داده‌هایی که برای نمایش در اعشار با دقت نیمه توصیه نمی‌شوند عبارتند از:

  • داده‌های موقعیت در مختصات جهانی
  • UV های بافت برای موارد استفاده با دقت بالا مانند مختصات عنصر UI در یک صفحه اطلس

دقت در کد شیدر

زبان‌های برنامه‌نویسی سایه‌زن OpenGL (GLSL) و زبان سایه‌زن سطح بالا (HLSL) از مشخصات دقت آرام یا دقت صریح برای انواع عددی پشتیبانی می‌کنند. دقت آرام به عنوان یک توصیه برای کامپایلر سایه‌زن در نظر گرفته می‌شود. دقت صریح یک الزام برای دقت مشخص شده است. دستگاه‌های Vulkan در اندروید معمولاً از فرمت‌های ۱۶ بیتی استفاده می‌کنند، زمانی که توسط دقت آرام پیشنهاد می‌شوند. سایر دستگاه‌های Vulkan، به ویژه در رایانه‌های رومیزی که از سخت‌افزار گرافیکی فاقد پشتیبانی از فرمت‌های ۱۶ بیتی استفاده می‌کنند، ممکن است دقت آرام را نادیده بگیرند و همچنان از فرمت‌های ۳۲ بیتی استفاده کنند.

افزونه‌های ذخیره‌سازی در GLSL

پسوندهای GLSL مناسب باید تعریف شوند تا پشتیبانی از قالب‌های عددی ۱۶ بیتی یا ۸ بیتی در ساختارهای ذخیره‌سازی و بافر یکنواخت امکان‌پذیر شود. اعلان‌های پسوند مربوطه عبارتند از:

// 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

برای فعال کردن استفاده از انواع ممیز شناور ۱۶ بیتی، افزونه‌ی GL_EXT_shader_explicit_arithmetic_types_float16 را در کد GLSL خود وارد کنید:

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

انواع اسکالر، برداری و ماتریسی ممیز شناور ۱۶ بیتی را در GLSL با استفاده از کلمات کلیدی زیر تعریف کنید:

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

انواع اسکالر و برداری عدد صحیح ۱۶ بیتی را در GLSL با استفاده از کلمات کلیدی زیر تعریف کنید:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

دقت آرام در HLSL

HLSL به جای دقت آرام از اصطلاح دقت حداقلی استفاده می‌کند. کلمه کلیدی نوع دقت حداقلی، حداقل دقت را مشخص می‌کند، اما اگر دقت بالاتر برای سخت‌افزار هدف انتخاب بهتری باشد، کامپایلر ممکن است دقت بالاتر را جایگزین کند. یک عدد اعشاری ۱۶ بیتی با دقت حداقلی با کلمه کلیدی min16float مشخص می‌شود. اعداد صحیح ۱۶ بیتی با علامت و بدون علامت با دقت حداقلی به ترتیب با کلمات کلیدی min16int و min16uint مشخص می‌شوند. مثال‌های دیگری از اعلان‌های دقت حداقلی شامل موارد زیر است:

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

دقت صریح در HLSL

اعداد اعشاری با دقت نیم با کلمات کلیدی half یا float16_t مشخص می‌شوند. اعداد صحیح ۱۶ بیتی علامت‌دار و بدون علامت به ترتیب با کلمات کلیدی int16_t و uint16_t مشخص می‌شوند. مثال‌های دیگری از اعلان‌های دقیق صریح شامل موارد زیر است:

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