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

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

فرمت های بهینه موارد زیر را انجام می دهند:

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

فرمت های ممیز شناور

اکثر محاسبات و داده ها در گرافیک سه بعدی مدرن از اعداد ممیز شناور استفاده می کنند. Vulkan در اندروید از اعداد ممیز شناور استفاده می کند که اندازه آنها 32 یا 16 بیت است. یک عدد ممیز شناور 32 بیتی معمولاً با دقت تک یا دقت کامل شناخته می شود. یک عدد ممیز شناور 16 بیتی، نیم دقت.

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

فرمت های عدد صحیح

از اعداد صحیح علامت دار و بدون علامت نیز برای داده ها و محاسبات استفاده می شود. اندازه عدد صحیح استاندارد 32 بیت است. پشتیبانی از سایر اندازه های بیت به دستگاه بستگی دارد. دستگاه های Vulkan دارای اندروید معمولاً از اعداد صحیح 16 بیتی و 8 بیتی پشتیبانی می کنند. Vulkan یک نوع عدد صحیح 64 بیتی را تعریف می کند، اما این نوع معمولاً توسط دستگاه های Vulkan در اندروید پشتیبانی نمی شود و استفاده از آن توصیه نمی شود.

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

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

تبدیل به و از ممیز شناور دقیق 32 بیتی و 16 بیتی هزینه محاسباتی دارد. با به حداقل رساندن تبدیل های دقیق در کد خود، هزینه های اضافی را کاهش دهید.

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

پشتیبانی از فرمت عددی

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

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

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

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

  • عدد صحیح 32 بیتی (اجباری)
  • ممیز شناور 32 بیتی (اجباری)
  • عدد صحیح 8 بیتی (اختیاری)
  • عدد صحیح 16 بیتی (اختیاری)
  • ممیز شناور نیمه دقیق 16 بیتی (اختیاری)

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

برای تعیین اینکه آیا دستگاه Vulkan از شناورهای 16 بیتی یا اعداد صحیح 8 بیتی پشتیبانی می کند، مراحل زیر را انجام دهید:

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

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

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

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

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

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

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

پشتیبانی از فرمت‌های 16 بیتی در بافرهای یکنواخت، بلوک‌های ثابت فشاری و رابط‌های ورودی/خروجی سایه‌زن معمولاً به سازنده 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;
  }
}

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

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

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

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

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

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

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

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

پسوند GL_EXT_shader_explicit_arithmetic_types_float16 را در کد GLSL خود بگنجانید تا امکان استفاده از انواع ممیز شناور 16 بیتی فراهم شود:

#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 از اصطلاح دقت حداقلی به جای دقت آرام استفاده می کند. یک کلمه کلیدی با نوع دقت حداقل حداقل دقت را مشخص می کند، اما اگر دقت بالاتر انتخاب بهتری برای سخت افزار هدف باشد، کامپایلر ممکن است دقت بالاتری را جایگزین کند. یک شناور 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;