بهینه‌سازی با دقت کاهش‌یافته

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

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

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

قالب‌های نقطه شناور

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

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

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

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

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

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

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

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

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

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

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

پشتیبانی از محاسبات عددی

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

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

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

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

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

درحالی‌که در Vulkan 1.1 یا نمایه پایه Android سال ۲۰۲۲ الزامی نیست، پشتیبانی از افزونه VK_KHR_shader_float16_int8 در دستگاه‌های Android بسیار رایج است.

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

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

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

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

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

پشتیبانی از قالب‌های ۱۶ بیتی در میانگیرهای یکنواخت، بلوک‌های ثابت فشاری، و واسط‌های ورودی/خروجی سایه‌زن معمولاً به سازنده واحد پردازش گرافیکی بستگی دارد. در Android، یک 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 محدود که می‌تواند به محدوده مختصات ۱٫۰- تا ۱٫۰ محدود شود
  • داده‌های نرمال، مماس، و دومماس
  • داده‌های رنگ رأس
  • داده‌های با الزامات دقت پایین با مرکزیت ۰٫۰

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

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

دقت در کد سایه‌زن

زبان‌های برنامه‌نویسی سایه‌زن OpenGL Shading Language (GLSL) و High-level Shader Language (HLSL) از مشخصات دقت آزاد یا دقت صریح برای انواع عددی پشتیبانی می‌کنند. دقت کاهش‌یافته به‌عنوان توصیه‌ای برای کامپایلر سایه‌زن درنظر گرفته می‌شود. دقت صریح یکی از الزامات دقت مشخص‌شده است. دستگاه‌های Vulkan در Android معمولاً از قالب‌های ۱۶ بیتی استفاده می‌کنند، وقتی که دقت کاهش‌یافته پیشنهاد می‌شود. سایر دستگاه‌های 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;