使用降低精度进行优化

图形数据的数值格式和着色器计算可能会对游戏的性能产生重大影响。

最佳格式执行以下操作

  • 提高GPU缓存使用的效率
  • 减少内存带宽消耗,节省电量并提高性能
  • 最大化着色器程序中的计算吞吐量
  • 最大限度地减少游戏的CPU RAM使用量

浮点格式

现代3D图形中的大部分计算和数据都使用浮点数。Android上的Vulkan使用大小为32位或16位的浮点数。32位浮点数通常称为单精度或全精度;16位浮点数,半精度。

Vulkan定义了64位浮点类型,但Android上的Vulkan设备通常不支持该类型,不建议使用。64位浮点数通常称为双精度。

整数格式

有符号和无符号整数也用于数据和计算。标准整数大小为32位。对其他位大小的支持取决于设备。运行Android的Vulkan设备通常支持16位和8位整数。Vulkan定义了64位整数类型,但Android上的Vulkan设备通常不支持该类型,不建议使用。

次优半精度行为

现代GPU架构将两个16位值组合到一个32位对中,并实现对该对进行操作的指令。为了获得最佳性能,避免使用标量16位浮点变量;将数据矢量化到二元或四元向量中。着色器编译器可能能够在向量运算中使用标量值。但是,如果您依赖编译器来优化标量,请检查编译器输出以验证矢量化。

在32位和16位精度浮点数之间进行转换会产生计算成本。通过最大限度地减少代码中的精度转换来减少开销。

对算法的16位和32位版本的性能差异进行基准测试。半精度并不总是导致性能提升,尤其是在复杂计算中。在矢量化数据上大量使用融合乘加 (FMA) 指令的算法是半精度性能提升的理想选择。

数值格式支持

Android上的所有Vulkan设备都支持数据和着色器计算中的单精度、32位浮点数和32位整数。不能保证其他格式可用,如果可用,则不能保证适用于所有用例。

Vulkan将可选数值格式的支持分为两类:算术和存储。在使用特定格式之前,请确保设备在这两类中都支持它。

算术支持

Vulkan设备必须声明对数值格式的算术支持才能在着色器程序中使用它。Android上的Vulkan设备通常支持以下格式的算术

  • 32位整数(强制)
  • 32位浮点数(强制)
  • 8位整数(可选)
  • 16位整数(可选)
  • 16位半精度浮点数(可选)

要确定Vulkan设备是否支持16位整数进行算术运算,请通过调用vkGetPhysicalDeviceFeatures2()函数并检查VkPhysicalDeviceFeatures2结果结构中shaderInt16字段是否为真来检索设备的功能。

要确定Vulkan设备是否支持16位浮点数或8位整数,请执行以下步骤

  1. 检查设备是否支持VK_KHR_shader_float16_int8 Vulkan扩展。该扩展是16位浮点数和8位整数支持所必需的。
  2. 如果支持VK_KHR_shader_float16_int8,则将VkPhysicalDeviceShaderFloat16Int8Features结构指针附加到VkPhysicalDeviceFeatures2.pNext链。
  3. 在调用vkGetPhysicalDeviceFeatures2()后,检查VkPhysicalDeviceShaderFloat16Int8Features结果结构的shaderFloat16shaderInt8字段。如果字段值为true,则该格式支持着色器程序算术运算。

虽然不是Vulkan 1.1或2022年Android基线配置文件的要求,但在Android设备上对VK_KHR_shader_float16_int8扩展的支持非常普遍。

存储支持

Vulkan设备必须声明对特定存储类型的可选数值格式的支持。VK_KHR_16bit_storage扩展声明对16位整数和16位浮点格式的支持。该扩展定义了四种存储类型。设备可以对部分或全部存储类型支持16位数字。

存储类型为

  • 存储缓冲区对象
  • 统一缓冲区对象
  • 推送常量块
  • 着色器输入和输出接口

Android上大多数(但不是全部)Vulkan 1.1设备都支持存储缓冲区对象中的16位格式。不要根据GPU型号假设支持。具有给定GPU的旧驱动程序的设备可能不支持存储缓冲区对象,而具有较新驱动程序的设备则支持。

统一缓冲区、推送常量块和着色器输入/输出接口中对16位格式的支持通常取决于GPU制造商。在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 映射且可以约束在 -1.0 到 1.0 坐标范围内的纹理 UV
  • 法线、切线和副切线数据
  • 顶点颜色数据
  • 以 0.0 为中心,精度要求低的数据

以下数据类型建议使用半精度浮点数表示:

  • 全局世界坐标中的位置数据
  • 对于高精度用例(例如图集表中的 UI 元素坐标)的纹理 UV

着色器代码中的精度

OpenGL 着色语言 (GLSL) 和高级着色语言 (HLSL) 着色器编程语言支持为数值类型指定宽松精度或显式精度。宽松精度被视为对着色器编译器的建议。显式精度是指定精度的要求。Android 上的 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 限定符来建议使用半精度浮点数。用于 Vulkan 的 GLSL 编译器将旧版 lowp 限定符解释为 mediump。以下是一些宽松精度的示例:

mediump vec4 my_vector; // Suggest 16-bit half precision
highp mat4 my_matrix;   // Suggest 32-bit single precision

GLSL 中的显式精度

在 GLSL 代码中包含 GL_EXT_shader_explicit_arithmetic_types_float16 扩展以启用 16 位浮点类型的使用。

#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require

使用以下关键字在 GLSL 中声明 16 位浮点标量、向量和矩阵类型:

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

使用以下关键字在 GLSL 中声明 16 位整数标量和向量类型:

int16_t     i16vec2     i16vec3    i16vec4
uint16_t    u16vec2     u16vec3    u16vec4

HLSL 中的宽松精度

HLSL 使用术语“最小精度”代替宽松精度。最小精度类型关键字指定最小精度,但如果更高精度对于目标硬件来说是更好的选择,编译器可能会替换为更高精度。最小精度 16 位浮点数由 min16float 关键字指定。最小精度有符号和无符号 16 位整数分别由 min16intmin16uint 关键字指定。以下是一些最小精度声明的示例:

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

HLSL 中的显式精度

半精度浮点数由 halffloat16_t 关键字指定。有符号和无符号 16 位整数分别由 int16_tuint16_t 关键字指定。以下是一些显式精度声明的示例:

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