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

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

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

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

Форматы с плавающей точкой

Большинство вычислений и данных в современной 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. Проверьте, поддерживает ли устройство расширение Vulkan VK_KHR_shader_float16_int8 . Оно необходимо для поддержки 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 Shading Language (GLSL) и High-level Shader Language (HLSL) поддерживают указание ослабленной или явной точности для числовых типов. Указание ослабленной точности рассматривается как рекомендация для компилятора шейдеров. Явная точность является обязательным условием для указанной точности. Устройства Vulkan на Android обычно используют 16-битные форматы, когда это указано в ослабленной точности. Другие устройства Vulkan, особенно на настольных компьютерах с графическим оборудованием, не поддерживающим 16-битные форматы, могут игнорировать ослабленную точность и по-прежнему использовать 32-битные форматы.

Расширения хранилища в GLSL

Для поддержки 16-битных или 8-битных числовых форматов в структурах хранения и унифицированных буферов необходимо определить соответствующие расширения 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, чтобы разрешить использование 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;