Оптимизация с пониженной точностью

Числовой формат графических данных и расчеты шейдеров могут оказать существенное влияние на производительность вашей игры.

Оптимальные форматы выполняют следующие функции:

  • Повышение эффективности использования кэша графического процессора
  • Уменьшите потребление полосы пропускания памяти, экономя электроэнергию и повышая производительность.
  • Максимизируйте вычислительную производительность в шейдерных программах.
  • Минимизируйте использование оперативной памяти процессора вашей игрой.

Форматы с плавающей запятой

Большинство вычислений и данных в современной 3D-графике используют числа с плавающей запятой. Vulkan на Android использует числа с плавающей запятой длиной 32 или 16 бит. 32-битное число с плавающей запятой обычно называют одинарной или полной точностью; 16-битное число с плавающей запятой, половинной точности.

Vulkan определяет 64-битный тип с плавающей запятой, но этот тип обычно не поддерживается устройствами Vulkan на Android, и его использование не рекомендуется. 64-битное число с плавающей запятой обычно называют двойной точностью.

Целочисленные форматы

Целые числа со знаком и без знака также используются для данных и вычислений. Стандартный размер целого числа составляет 32 бита. Поддержка других размеров битов зависит от устройства. Устройства Vulkan под управлением Android обычно поддерживают 16-битные и 8-битные целые числа. Vulkan определяет 64-битный целочисленный тип, но этот тип обычно не поддерживается устройствами Vulkan на Android, и его использование не рекомендуется.

Субоптимальное поведение половинной точности

Современные архитектуры графических процессоров объединяют два 16-битных значения в 32-битную пару и реализуют инструкции, которые работают с этой парой. Для оптимальной производительности избегайте использования скалярных 16-битных переменных с плавающей запятой; векторизовать данные в двух- или четырехэлементные векторы. Компилятор шейдеров может использовать скалярные значения в векторных операциях. Однако, если вы полагаетесь на компилятор для оптимизации скаляров, проверьте выходные данные компилятора, чтобы проверить векторизацию.

Преобразование в 32-битные и 16-битные числа с плавающей запятой и обратно требует вычислительных затрат. Сократите накладные расходы за счет минимизации точных преобразований в коде.

Оцените разницу в производительности между 16-битной и 32-битной версиями ваших алгоритмов. Половинная точность не всегда приводит к повышению производительности, особенно для сложных вычислений. Алгоритмы, которые интенсивно используют инструкции объединенного умножения-сложения (FMA) для векторизованных данных, являются хорошими кандидатами на повышение производительности при половинной точности.

Поддержка числового формата

Все устройства Vulkan на Android поддерживают 32-битные числа с плавающей запятой одинарной точности и 32-битные целые числа в данных и вычислениях шейдеров. Поддержка других форматов не гарантируется, а если и доступна, то не гарантируется для всех случаев использования.

Vulkan поддерживает две категории дополнительных числовых форматов: арифметический и хранимый. Прежде чем использовать определенный формат, убедитесь, что устройство поддерживает его в обеих категориях.

Арифметическая поддержка

Устройство Vulkan должно декларировать арифметическую поддержку числового формата, чтобы его можно было использовать в шейдерных программах. Устройства Vulkan на Android обычно поддерживают следующие арифметические форматы:

  • 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. Проверьте shaderFloat16 shaderInt8 структуры результатов VkPhysicalDeviceShaderFloat16Int8Features после вызова vkGetPhysicalDeviceFeatures2() . Если значение поля равно true , формат поддерживается для арифметики программы шейдера.

Поддержка расширения VK_KHR_shader_float16_int8 , хотя и не является обязательным требованием в Vulkan 1.1 или профиле Android Baseline 2022, очень распространена на устройствах Android.

Поддержка хранилища

Устройство Vulkan должно заявить о поддержке дополнительного числового формата для определенных типов хранилища. Расширение VK_KHR_16bit_storage заявляет о поддержке 16-битных целочисленных форматов и 16-битных форматов с плавающей запятой. Расширение определяет четыре типа хранилища. Устройство может поддерживать 16-битные числа ни для одного, ни для некоторых, ни для всех типов памяти.

Типы хранения:

  • Объекты буфера хранения
  • Единые буферные объекты
  • Нажмите константные блоки
  • Интерфейсы ввода и вывода шейдера

Большинство, но не все, устройств Vulkan 1.1 на Android поддерживают 16-битные форматы в объектах буфера хранения. Не предполагайте поддержку на основе модели графического процессора. Устройства со старыми драйверами для данного графического процессора могут не поддерживать объекты буфера хранения, в то время как устройства с более новыми драйверами поддерживают.

Поддержка 16-битных форматов в унифицированных буферах, блоках констант push и интерфейсах ввода-вывода шейдеров обычно зависит от производителя графического процессора. В 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;
  }
}

Уровень точности данных

Число с плавающей запятой половинной точности может представлять меньший диапазон значений с более низкой точностью, чем число с плавающей запятой одинарной точности. Половинная точность часто является простым и без потерь выбором по сравнению с одинарной точностью. Однако половинная точность может оказаться непрактичной во всех случаях использования. Для некоторых типов данных уменьшение диапазона и точности может привести к графическим артефактам или неправильному рендерингу.

Типы данных, которые являются хорошими кандидатами для представления в формате с плавающей запятой половинной точности, включают:

  • Данные о положении в координатах локального пространства
  • UV текстуры для текстур меньшего размера с ограниченной UV-оберткой, которая может быть ограничена диапазоном координат от -1,0 до 1,0.
  • Нормальные, касательные и битангенсные данные
  • Данные цвета вершин
  • Данные с низкими требованиями к точности с центром в 0,0

К типам данных, которые не рекомендуется представлять в формате с плавающей запятой половинной точности, относятся:

  • Данные о положении в глобальных мировых координатах
  • UV-текстуры для высокоточных случаев использования, таких как координаты элементов пользовательского интерфейса на листе атласа.

Точность в коде шейдера

Языки программирования шейдеров OpenGL (GLSL) и High-level Shader Language (HLSL) поддерживают указание расслабленной или явной точности для числовых типов. Ослабленная точность рассматривается как рекомендация для компилятора шейдеров. Явная точность — это требование указанной точности. Устройства 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

Включите расширение 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;