Real Unreal Engine C++ 2017-12 (part-2/5)
(はてなブログの記事あたりの容量制限のため前の部分 §1.8. までは前の記事でどうぞ→http://usagi.hatenablog.jp/entry/2017/12/01/ac_ue4_2)
1.9. UE4/C++ における入門的な「数学」ライブラリーの std 互換性と独自性
1.9.1. FMath
における C++ CRT 互換層
C++er は "事足りる限り" は通常 <cmath>
を用いて数学処理を実装する。
UE4 では多くの数学的な実装は FMath
型に static
メンバー関数として実装されている。実はこの Fmath
のソースコードを辿ると、 "Runtime/Core/Public/HAL/PlatformCrt.h" へ辿り着き、 math.h
が #include
される。
#include <new> // <-- <new> も有効になるので placement new 構文など使用可能になる #include <wchar.h> #include <stddef.h> // <-- size_t はここ。 #include <stdlib.h> #include <stdio.h> #include <stdarg.h> #include <math.h> // <-- 本項で触れる互換性 #include <float.h> // <-- ついでに読んでいる #include <string.h>
と、云うわけで、 C++er は UE4 プロジェクトでも <cmath>
の機能をそのまま使用して数学的な処理を記述「することも」できる。
但し、 <cmath>
は処理系依存で挙動が変わる機能もあり、また実行速度に最適ではない実装が採用されている事もあり、 UE4 では FMath
型を定義し、 static
なメンバー関数として <cmath>
の薄いラッパーから独自の再実装、 <cmath>
にない機能の実装などを整理している。
よって、 UE4/C++er は特定の単一あるいは十分に <cmath>
で十分に製品の目的に適う場合を除き、 UE4 ではイッパンに FMath
に定義される数学関数を使用する。一般的にゲーム用途では FMath
の実装で十分であるが、用途や製品の設計によっては double
, long double
, 複素数、疎行列などを扱う必要もあるかもしれない。そうした場合には FMath
で不足する機能を <cmath>
の template
実装で補ったり、 Eigen
をプロジェクトへ取り込んだりする必要が生じる。
念の為、整理しておく。
- UE4/C++er は事足りる限りは
FMath
を使う。 long double
などFMath
に定義されないが<cmath>
ならば扱える機能が必要な場合は<cmath>
の実装を使う。- 但し、クロスプラットフォーム展開する製品では実装依存の挙動や実装の処理速度最適化が適当ではない可能性について熟知した上で使用する。
FMath
,<cmath>
では対応が困難な機能は、外部ライブラリーを取り込んで使えば良い。
次項の前に少し <cmath>
と定数とコンパイルオプション -D
相当の渡し方について、ついでに紹介する。
<cmath>
と言えば、ベテランの C++er にはよく知られる _USE_MATH_DEFINES
と C++ 規格外の定数値がある。 _USE_MATH_DEFINES
はユーザーが事前に手作業で定義していなければ UE4 では定義されない。特に Microsoft プラットフォームの C++er はよく把握しているように、 M_PI
や M_E
などの頻出する数学定数の #define
値は定義されない。 M_PI
などの定数にについては UE4 にも独自の定義 PI
などが使用可能なよう定義されるので通常はそちらを使用する。ふだん、これら非標準ながら "事実上一般化" している <cmath>
の定数群に慣れている C++er は注意すると良い。
UE4 で定義される数学的な定数について確認したければ UnrealMathUtility.h Floating point constants を眺めると良い。
さて、コンパイルオプションレベルで _USE_MATH_DEFINES
を定義できないのか、と言えば実はできる。 "{your-project-name}.Build.cs" に次のように記述すれば任意の定義をコンパイルオプション -D
で渡せる。
// ModuleRules を継承した {your-project-name} クラスの ctor 内へ追加する Definitions.Add( "_USE_MATH_DEFINES" );
Definitions
は List<T>
型で実装されている。詳細については ModuleRules.cs Definitions
や VCToolChain.cs を読むと良い。
と、紹介して終わりたいのだが、実は件の UE4 の FMath
の #include
の流れで <math.h>
へ辿り着くのは UE4 プロジェクトのソースコードの翻訳単位よりも前なので、もしこの方法で M_PI
や M_SQRT1_2
を使いたければ、愚直に .cpp で Definitions
またはソース内での #define
を適当に行い #include <math.h>
するのが最も簡単だったりする。
C++er にとって、特に開発中、デバッグ用途で Definitions
はしばしば有用な事があるので覚えておくと良い。
1.9.2. FMath
における Generic Platform 層
FMath
の実装では <cmath>
の薄いラッパーによる実装もあるが、 C++ CRT の数学機能には地味に実装依存な挙動もあり、しばしば UE4 では次項の Generic Platform 層の独自実装(≃ UE4 のサポート対象プラットフォームであれば同様の挙動を示す)も混在すると前項で紹介した。本稿では UE4 が Generic Platform 層として CRT 互換層をラップしたり、独自実装を定義し、 UE4 の展開するプラットフォーム群に対して一般に同じ挙動を速度フォーカスした機能群について std 慣れした C++er にわかりやすいよう比較を交えて整理する。
- Header: Runtime/Core/Public/GenericPlatform/GenericPlatformMath.h
- Class: FGenericPlatformMath
関数名については、おおよそは <cmath>
の sin
に対して UE4 の FMath
では Sin
のように先頭が大文字に変化する程度。
以下に std 慣れした C++er にわかりやすいよう実装の概略を挙げつつ最終的には FMath
となる FGenericPlatformMath
の基礎的な数学関数群を整理する。なお実装の概略はあくまでも概略であり、実装詳細そのものではない。
FGenericPlatformMath 関数 |
一般的に云う効果 | (C++er に優しい) 実装の概略と蛇足 |
---|---|---|
int32 TruncToInt(float F) |
整数キャスト | (int32)F |
float TrucToFloat(float F) |
整数キャスト+浮動小数点数キャスト | (float)(int32)F |
int32 FloorToInt(float F) |
床 | (int32)floorf(F) |
float FloorToFloat(float F) |
床 | floorf(F) |
double FloorToDouble(double F) |
床 | floor(F) |
int32 RoundToInt(float F) |
四捨五入 | (int32)floor(F+0.5f) // C++11 round 不使用 |
float RoundToFloat(float F) |
四捨五入 | floorf(F+0.5f) // C++11 round 不使用 |
double RoundToDouble(double F) |
四捨五入 | floor(F+0.5f) // C++11 round 不使用 |
int32 CeilToInt(float F) |
天井 | (int32)ceilf(F) |
float CeilToFloat(float F) |
天井 | ceilf(F) |
double CeilToDouble(double F) |
天井 | ceil(F) |
float Fractional(float Value) |
符号付き小数部 (+値→[0..1),-値→[-1..0)) |
Value - (float)(int32)Value |
float Frac(float Value) |
小数部([0..1)) | Value - floorf(Value) |
float Modf(const float InValue, float* OutIntPart) |
整数部と小数部への分解 | modff(Invalue, OutIntPart) |
double Modf(const double InValue, double* OutIntPart) |
整数部と小数部への分解 | modf(Invalue, OutIntPart) |
float Exp( float Value ) |
底eの指数 | expf(Value) |
float Exp2( float Value ) |
底2の指数 | powf(2.f, Value) // C++11 exp2 不使用 |
float Loge( float Value ) |
底eの対数 | logf(Value) |
float LogX( float Base, float Value ) |
任意の底の対数 | logf(Value)/logf(Base) |
float Log2( float Value ) |
底2の対数 | logf(Value) * 1.4426950f // C++11 log2 不使用 |
float Fmod(float X, float Y) |
浮動小数点数の剰余 | // fmod 不使用, fabsf 使用 // , 独自のエラー通知あり |
float Sin( float Value ) |
正弦 | sinf(Value) |
float Asin( float Value ) |
安全な値域[-1..+1]へのクランプ付き逆正弦 | asinf( (Value<-1.f) ? -1.f : ((Value<1.f) ? Value : 1.f) ) |
float Sinh(float Value) |
双曲線正弦 | sinhf(Value) |
float Cos( float Value ) |
余弦 | cosf(Value) |
float Acos( float Value ) |
安全な値域[-1..+1]へのクランプ付き逆余弦 | acosf( (Value<-1.f) ? -1.f : ((Value<1.f) ? Value : 1.f) ) |
float Tan( float Value ) |
正接 | tanf(Value) |
float Atan( float Value ) |
逆正接 | atanf(Value) |
float Atan2( float Y, float X ) |
勾配の軸成分の逆正接 | // CRT 実装依存バグ対策と高速化のため独自実装 //, CRT 比で誤差7.15255737e-7 以下 |
float Sqrt( float Value ) |
平方根 | sqrtf(Value) |
float Pow( float A, float B ) |
任意の底の指数 | powf(A, B) |
float InvSqrt( float F ) |
平方根の逆数 | 1.0f/ sqrtf(F) |
float InvSqrtEst( float F ) |
平方根の逆数 | // 高速版のはずだけどこの層の実装では // InvSqrt(F) を呼んでいるだけ |
bool IsNaN( float A ) |
NaN判定 | (((uint32)&A) & 0x7FFFFFFF) > 0x7F800000 // isnan 不使用 |
bool IsFinite( float A ) |
有限判定 | (((uint32)&A) & 0x7F800000) != 0x7F800000 // isfinite 不使用 |
bool IsNegativeFloat(const float& A) |
負判定 | ( ((uint32)&A) >= (uint32)0x80000000 ) // signbit 不使用 |
bool IsNegativeDouble(const double& A) |
負判定 | ( ((uint64)&A) >= (uint64)0x8000000000000000 ) // signbit 不使用 |
int32 Rand() |
プラットフォーム依存の 線形合同法の疑似乱数の生成 |
rand() |
void RandInit(int32 Seed) |
プラットフォーム依存の 線形合同法の系の初期化 |
srand(Seed) |
float FRand() |
プラットフォーム依存の 線形合同法の擬似乱数を元にした 簡易的な非負正規化浮動小数点数 |
Rand() / (float)RAND_MAX |
void SRandInit( int32 Seed ) |
線形合同法を非負正規化浮動小数点数の 直接生成に特殊化した擬似乱数生成器の初期化 |
// 大域変数 GSrandSeed の値を代入するだけ |
int32 GetRandSeed() |
線形合同法を非負正規化浮動小数点数の 直接生成に特殊化した擬似乱数生成器に種値の取得 |
// 大域変数 GSRandSeed の値を返すだけ |
float SRand() |
線形合同法を非負正規化浮動小数点数の 直接生成に特殊化した擬似乱数生成器による乱数生成 |
// GSRandSeed*196314165+907633515のビット列を // floatの非負正規化領域へマスクし // 小数部を取得する擬似乱数生成器 |
uint32 FloorLog2(uint32 Value) |
底が2の対数の2進数床 // 0,1,2,3,4 --> 0,0,1,1,2,2 | if (Value >= 1<<16) { Value >>= 16; pos += 16; } // 的な繰り返しにより算出 |
uint64 FloorLog2_64(uint64 Value) |
底が2の対数の2進数床 // 0,1,2,3,4 --> 0,0,1,1,2,2 | if (Value >= 1ull<<32) { Value >>= 32; pos += 32; } // 的な繰り返しにより算出 |
uint32 CountLeadingZeros(uint32 Value) |
2進数で先頭から連続する0の個数を取得 | if (Value == 0) return 32; return 31 - FloorLog2( Value ) |
uint64 CountLeadingZeros64(uint64 Value) |
2進数で先頭から連続する0の個数を取得 | if (Value == 0) return 64; return 63 - FloorLog2_64( Value ) |
uint32 CountTrailingZeros(uint32 Value) |
2進数で末尾から連続する0の個数を取得 | // 右シフトを繰り返し末尾1ビットを検出 |
uint32 CeilLogTwo( uint32 Arg ) |
底が2の対数の2進数天井 // 0,1,2,3,4,5 --> 0,0,1,2,2,3 | (32 - CountLeadingZeros(Arg - 1)) & (~(((int32)(CountLeadingZeros(Arg) << 26)) >> 31)) |
uint64 CeilLogTwo64( uint64 Arg ) |
底が2の対数の2進数天井 // 0,1,2,3,4,5 --> 0,0,1,2,2,3 | (64 - CountLeadingZeros(Arg - 1)) & (~(((int32)(CountLeadingZeros(Arg) << 57)) >> 63)) |
uint32 RoundUpToPowerOfTwo(uint32 Arg) |
底が2の指数の2進数天井 // 0,1,2,3,4,5 --> 1,1,2,4,4,8 | 1 << CeilLogTwo(Arg) |
uint32 MortonCode2( uint32 x ) |
2次元のモートン符号化 | // ビットの &, <<, ^ により実装 |
uint32 ReverseMortonCode2( uint32 x ) |
2次元のモートン復号化 | // ビットの &, >>, ^ により実装 |
uint32 MortonCode3( uint32 x ) |
3次元のモートン符号化 | // ビットの &, <<, ^ により実装 |
uint32 ReverseMortonCode3( uint32 x ) |
3次元のモートン復号化 | // ビットの &, >>, ^ により実装 |
float FloatSelect( float Comparand, float ValueGEZero, float ValueLTZero ) |
1つの比較対象を0以上か判定し他の2つの値の何れかを選択 | v >= 0.f ? a : b |
double FloatSelect( double Comparand, double ValueGEZero, double ValueLTZero ) |
1つの比較対象を0以上か判定し他の2つの値の何れかを選択 | v >= 0. ? a : b |
T Abs( const T A ) |
絶対値 | v >= 0 ? v : -v // std::abs 不使用 |
T Sign( const T A ) |
正、負、零の3値へ正規化する | v > 0 ? 1 : v < 0 ? -1 : 0 |
T Max( const T A, const T B ) |
2つの値から大きい方を選択 | a >= b ? a : b // std::max 不使用 |
T Min( const T A, const T B ) |
2つの値から小さい方を選択 | a <= b ? a : b // std::min 不使用 |
T Min(const TArray<T>& Values, int32* MinIndex = NULL) |
配列から最も小さい値とインデックスを取得 | // 単純に for で全探索する実装 |
T Max(const TArray<T>& Values, int32* MaxIndex = NULL) |
配列から最も大きい値とインデックスを取得 | // 単純に for で全探索する実装 |
int32 CountBits(uint64 Bits) |
ハミング重み | // std::bitset::count 不使用 |
float Abs( const float A ) |
絶対値 | // std::fabsf 使用 |
以上の FMath
の Generic Platform 層の実装では、ほとんどの実装で std の <cmath>
の薄いラッパーとしたり、実装詳細へ応用しないよう実装されている。これは多くはポリシーによるものだが、一部 "蛇足" として表中に記載したように一般的な実装よりも速度最適化された実装とするためのもの(例: Atan2
)、実用上の安全性を向上したもの(例: Asin
)もある。
UE4/C++er は特別の事情が無ければ、基礎的な数学関数については <cmath>
よりも FMath
を用いると良い。
なお、 std にあるが UE4 FMath
に無い数学関数は以下の通り:
- 双曲線; (
sinh
だけはFMath
にもある)cosh
,tanh
asinh
,acosh
,atanh
- 指数, 対数
expm1
log10
,log1p
- 仮数, 指数
ldexp
,frexp
ilogb
,logb
scalbn
,scalbln
- 冪乗, 冪根, 絶対値
cbrt
hypot
- 誤差, γ
erf
,erfc
tgamma
,lgamma
- C++17 で追加される高度な数学関数群
assoc_laguerre
,assoc_legendre
,beta
,comp_ellint_1
,comp_ellint_2
,comp_ellint_3
,cyl_bessel_i
,cyl_bessel_j
,cyl_bessel_k
,cyl_neumann
,ellint_1
,ellint_2
,ellint_3
,expint
,hermite
,laguerre
,legendre
,riemann_zeta
,sph_bessel
,sph_legendre
,sph_neumann
- 剰余
reminder
,remquo
- 浮動小数点数
nan
,nanf
,nanl
fpclassify
isinf
,isnormal
- 融合積和演算
fma
これらが必要な場合は既存実装を組み立てるか、自作するか、あるいは素直に std 版の実装を使えばよい。
逆に、 std に無いが FMath
に Generic Platform 層として組み込まれている機能は次の通り:
- 冪根
InvSqrt
- 2進数演算
FloorLog2
,FloorLog2_64
CountLeadingZeros
,CountLeadingZeros64
,CountTrailingZeros
CeilLogTwo
,CeilLogTwo64
,RoundUpToPowerOfTwo
CountBits
- 浮動小数点数
IsNegativeFloat
,IsNegativeDouble
FloatSelect
Sign
- 最大, 最小
T Min( TArray<T>, int32* )
,T Max( TArray<T>, int32* )
- 擬似乱数; (
Rand
,RandInit
は std のrand
,srand
が対応。)FRand
SRandInit
,GetRandSeed
,SRand
- モートン符号
MortonCode2
,ReverseMortonCode2
MortonCode3
,ReverseMortonCode3
地味に2進数処理を中心に "チョットベンリ" 的な実装がやや充実している。
FMath
の擬似乱数機能は Rand
系にせよ Srand
系にせよ、特に優れた利点は無いので、一般的な C++er は化石を温かく見守る気持ちで眺めつつ、 std の <random>
実装を使えば良い。
また、生きてきた分野にもよるが、モートン符号については知らない C++er も少なくないかもしれない。そこで、本記事の執筆と新人くんへ解説にも使う手前、日本語版 Wikipedia に Z階数曲線 として English 版の解説を基に執筆したので興味があれば確認されたい。 MortonCode2
は入力値を 2 次元用のモートンコード、すなわち 0-Bit 目を含めた偶数ビットへ展開、 ReverseMortonCode2
はその逆変換を行い、 MortonCode3
, ReverseMortonCode3
は 3 次元用に展開するビットが 3 bit ごとになったバージョンでそれぞれ最大で 65,536 * 65,536 分割された 2 次元空間または 1,024 * 1,024 * 1,024 分割された 3 次元空間に対して使用できる。
1.9.3. FMath
における Platform Native 層
Generic Platform 層の上に、 Platform Native 層があり、プラットフォームごとに Generic Platform 層よりも高効率な実装が可能な場合にはより高効率な実装が使われるよう FGenericPlatformMath
型から継承し、例えば Windows プラットフォームであれば FWindowsPlatformMath
型が定義される。
この仕組みにより、最終的に FMath
で使用される数学関数は、各プラットフォームで可能な限り速度最適化された実装が採用されるよう設計されている。・・・設計上は。
と、いうのも実は開発リソースと優先順序のためか、 UE4 "Runtime/Core/Public/{platform}/{platform}(Platform)Math.h" を見比べてみると、 Android, iOS, HTML5 向けの FMath
の基礎的な実装についてのプラットフォーム最適化は UE-4.18 の執筆現在、残念ながら実質無いに等しい。
そこで、本稿では紹介程度に留める意味で Windows プラットフォーム向けの最適化実装 FWindowsPlatformMath
を例に具体的にどのようなプラットフォーム最適化実装が行われているのかやんわりと整理するに留める。同様の最適化はいわゆる PC 系プラットフォームに対して定義があり、 Windows のほかには Linux, Mac に対して同等の最適化が施される。
Windows, Linux, Mac の F{PlatformName}PlatformMath
の実装では、 "Public/Core/Math/UnrealPlatformMathSSE.h" から InvSqrt
と InvSqrtEst
の最適化版が読み込まれる。 Generic 層では "名ばかりの高速版" だった InvSqrtEst
もここで意味を持つようになる。
この機能は一般に C++er が特別の需要が無く実装を書くならば 1 / std::sqrt( x )
となる。実装は著者の知る限りでは浮動小数点数の型に応じて必要な精度を満たすようニュートン法により実装される。
ここで、C++ 標準から x64 の CPU 命令へ視点を移すと、平方根は _mm_sqrt_ss
系、 平方根の逆数は _mm_rsqrt_ss
系の SSE 命令で得られる。しかし、これら SSE 命令の平方根関数及び平方根逆数関数の群は 12 bits 程度の精度の結果しか得られない。つまり、 10 進数の実数にしてせいぜい3桁程度しか信頼できる値を得られない。( _mm_rsqrt_ss
命令の相対誤差は |相対誤差| ≤ 1.5 * pow(2,-12)
。 詳細は Intel® 64 and IA-32 Architectures Software Developer Manuals を参照すると良い。)
そこで、より精度の高い平方根の逆数が必要な場合には _mm_rsqrt_ss
の結果を基に Newton-Raphson 法により精度を向上する手法が用いられる。 UE4 における UnrealPlatformMathSSE
で最適化される InvSqrt
と InvSqrtEst
も _mm_rsqrt_ss
+ Newton-Raphson 法により実装されている。
// ===== 以下は InvSqrt, InvSqrtEst 共通 ===== // 導関数と Newton-Raphson 法のイテレーションの展開 // v^-0.5 = x // => x^2 = v^-1 // => 1/(x^2) = v // => F(x) = x^-2 - v // F'(x) = -2x^-3 // // x1 = x0 - F(x0)/F'(x0) // => x1 = x0 + 0.5 * (x0^-2 - Vec) * x0^3 // => x1 = x0 + 0.5 * (x0 - Vec * x0^3) // => x1 = x0 + x0 * (0.5 - 0.5 * Vec * x0^2) // Step 0: 準備 const __m128 fOneHalf = _mm_set_ss(0.5f); __m128 Y0, X0, X1, X2, FOver2; float temp; // Step 1: _rsqrt_ss により 12 bits 精度の平方根の逆数を得る Y0 = _mm_set_ss(F); X0 = _mm_rsqrt_ss(Y0); // 1/sqrt estimate (12 bits) FOver2 = _mm_mul_ss(Y0, fOneHalf); // Step 2.1: Newton-Raphson 法による1回目のイテレーション X1 = _mm_mul_ss(X0, X0); X1 = _mm_sub_ss(fOneHalf, _mm_mul_ss(FOver2, X1)); X1 = _mm_add_ss(X0, _mm_mul_ss(X0, X1)); // ===== 以下は InvSqrt のみ ===== // Step 2.2: Newton-Raphson 法による2回目のイテレーション X2 = _mm_mul_ss(X1, X1); X2 = _mm_sub_ss(fOneHalf, _mm_mul_ss(FOver2, X2)); X2 = _mm_add_ss(X1, _mm_mul_ss(X1, X2));
感覚として掴みやすいようより具体的な実行速度と10進数における有効桁数の比較として整理する:
出処 | method | #/sec (O0) | #/sec (O3) | vs. (O3) | 有効桁数 |
---|---|---|---|---|---|
std | 1.0f / sqrt( (float) count ) | 17,043,072 | 23,573,025 | 1.000 | 7 |
UE4 | UnrealPlatformMathSSE::InvSqrt( (float)count ) | 10,777,476 | 22,967,873 | 0.974 | 7 |
UE4 | UnrealPlatformMathSSE::InvSqrtEst( (float)count ) | 13,050,090 | 23,568,536 | 1.000 | 6 |
SSE | rsqrt | 16,812,451 | 24,343,710 | 1.033 | 2 |
実行速度の計測は uint64
の counter
を 0
からインクリメントしつつ、その counter
値を各種の平方根逆数関数へ入れ、結果を float
の sum
へ逐次 +=
するループを std::chrono::steady_clock
で 20 秒間行い、 count / 20
により秒あたりの処理回数とした。
また、以下に各種の平方根逆数関数で 1/√2
を float
で計算した結果について、 32-Bit の全ビットと10進数の実数としての値を std::wstringstream
へ << std::fixed << std::setprecision(10)
の上で operator<<
した結果を示す:
出処 | method | 32-Bit | 数値(10桁) |
---|---|---|---|
std | sqrt | 00111111001101010000010011110011 | 0.7071067691 |
UE4 | UnrealPlatformMathSSE::InvSqrt | 00111111001101001111100000000000 | 0.7069091797 |
UE4 | UnrealPlatformMathSSE::InvSqrtEst | 00111111001101010000010011110011 | 0.7071067691 |
SSE | rsqrt | 00111111001101010000010011110010 | 0.7071067095 |
出処を SSE としている rsqrt
は InvSqrt
の Newton-Raphson 法なしの結果。
今回の std::sqrt
は MSVC++ 2017 の cl.exe-19.11.25547 for x64 の結果だが、恐らく最終的には UE4 の SSE 手書き最適化版と同様の機械語が生成されていると思われる。本記事の主目的はその詳細ではないため…というかアドベンカレンダーの期日の都合もあり割愛するが、興味があれば逆アセンブルして読んで見ると面白そうだ。
UE4 プラットフォーム最適化版の InvSqrt
と InvSqrtEst
及び rsqrt
については計測上の数値としては一応程度に有意な差が観測された。また、精度の問題も 1/√2
の結果から実数値としての感覚としても例示した。
プラットフォーム最適化と言いながらも少々歯切れが悪くなるが O0, O3 の速度計測結果も考慮するに、少なくとも MSVC++-19 で翻訳する限りにおいては std 実装に留めるのが最善だったようである。機会があれば詳細について機械語レベルで調べたい。
なお、蛇足として、 float
値の 32-Bit の UE4 コードでの出力は次の様に実装する:
float x = 1 / std::sqrt( 2.0f ); std::bitset<32> b( *(int32*)&x ); // reinterpret_cast を使いたければ使えば良い std::wstringstream s; s << std::fixed << std::setprecision( 10 ) << b; UE_LOG( LogTemp, Log, TEXT( "%s" ), s.str().c_str() );
さて、ここからようやく FWindowsPlatformMath
の本題に入る。最適化の多くは SSE 系の命令による。簡単な例では TructToInt
の実装が _mmcvtt_ss2si( _mm_set_ss( x ) )
になるなど。
以下に FWindowsPlatformMath
で実装が最適化される関数群について、 GenericPlatformMath
版との速度変化の計測とともに整理する:
関数 | Generic(O3) #/sec | Windows(O3) #/sec | std(O3) #/sec |
---|---|---|---|
TruncToInt | 23,696,272 | 24,095,555 | |
TruncToFloat | 25,038,371 | 24,842,574 | 22,460,595 |
RoundToInt | 24,918,013 | 24,476,462 | |
RoundToFloat | 24,955,395 | 24,732,947 | 20,528,517 |
FloorToInt | 25,086,680 | 24,391,177 | |
FloorToFloat | 24,184,578 | 23,300,533 | 25,007,228 |
CeilToInt | 25,085,954 | 24,233,785 | |
CeilToFloat | 25,134,920 | 25,039,425 | 25,082,468 |
IsNaN | 24,950,199 | 23,871,049 | 23,541,114 |
IsFinite | 24,947,065 | 23,830,000 | 24,016,180 |
InvSqrt | 24,405,314 | 24,278,375 | |
InvSqrtEst | 23,083,880 | 23,469,950 | |
FloorLog2 | 25,135,630 | 24,765,317 | |
CountLeadingZeros | 24,110,845 | 21,307,304 | |
CountLeadingZeros64 | 24,340,359 | 24,123,377 | |
CountTrailingZeros | 23,178,912 | 24,074,612 | |
CeilLogTwo | 24,430,826 | 24,274,930 | |
RoundUpToPowerOfTwo | 23,317,767 | 24,337,332 | |
CeilLogTwo64 | 23,869,462 | 20,324,985 | |
CountBits | 24,806,175 | 23,414,599 |
これも厳密な計測ではないためであくまでも参考値程度ではあるが、これらもまた期待したほど Platform 最適化がうまく実装できているとは言えない結果が得られた。特に Generic 実装の方が翻訳後の機械語レベルでの最適化結果が明らかに良好な傾向のある関数もあり、 UE4 エンジン開発者たちには Android, iOS, HTML5 あるいは Switch や arm 系プロセッサーの Neon も意識した最適化などもっと大真面目にこの分野にも取り組んで貰いたい。
さて、やや執筆前の予想とは異なる結果に少々の困惑が著者にもあるものの、一般的な C++er としてこの Platform Native 層は、 UE4 の FMath
の階層として一応は存在している、という程度に知識として把握しておく程度で十分だろう。
(はてなブログの記事あたりの容量制限のため続き §1.9.4. 以降は次の記事でどうぞ→http://usagi.hatenablog.jp/entry/2017/12/01/ac_ue4_2_p3)