精度を下げて最適化する

グラフィックス データとシェーダー計算の数値形式は、ゲームのパフォーマンスに大きく影響する可能性があります。

形式が最適であれば、次のようになります。

  • 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 アーキテクチャは、2 つの 16 ビット値を組み合わせて 32 ビットペアにし、このペアで動作する命令を実装しています。パフォーマンスを最適にするには、スカラーの 16 ビット浮動小数点変数を使用せず、データを 2 要素または 4 要素ベクトルにベクトル化します。シェーダー コンパイラは、ベクトル演算でスカラー値を使用できることがありますが、コンパイラに依存してスカラーを最適化する場合は、コンパイラの出力を調べてベクトル化を確認してください。

32 ビットと 16 ビット精度の間の浮動小数点数の変換には、計算コストがかかります。オーバーヘッドを削減するには、コード内での精度の変換回数を最小限に抑えます。

ベンチマークのパフォーマンスは、アルゴリズムの 16 ビット バージョンと 32 ビット バージョンで異なります。特に複雑な計算では、半精度はパフォーマンスの向上につながらないこともあります。半精度でパフォーマンスを向上させるには、ベクトル化されたデータに融合積和演算(FMA)命令を多用するアルゴリズムをおすすめします。

数値形式のサポート

Android の Vulkan デバイスはすべて、データとシェーダー計算で、単精度の 32 ビットの浮動小数点数と 32 ビットの整数をサポートしています。他の形式のサポートは保証されません。また、利用できる場合でも、すべてのユースケースでサポートが保証されるわけではありません。

Vulkan は、オプションの数値形式として、演算とストレージの 2 つのカテゴリをサポートしています。特定の形式を使用する場合は、デバイスが両方のカテゴリでその形式をサポートしていることを確認してください。

演算のサポート

Vulkan デバイスは、シェーダー プログラムで使用できるよう数値形式の演算のサポートを宣言する必要があります。Android の Vulkan デバイスは一般に、演算では次の形式をサポートしています。

  • 32 ビットの整数(必須)
  • 32 ビットの浮動小数点数(必須)
  • 8 ビットの整数(省略可)
  • 16 ビットの整数(省略可)
  • 16 ビットの半精度浮動小数点数(省略可)

Vulkan デバイスが演算で 16 ビット整数をサポートしているかどうかを判断するには、vkGetPhysicalDeviceFeatures2() 関数を呼び出し、VkPhysicalDeviceFeatures2 結果構造体の shaderInt16 フィールドが true であるかどうかを確認して、デバイスの機能を取得します。

Vulkan デバイスが 16 ビットの浮動小数点数や 8 ビットの整数をサポートしているかどうかを確認する手順は次のとおりです。

  1. デバイスが Vulkan 拡張機能 VK_KHR_shader_float16_int8 をサポートしているかどうかを確認します。16 ビットの浮動小数点数と 8 ビットの整数をサポートするには、この拡張機能が必要です。
  2. VK_KHR_shader_float16_int8 がサポートされている場合、VkPhysicalDeviceShaderFloat16Int8Features 構造体ポインタを VkPhysicalDeviceFeatures2.pNext チェーンに追加します。
  3. vkGetPhysicalDeviceFeatures2() を呼び出して、VkPhysicalDeviceShaderFloat16Int8Features 結果構造体の shaderFloat16 フィールドと shaderInt8 フィールドを確認します。フィールドの値が true の場合、その形式はシェーダー プログラムの演算でサポートされます。

Vulkan 1.1 または 2022 Android ベースライン プロファイルの要件ではありませんが、Android デバイスでは VK_KHR_shader_float16_int8 拡張機能のサポートは非常に一般的です。

ストレージのサポート

Vulkan デバイスは、特定のストレージ タイプではオプションの数値形式のサポートを宣言する必要があります。VK_KHR_16bit_storage 拡張機能が、16 ビットの整数形式と 16 ビットの浮動小数点数形式のサポートを宣言します。この拡張機能によって 4 つのストレージ タイプが定義されます。デバイスはどのストレージ タイプでも 16 ビットの数値をサポートできない場合と、一部のストレージ タイプまたはすべてのストレージ タイプでサポートできる場合があります。

ストレージ タイプは次のとおりです。

  • ストレージ バッファ オブジェクト
  • ユニフォーム バッファ オブジェクト
  • プッシュ定数ブロック
  • シェーダー入出力インターフェース

すべてではありませんが、Android のほとんどの Vulkan 1.1 デバイスは、ストレージ バッファ オブジェクトで 16 ビット形式をサポートしています。GPU モデルに基づくサポートを前提としないでください。特定の GPU 用の古いドライバを搭載したデバイスは、ストレージ バッファ オブジェクトをサポートしていないことがありますが、新しいドライバを搭載したデバイスはサポートしています。

ユニフォーム バッファ、プッシュ定数ブロック、シェーダー入出力インターフェースでの 16 ビット形式のサポートは、一般に GPU のメーカーによって異なります。Android の GPU は一般に、これらの 3 つのタイプをすべてサポートするか、いずれもサポートしないかのどちらかです。

以下に、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;
  }
}

データの精度レベル

半精度の浮動小数点数は、単精度の浮動小数点数よりも低い精度で狭い範囲の値を表現できます。半精度は多くの場合、単精度よりシンプルで視覚的にロスレスですが、半精度がすべてのユースケースで役立つとは限りません。データの種類によっては、範囲が狭く、精度が低いと、グラフィックのアーティファクトやレンダリングの誤りが生じることがあります。

半精度の浮動小数点数での表現に推奨されるデータの種類は次のとおりです。

  • ローカルな空間座標の位置データ
  • -1.0~1.0 の座標範囲に制約できる、UV ラッピング制限のある小さいテクスチャ向けのテクスチャ UV
  • 正規化データ、タンジェント データ、バイタンジェント データ
  • 頂点カラーデータ
  • 0.0 を中心とする低精度要件のデータ

半精度の浮動小数点数での表現に推奨されないデータの種類は次のとおりです。

  • グローバルなワールド座標の位置データ
  • アトラスシートの UI 要素座標など、高精度のユースケース向けのテクスチャ UV

シェーダー コードの精度

OpenGL シェーディング言語(GLSL)上位レベル シェーダー言語(HLSL)のシェーダー プログラミング言語は、数値型で緩和精度または明示的精度の指定をサポートしています。緩和精度は、シェーダー コンパイラの推奨として扱われます。明示的精度は、指定された精度の要件です。通常、Android の Vulkan デバイスは、緩和精度で推奨されている場合は 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 修飾子を使用します。 Vulkan の GLSL コンパイラは、以前の lowp 修飾子を mediump として解釈します。以下に、緩和精度の例を示します。

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

GLSL の明示的精度

16 ビットの浮動小数点数型の使用を有効にするには、GLSL コードに GL_EXT_shader_explicit_arithmetic_types_float16 拡張機能を追加します。

#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 ビットの整数は、それぞれ 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;