2009/10/24
Direct3D ファントムダストのオブジェクトスペースノーマルマップ
また今更な古い話なので興味ない方は無視してください。(前回)
当時 CG WORLD の 2004 年 9 月号を見た人から、キャラに object space (OS) の
ノーマルマップを使っているのはなぜかと良く聞かれることがありました。
それを思い出したのでちょっと書いてみます。
●理由
Phantom Dust は、シーン全部にほぼノーマルマップまたは同等の重いシェーダーを
適用しています。
Xbox は Shader Model 1.x 世代なので、Pixel Shader は 9~12bit の低精度な
固定小数だし 8 + 4 stage という命令数制限もありました。
この制限の中に必要な機能を詰め込むにはかなり工夫が必要です。
逆に頂点シェーダー側は浮動小数演算だし命令スロット数も比較的余裕はあります。
でも頂点演算はタイトです。シェーダーが 1命令 (0.5 cycle) 増えただけで描画速度に
大きく響きます。
PixelShader
・命令数制限、演算精度が厳しいので、必要な機能を入れるだけでぎりぎり。
VertexShader
・描画速度にかなり影響が生じる。機能的には十分だが速度的には余裕がない。
キャラクタ用シェーダーの頂点は、スキニング処理が入るためただでさえ負荷が高く、
頂点データも index と weight の分だけ多くなります。
・頂点のデータ量を減らしたい
・演算量を減らして高速化したい。
よって最適化のために、キャラクタ用シェーダーではオブジェクトスペースのノーマル
マップを使っています。
速度だけでなくメモリを大量に消費することもノーマルマップでは大問題でした。
●計算
オブジェクトスペースで作られたノーマルマップは、シェイプの変形に柔軟に追従する
ことが出来ません。
そのため、法線をテクスチャに焼き込んだ形状が保存されない Maya 等のツール上では、
オブジェクトスペースのノーマルマップではアニメーションすることが不可能となります。
しかしながら焼き込んだ形状が保存されていて、かつ変形したジオメトリとの差分が求まるならば、
オブジェクトスペースのノーマルマップから変形後の法線を導き出すことが出来ます。
ボーンによるアニメーションでも全く同じことです。基準となるバインドポーズを持っていて、
ツールから出力されるメッシュはこの形状を保ったスタティックなものです。
スキニングの演算自体が、骨の移動に従った基準形状からの各頂点の変化量を求めていることに
なります。このマトリクスがそのまま使えます。
Phantom Dust では光源ベクトルを逆変換しているので、頂点で求めた Local To World
Matrix の逆行列 (3x3) を光源に掛けます。
これが各頂点毎の Local 空間 (OS) なので、サンプリングされたオブジェクトスペースの
ノーマルマップを使ったライティングを行えます。
基本姿勢の OS を Os0 とすると、光源を World → Os → Os0 と変換していることに
なります。
頂点に Tangent Space の Matrix を埋め込まなくて済むので、こちらの方がデータ量として
かなりお得です。Tanget Space への変換分だけ演算負荷が減るし、頂点サイズの縮小も
そのまま高速化に繋がります。
ただしオブジェクトスペースでノーマルマップを作った場合は、以後絶対に基本となる
ポーズを変えてはいけません。Export 時に、ツール上でうっかり触ってずれてしまった、
とか許されなくなるわけです。
●ムービーレンダリング
プロジェクト終盤で困ったのがムービーのレンダリングです。
ゲーム内のキャラデータを使ってムービーシーンを作る必要があったのですが、次の理由に
よりそれが困難でした。
(1) 法線マップのレンダリング方法
(2) アニメーション変形
(1) 当時の Maya (4.5 あたり) には法線マップ用のレンダリング機能がなかったので、
そのままではレンダリングすることが出来ません。シェーディングノードを複雑に組み合わせて
Matrix 演算を行うことも出来ますが、いろいろ無理があります。
(2) 上でも書いたとおり、オブジェクトスペースのノーマルマップはツール上だと
元の形状からの変化量が求まらないのでアニメーション変形に対応することができません。
結局ムービーレンダリングのために専用のプラグインを作成しました。
プロジェクト終盤ではすでに RADEON 9700 が出ていたので、RADEON + ShaderModel 2.0
相当の OpenGL 機能を使っています。
OpenGL + ARB_vertex_program + ARB_fragment_program を使い、
Maya ビューポートへのリアルタイムプレビュー +
Maya ハードウエアレンダーバッファへのレンダリングを行っています。
確か条件分けは 3通り。
1. TS Normal map ではそのまま何もせずレンダリング可能。アニメーションも OK。
2. OS Normal map でも変形しない物ならそのままレンダリングします。
3. OS かつアニメーションを行う場合、プラグインに対して事前に基本形状を登録する
必要あり。
3. では登録したメッシュを内部で記録して、Object Space のレンダリング時に参照しています。
これは Transform node (Matrix) ではなくメッシュ自体を丸ごと保存しています。
そのため実機と違い、ボーンアニメーションだけでなくシェイプアニメーションにも対応
出来るようになっています。
これが可能なのは OS だけど各頂点に Tangent Space 用の Matrix を埋め込んでいるから。
変形後の頂点から Tangent Space の Matrix が求まるので、そこから逆変換すれば
OS のノーマルマップから得た法線を TS に持って行くことが出来ます。
つまり頂点が持っている Tangent Space 変換 Matrix は 2つです。
Os0 は基本形状の Object Space 、Os はアニメーション後の Object Space とすると
サンプリングした法線は Matrix1 で逆変換後 Matrix2 に適用すればよいことになります。
●背景
背景は Tangent Space (TS) です。地面や壁などバンプマップとしてタイリングするし
同じノーマルマップテクスチャを何度も使い回すからです。
貼られる面も任意となります。
●その後
あくまで昔の話です。キャラデータに OS のノーマルマップを使用したのはこのときだけ。
変形があるとデータの扱いが難しいし、その後のシェーダーでは PixelShader もかなり
高機能化されたので、ここまで考える必要がなくなりました。
Shader Model 2.0~3.0 の描画パイプラインの設計では、全く区別のない両対応をしたものの
結局キャラにも Tangent Space を使っています。
ただしツール内で生成された Tangent Space には互換性が無いので、最終的な
レンダリングプログラムと条件を一致させなければなりません。
結局自前のツールでデータを生成することになります。
Shader Model 4.0 以降は TS のみの対応としました。
シェーダーの基本機能として当たり前に使えるけど、その比重はさらに下がっています。
すでに Direct3D 11 の時代、Shader Model 5.0 ならなおさらでしょう。
関連エントリ
・Direct3D ファントムダストと破壊の穴
・3D一般 ノーマルマップの互換性問題(2) UVに潜む罠
当時 CG WORLD の 2004 年 9 月号を見た人から、キャラに object space (OS) の
ノーマルマップを使っているのはなぜかと良く聞かれることがありました。
それを思い出したのでちょっと書いてみます。
●理由
Phantom Dust は、シーン全部にほぼノーマルマップまたは同等の重いシェーダーを
適用しています。
Xbox は Shader Model 1.x 世代なので、Pixel Shader は 9~12bit の低精度な
固定小数だし 8 + 4 stage という命令数制限もありました。
この制限の中に必要な機能を詰め込むにはかなり工夫が必要です。
逆に頂点シェーダー側は浮動小数演算だし命令スロット数も比較的余裕はあります。
でも頂点演算はタイトです。シェーダーが 1命令 (0.5 cycle) 増えただけで描画速度に
大きく響きます。
PixelShader
・命令数制限、演算精度が厳しいので、必要な機能を入れるだけでぎりぎり。
VertexShader
・描画速度にかなり影響が生じる。機能的には十分だが速度的には余裕がない。
キャラクタ用シェーダーの頂点は、スキニング処理が入るためただでさえ負荷が高く、
頂点データも index と weight の分だけ多くなります。
・頂点のデータ量を減らしたい
・演算量を減らして高速化したい。
よって最適化のために、キャラクタ用シェーダーではオブジェクトスペースのノーマル
マップを使っています。
速度だけでなくメモリを大量に消費することもノーマルマップでは大問題でした。
●計算
オブジェクトスペースで作られたノーマルマップは、シェイプの変形に柔軟に追従する
ことが出来ません。
そのため、法線をテクスチャに焼き込んだ形状が保存されない Maya 等のツール上では、
オブジェクトスペースのノーマルマップではアニメーションすることが不可能となります。
しかしながら焼き込んだ形状が保存されていて、かつ変形したジオメトリとの差分が求まるならば、
オブジェクトスペースのノーマルマップから変形後の法線を導き出すことが出来ます。
ボーンによるアニメーションでも全く同じことです。基準となるバインドポーズを持っていて、
ツールから出力されるメッシュはこの形状を保ったスタティックなものです。
スキニングの演算自体が、骨の移動に従った基準形状からの各頂点の変化量を求めていることに
なります。このマトリクスがそのまま使えます。
Blend された Local to World matrix をそのまま使うだけ
Phantom Dust では光源ベクトルを逆変換しているので、頂点で求めた Local To World
Matrix の逆行列 (3x3) を光源に掛けます。
これが各頂点毎の Local 空間 (OS) なので、サンプリングされたオブジェクトスペースの
ノーマルマップを使ったライティングを行えます。
基本姿勢の OS を Os0 とすると、光源を World → Os → Os0 と変換していることに
なります。
頂点に Tangent Space の Matrix を埋め込まなくて済むので、こちらの方がデータ量として
かなりお得です。Tanget Space への変換分だけ演算負荷が減るし、頂点サイズの縮小も
そのまま高速化に繋がります。
ただしオブジェクトスペースでノーマルマップを作った場合は、以後絶対に基本となる
ポーズを変えてはいけません。Export 時に、ツール上でうっかり触ってずれてしまった、
とか許されなくなるわけです。
●ムービーレンダリング
プロジェクト終盤で困ったのがムービーのレンダリングです。
ゲーム内のキャラデータを使ってムービーシーンを作る必要があったのですが、次の理由に
よりそれが困難でした。
(1) 法線マップのレンダリング方法
(2) アニメーション変形
(1) 当時の Maya (4.5 あたり) には法線マップ用のレンダリング機能がなかったので、
そのままではレンダリングすることが出来ません。シェーディングノードを複雑に組み合わせて
Matrix 演算を行うことも出来ますが、いろいろ無理があります。
(2) 上でも書いたとおり、オブジェクトスペースのノーマルマップはツール上だと
元の形状からの変化量が求まらないのでアニメーション変形に対応することができません。
結局ムービーレンダリングのために専用のプラグインを作成しました。
プロジェクト終盤ではすでに RADEON 9700 が出ていたので、RADEON + ShaderModel 2.0
相当の OpenGL 機能を使っています。
OpenGL + ARB_vertex_program + ARB_fragment_program を使い、
Maya ビューポートへのリアルタイムプレビュー +
Maya ハードウエアレンダーバッファへのレンダリングを行っています。
確か条件分けは 3通り。
1. TS Normal map ではそのまま何もせずレンダリング可能。アニメーションも OK。
2. OS Normal map でも変形しない物ならそのままレンダリングします。
3. OS かつアニメーションを行う場合、プラグインに対して事前に基本形状を登録する
必要あり。
3. では登録したメッシュを内部で記録して、Object Space のレンダリング時に参照しています。
これは Transform node (Matrix) ではなくメッシュ自体を丸ごと保存しています。
そのため実機と違い、ボーンアニメーションだけでなくシェイプアニメーションにも対応
出来るようになっています。
これが可能なのは OS だけど各頂点に Tangent Space 用の Matrix を埋め込んでいるから。
変形後の頂点から Tangent Space の Matrix が求まるので、そこから逆変換すれば
OS のノーマルマップから得た法線を TS に持って行くことが出来ます。
つまり頂点が持っている Tangent Space 変換 Matrix は 2つです。
Matrix1: Ts → Os0 Matrix2: Ts → Os
Os0 は基本形状の Object Space 、Os はアニメーション後の Object Space とすると
サンプリングした法線は Matrix1 で逆変換後 Matrix2 に適用すればよいことになります。
●背景
背景は Tangent Space (TS) です。地面や壁などバンプマップとしてタイリングするし
同じノーマルマップテクスチャを何度も使い回すからです。
貼られる面も任意となります。
●その後
あくまで昔の話です。キャラデータに OS のノーマルマップを使用したのはこのときだけ。
変形があるとデータの扱いが難しいし、その後のシェーダーでは PixelShader もかなり
高機能化されたので、ここまで考える必要がなくなりました。
Shader Model 2.0~3.0 の描画パイプラインの設計では、全く区別のない両対応をしたものの
結局キャラにも Tangent Space を使っています。
ただしツール内で生成された Tangent Space には互換性が無いので、最終的な
レンダリングプログラムと条件を一致させなければなりません。
結局自前のツールでデータを生成することになります。
Shader Model 4.0 以降は TS のみの対応としました。
シェーダーの基本機能として当たり前に使えるけど、その比重はさらに下がっています。
すでに Direct3D 11 の時代、Shader Model 5.0 ならなおさらでしょう。
関連エントリ
・Direct3D ファントムダストと破壊の穴
・3D一般 ノーマルマップの互換性問題(2) UVに潜む罠
2009/10/15
ATI Stream SDK v2.0 RADEON HD 5850 と OpenCL
ATI Stream SDK v2.0 beta4 がリリースされ、OpenCL も GPU で動作するようになりました。
・ATI Stream Software Development Kit (SDK) v2.0 Beta Program
以前 こちら で書いたとおり、これまでは OpenCL の対象デバイスは CPU のみ列挙され、
GPU が使われることはありませんでした。
今回のアップデートでは GPU に対応しており、やっと本来の姿になったといえます。
サポート対象は基本的に Radeon 4000/5000 シリーズ。
Windows7 にもインストールできます。
OpenCL はすでに MacOSX や GeForce で動いており、GPU 対応もあまり目新しいものでは
無いかもしれません。でも HD 5000 があれば Direct3D 11, Compute Shader 5.0,
OpenCL 1.0 と新しい API を全部試すことが出来るので、ボードの抜き差し回数が減りそうです。
唯一 OpenGL 3.2 対応はまだのようです。
OpenCL 対応 ATI Stream SDK を使うにはドライバのアップデートも必要です。
RADEON HD 5870/5850 のドライバは 8.66 から 8.67 に更新されました。
下記は 以前 と同じプログラムで表示したもの。
DEVICE の列挙は 2つあり、上は CPU です。下が RADEON HD 5850 です。
COMPUTE UNIT が 18 個とありますが、5870 ではここが 20 でした。
関連エントリ
・ATI Stream SDK v2.0 beta4 OpenCL
・DirectX 11 / Direct3D 11 と RADEON HD 5870 の caps
・OpenCL の vector 型
・ATI Stream SDK 2.0 beta と OpenCL
・ATI Stream Software Development Kit (SDK) v2.0 Beta Program
以前 こちら で書いたとおり、これまでは OpenCL の対象デバイスは CPU のみ列挙され、
GPU が使われることはありませんでした。
今回のアップデートでは GPU に対応しており、やっと本来の姿になったといえます。
サポート対象は基本的に Radeon 4000/5000 シリーズ。
Windows7 にもインストールできます。
OpenCL はすでに MacOSX や GeForce で動いており、GPU 対応もあまり目新しいものでは
無いかもしれません。でも HD 5000 があれば Direct3D 11, Compute Shader 5.0,
OpenCL 1.0 と新しい API を全部試すことが出来るので、ボードの抜き差し回数が減りそうです。
唯一 OpenGL 3.2 対応はまだのようです。
OpenCL 対応 ATI Stream SDK を使うにはドライバのアップデートも必要です。
RADEON HD 5870/5850 のドライバは 8.66 から 8.67 に更新されました。
下記は 以前 と同じプログラムで表示したもの。
DEVICE の列挙は 2つあり、上は CPU です。下が RADEON HD 5850 です。
COMPUTE UNIT が 18 個とありますが、5870 ではここが 20 でした。
CL_PLATFORM_PROFILE = FULL_PROFILE CL_PLATFORM_VERSION = OpenCL 1.0 ATI-Stream-v2.0-beta4 CL_PLATFORM_NAME = ATI Stream CL_PLATFORM_VENDOR = Advanced Micro Devices, Inc. CL_PLATFORM_EXTENSIONS = // Core i7 920 CL_DEVICE_TYPE = 2 // CL_DEVICE_TYPE_CPU CL_DEVICE_VENDOR_ID = 4098 CL_DEVICE_MAX_COMPUTE_UNITS = 8 CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS = 3 CL_DEVICE_MAX_WORK_ITEM_SIZES = 1024 1024 1024 CL_DEVICE_MAX_WORK_GROUP_SIZE = 1024 CL_DEVICE_EXECUTION_CAPABILITIES = 1 // RADEON HD 5850 CL_DEVICE_TYPE = 4 // CL_DEVICE_TYPE_GPU CL_DEVICE_VENDOR_ID = 4098 CL_DEVICE_MAX_COMPUTE_UNITS = 18 // 5870 では 20 CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS = 3 CL_DEVICE_MAX_WORK_ITEM_SIZES = 256 256 256 CL_DEVICE_MAX_WORK_GROUP_SIZE = 256 CL_DEVICE_EXECUTION_CAPABILITIES = 1
関連エントリ
・ATI Stream SDK v2.0 beta4 OpenCL
・DirectX 11 / Direct3D 11 と RADEON HD 5870 の caps
・OpenCL の vector 型
・ATI Stream SDK 2.0 beta と OpenCL
2009/10/14
ATI Stream SDK v2.0 beta4 OpenCL
GPU に対応したようです。
RADEON 58 だけでなく 4000 系も含まれています。
・ATI Stream Software Development Kit (SDK) v2.0 Beta Program
RADEON 58 だけでなく 4000 系も含まれています。
・ATI Stream Software Development Kit (SDK) v2.0 Beta Program
2009/10/09
Direct3D 11 / DirectX 11 UAV を使った書き込みとブレンド
前回説明したとおり、Direct3D 11 の Pixel Shader 5.0 は UAV (Unordered Access View)
を使った書き込みが出来ます。
UAV を RTV (Render Target View) の代わりに用いる利点は次の通り。
(1) 書き込み座標を任意に指定できる
(2) 読み込める
本来 Render Target に書き込む座標はラスタライズ時に決定します。
このスクリーン座標値は Shader Model 5.0 (ps_5_0) の場合 SV_Position で受け取る
ことができます。
UAV は書き込むアドレスを直接指定できるため、ラスタライズ座標以外の場所に点を打てるし
一度に複数の点を書き込むことも可能です。
Geometry Shader がプリミティブの単位で座標をいじれたり増やしたり出来るのに似ています。
例えばスクリーン座標で左右反転したり、画像を複製したり。

↑これは実際に 1枚のプリミティブを書き込んでいますが、Pixel Shader 内で複製して
100 ピクセル離れた場所にも同時に書き込んでいます。
わかりにくいけど、半透明の背景が同じものになっています。
(2) は現在書き込んでいるフレームバッファの値を事前に読み取って演算出来ることを
意味しています。うまく活用すれば Alpha Blend を自由にシェーダーでプログラム
出来るかもしれません。
結果だけ述べると、さすがに少々無理があったようです。

↑ちらつき
この 4枚のポリゴンは一度の Draw() で描画しています。
Reference Device では期待通り動きますが、RADEON HD 5870 の場合は自分自身との
重なりなど、直前に書き込んだピクセルの値が反映される場合とされない場合があります。
おそらく書き込みバッファがフラッシュされる前に読み込んでいるのだと思います。
プリミティブが大きくて一度の描画面積が大きい場合はおそらくバッファ容量を超えているため
うまく動いているように見えます。

↑自分自身との合成でもうまくいくシーン
以下実際に試したプログラムと解説。
フレームバッファ用のテクスチャを DXGI_FORMAT_R32_FLOAT で作っておきます。
SRV, RTV, UAV 全部作ります。
R8G8B8 のカラー値を変換して 32bit 整数値にして R32_FLOAT に書き込んでいます。
asfloat()/asint() を使っているため実質 R32_UINT でも同じです。
モデルデータを描画する場合
1. フレームバッファの値を読み込む
2. 自前でブレンド計算する
3. UAV に書き込む
最後に R32_FLOAT で書き込まれたフレームバッファを RGB に戻して描画します。
モデルを描画するシェーダー。四角形 1枚のみ。
draw.hlsl の続き
C言語側。リソースの解放、BG の描画は省略しています。
CreateShader(), LoadTexture() の中身も略。
関連エントリ
・Direct3D 11 / DirectX 11 UnorderedAccessView と RenderTarget の関係
・DirectX 11 / Direct3D 11 と RADEON HD 5870 の caps
・Direct3D11/DirectX11 (7) テセレータの流れの基本部分
・その他 Direct3D 11 関連
を使った書き込みが出来ます。
UAV を RTV (Render Target View) の代わりに用いる利点は次の通り。
(1) 書き込み座標を任意に指定できる
(2) 読み込める
本来 Render Target に書き込む座標はラスタライズ時に決定します。
このスクリーン座標値は Shader Model 5.0 (ps_5_0) の場合 SV_Position で受け取る
ことができます。
UAV は書き込むアドレスを直接指定できるため、ラスタライズ座標以外の場所に点を打てるし
一度に複数の点を書き込むことも可能です。
Geometry Shader がプリミティブの単位で座標をいじれたり増やしたり出来るのに似ています。
例えばスクリーン座標で左右反転したり、画像を複製したり。

↑これは実際に 1枚のプリミティブを書き込んでいますが、Pixel Shader 内で複製して
100 ピクセル離れた場所にも同時に書き込んでいます。
わかりにくいけど、半透明の背景が同じものになっています。
(2) は現在書き込んでいるフレームバッファの値を事前に読み取って演算出来ることを
意味しています。うまく活用すれば Alpha Blend を自由にシェーダーでプログラム
出来るかもしれません。
結果だけ述べると、さすがに少々無理があったようです。

↑ちらつき
この 4枚のポリゴンは一度の Draw() で描画しています。
Reference Device では期待通り動きますが、RADEON HD 5870 の場合は自分自身との
重なりなど、直前に書き込んだピクセルの値が反映される場合とされない場合があります。
おそらく書き込みバッファがフラッシュされる前に読み込んでいるのだと思います。
プリミティブが大きくて一度の描画面積が大きい場合はおそらくバッファ容量を超えているため
うまく動いているように見えます。

↑自分自身との合成でもうまくいくシーン
以下実際に試したプログラムと解説。
フレームバッファ用のテクスチャを DXGI_FORMAT_R32_FLOAT で作っておきます。
SRV, RTV, UAV 全部作ります。
R8G8B8 のカラー値を変換して 32bit 整数値にして R32_FLOAT に書き込んでいます。
asfloat()/asint() を使っているため実質 R32_UINT でも同じです。
モデルデータを描画する場合
1. フレームバッファの値を読み込む
2. 自前でブレンド計算する
3. UAV に書き込む
最後に R32_FLOAT で書き込まれたフレームバッファを RGB に戻して描画します。
モデルを描画するシェーダー。四角形 1枚のみ。
// draw.hlsl struct VS_INPUT { uint vIndex : SV_VertexID; }; struct VS_OUTPUT { float4 vPos : SV_Position; float2 vTexcoord : TEXCOORD; }; struct PS_INPUT : VS_OUTPUT { float2 uv : TEXCOORD; }; cbuffer g_buf : register( c0 ) { float4x4 ViewProjection; }; // 頂点は VS で生成 VS_OUTPUT vmain_Plane( VS_INPUT vin ) { float4 vlist[4]= { { -1.0f, 1.0f, 1.0f, 1.0f }, { 1.0f, 1.0f, 1.0f, 1.0f }, { -1.0f, -1.0f, 1.0f, 1.0f }, { 1.0f, -1.0f, 1.0f, 1.0f }, }; float2 texlist[4]= { { 0.0f, 0.0f, }, { 1.0f, 0.0f, }, { 0.0f, 1.0f, }, { 1.0f, 1.0f, }, }; VS_OUTPUT vout; float4 pos= vlist[ vin.vIndex ]; vout.vPos= mul( pos, ViewProjection ); vout.vTexcoord= texlist[ vin.vIndex ]; return vout; } // パックされたカラーを展開する float3 pf_to_float3( float pf ) { uint data= asint( pf ); float3 rcol; const float tof= 1.0f/255.0f; rcol.x= ( data & 255) * tof; rcol.y= ((data>> 8) & 255) * tof; rcol.z= ((data>>16) & 255) * tof; return rcol; } // カラーを float1 に圧縮する float float3_to_pf( float3 color ) { uint3 bcol= (uint3)( color * 255.0f ) & 255; return asfloat( (bcol.z << 16) + (bcol.y << 8) + bcol.x ); } SamplerState sample0 : register( s0 ); Texture2D<float> tex0 : register( t0 ); RWTexture2D<float> tex1 : register( u1 ); Texture2D<float4> tex2 : register( t1 ); // UAV へレンダリング float4 pmain_Plane( PS_INPUT pin, float4 fscrpos : SV_POSITION ) : SV_TARGET { // 書き込むデータのテクスチャ float3 data= tex2[ pin.uv ]; // フレームバッファの座標 int2 scrpos= (int2)( fscrpos.xy ); // フレームバッファの内容を読み出す float3 scrdata= pf_to_float3( tex1[ scrpos ] ); // 適当にブレンドしてみる data= saturate( data * 0.7f + scrdata * 0.3f ); // UAV へ書き込んでいる tex1[ scrpos.xy + int2( 100, 0 ) ]= float3_to_pf( data ); tex1[ scrpos.xy + int2( 0, 0 ) ]= float3_to_pf( data ); // RTV へは出力しない return float4( 0,0,0,0 ); }
draw.hlsl の続き
VS_OUTPUT vmain_Render( VS_INPUT vin ) { float4 vlist[4]= { { -1.0f, 1.0f, 0.0f, 1.0f }, { 1.0f, 1.0f, 0.0f, 1.0f }, { -1.0f, -1.0f, 0.0f, 1.0f }, { 1.0f, -1.0f, 0.0f, 1.0f }, }; float2 texlist[4]= { { 0.0f, 0.0f, }, { 1.0f, 0.0f, }, { 0.0f, 1.0f, }, { 1.0f, 1.0f, }, }; VS_OUTPUT vout; vout.vPos= vlist[ vin.vIndex ]; vout.vTexcoord= texlist[ vin.vIndex ]; return vout; } // フレームバッファの値は圧縮されているので展開する float4 pmain_Render( PS_INPUT pin ) : SV_TARGET { float2 size; tex0.GetDimensions( size.x, size.y ); int2 loc= (int2)( pin.uv * size ); float3 data= pf_to_float3( tex0[ loc ] ); return float4( data, 1.0f ); }
C言語側。リソースの解放、BG の描画は省略しています。
CreateShader(), LoadTexture() の中身も略。
ID3D11Device* iDevice= NULL; struct BufferSet { ID3D11Texture2D* iTexture; ID3D11ShaderResourceView* iSRV; ID3D11RenderTargetView* iRTV; ID3D11UnorderedAccessView* iUAV; public: BufferSet() : iTexture( NULL ), iSRV( NULL ), iRTV( NULL ), iUAV( NULL ) { } void Create( int width, int height, DXGI_FORMAT buffer_format ); void Release(); }; void BufferSet::Create( int width, int height, DXGI_FORMAT buffer_format ) { // Texture CD3D11_ ~ は D3D11.h に定義されている CD3D11_TEXTURE2D_DESC tdesc( buffer_format, width, height, 1, 1, D3D11_BIND_SHADER_RESOURCE |D3D11_BIND_RENDER_TARGET |D3D11_BIND_UNORDERED_ACCESS, D3D11_USAGE_DEFAULT, 0, 1, 0, 0 ); iDevice->CreateTexture2D( &tdesc, NULL, &iTexture ); // ShaderResourceView も作る CD3D11_SHADER_RESOURCE_VIEW_DESC vdesc( D3D11_SRV_DIMENSION_TEXTURE2D, buffer_format, 0, 1, 0, 1, 0 ); iDevice->CreateShaderResourceView( iTexture, &vdesc, &iSRV ); // RenderTargetView も作る CD3D11_RENDER_TARGET_VIEW_DESC rdesc( D3D11_RTV_DIMENSION_TEXTURE2D, buffer_format, 0, 0, 1 ); iDevice->CreateRenderTargetView( iTexture, &rdesc, &iRTV ); // UnorderedAccessVieww も作る CD3D11_UNORDERED_ACCESS_VIEW_DESC uodesc( iTexture, D3D11_UAV_DIMENSION_TEXTURE2D, DXGI_FORMAT_UNKNOWN, 0, 0, 1 ); iDevice->CreateUnorderedAccessView( iTexture, &uodesc, &iUAV ); } BufferSet Screen; ID3D11DeviceContext* iContext= NULL; ID3D11VertexShader* iVS_Render= NULL; ID3D11PixelShader* iPS_Render= NULL; ID3D11VertexShader* iVS_Plane= NULL; ID3D11PixelShader* iPS_Plane= NULL; ID3D11ShaderResourceView* iInputImage= NULL; ID3D11ShaderResourceView* iBGImage= NULL; void Initialize() { Screen.Create( ScreenWidth, ScreenHeight, DXGI_FORMAT_R32_FLOAT ); iVS_Render= CreateShader( "draw.hlsl", "vs_5_0", "vmain_Render" ); iPS_Render= CreateShader( "draw.hlsl", "ps_5_0", "pmain_Render" ); iVS_Plane= CreateShader( "draw.hlsl", "vs_5_0", "vmain_Plane" ); iPS_Plane= CreateShader( "draw.hlsl", "ps_5_0", "pmain_Plane" ); iInputImage= LoadTexture( "plane.bmp" ); iBGImage= LoadTexture( "bgimage.bmp" ); } // モデル描画 void Draw() { CD3D11_VIEWPORT viewp( 0.0f, 0.0f, ScreenWidth, ScreenHeight ); iContext->RSSetViewports( 1, &viewp ); // ブレンドするので Zバッファは使わない iContext->OMSetDepthStensilState( iZDisable, 0 ); // Unordered Access View を設定する。RTV は NULL ID3D11RenderTargetView* ZERO_RTV= NULL; iContext->OMSetRenderTargetsAndUnorderedAccessViews( 1, // NumViews &ZERO_RTV, NULL, // depth buffer 1, // UAVStartSlot 1, // NumUAVs &Screen.iUAV, NULL ); iContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP ); iContext->VSSetShader( iVS_Plane, NULL, 0 ); iContext->PSSetShader( iPS_Plane, NULL, 0 ); iContext->PSSetShaderResources( 0, 1, &iInputImage ); iContext->PSSetSamplers( 0, 1, &iSampler ); iContext->Draw( 4, 0 ); } // フレームバッファ (Screen) の内容を実際に描画する。 // 本来のフレームバッファに転送 void Flush() { CD3D11_VIEWPORT viewp( 0.0f, 0.0f, ScreenWidth, ScreenHeight ); iContext->RSSetViewports( 1, &viewp ); iContext->OMSetDepthStensilState( iZDisable, 0 ); iContext->OMSetRenderTargets( 1, &iDefaultRenderTarget, NULL ); iContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP ); iContext->VSSetShader( iVS_Render, NULL, 0 ); iContext->PSSetShader( iPS_Render, NULL, 0 ); iContext->PSSetShaderResources( 0, 1, &Screen.iSRV ); iContext->Draw( 4, 0 ); }
関連エントリ
・Direct3D 11 / DirectX 11 UnorderedAccessView と RenderTarget の関係
・DirectX 11 / Direct3D 11 と RADEON HD 5870 の caps
・Direct3D11/DirectX11 (7) テセレータの流れの基本部分
・その他 Direct3D 11 関連
DirectX10 で大きく仕様が変わったのは、より汎用化を進めるためでした。
つまり専用の機能が無くなったということ。
ライティングのための機能、マテリアルの設定などといった目的別の API は
完全に姿を消しています。
では CPU のように完全に何にでも使える、単純でフラットな空間が得られたのかと言えば必ずしも
そうではなく、特にリソースとバッファの扱いはかえって複雑になった印象を受けます。
汎用化や何にでも使えることが、そのまま単純化に繋がるとは限らないためです。
複数の機能の要素をマージしたこと、目的別の機能の代わりにより抽象的な用語や仕様が
増えたことがその要因といえるかもしれません。
DirectX11 はさらに CompteShader が使えるようになり、書き込み可能なバッファが
追加されています。
以下 FEATURE_LEVEL 11.0、 ShaderModel 5.0 を対象としています。
●リソース
Direct3D 11 では View も ShaderObject も種類が増えており何を見て良いのか
わからなくなります。
でもリソースを作る手段は二つだけ。CreateBuffer() か CreateTexture~() です。
Constant も Buffer だし、Vertex や Index もただの Buffer です。
● View
リソースをどう扱うか、それを決めるのが View です。
同じリソースを複数の View を通して見ることが可能で、テクスチャとして参照したり
レンダリングしたり、データとして読み書きするなど流用が可能となります。
View は次の 4種類だけです。
・読み込みのための View、テクスチャやバッファなど
ShaderResourceView (SRV)
・Merger による書き込みのための View
RenderTargetView (RTV)
DepthStencilView (DSV)
・読み書きのための View
UnorderedAccessView (UAV)
VertexBuffer/IndexBuffer/ConstantBuffer は View を用いずに設定します。
●タイプとシェーダーオブジェクト
データをアクセスする方法を決定するのが、バッファ作成時の細かい指定と
DXGI フォーマットタイプです。
これらのシェーダーオブジェクトは、シェーダーがアクセスする方法を決定しています。
シェーダー側の宣言だけではだめで、リソース生成時にもいろいろとフラグを指定
しておく必要があります。
● UnorderedAccessView
RW がついているものと AppendSturctutedBuffer/ConsumeStructuredBuffer は
書き込み可能なシェーダーオブジェクトです。
これらは UnorderedAccessView (UAV) を使います。
UAV が使えるのは PixelShader と CompteShader だけ。
Compte Shader は Output Merger が無いので、出力のために必ず何らかの UAV を
用いることになります。
実際に使ってみると、出力はすべて UnorderedAccessView に相当することがわかってきます。
つまり RenderTarget も出力先アドレスが固定された UnorderedAccessView 相当だということです。
UAV の設定 API は次の通り
OMSetRenderTargetsAndUnorderedAccessViews() で一見何のために存在する
パラメータなのかわかりにくいのが UAVStartSlot です。
これは RenderTarget との衝突を避けるためのものでした。
例えば RenderTarget を 2 枚設定した場合は、UnorderedAccessView は必ず
番号 2 から始めなければなりません。
下記のようなリソースを登録する場合
RenderTarget x2
UnorderedAccessView x3
OMSetRenderTargetsAndUnorderedAccessViews() には次のようなパラメータを
渡します。
※ August 2009 のマニュアル OMSetRenderTargetsAndUnorderedAccessViews()
の引数は若干間違いがあります。ヘッダファイルを見た方が正しい宣言です。
シェーダーレジスタはこのように割り当てられます。
つまり o レジスタと u レジスタは同時に同じ番号を使うことが出来ず、
使用可能な View はあわせて 8 個まで。この両者ほぼ同じ扱いです。
UnorderedAccessView は読み書き可能なバッファです。
よく考えると RenderTarget や DepthBuffer と同じこと。
UAV は読み込んだあとにブレンドしたり、判定した結果をまた書き戻すことが出来るわけです。
そのパス内で更新できる OM をプログラムできるなら、いろいろ面白いことが出来そうです。
実際試してみたらエラーで、RW で読み込めるのは sngle コンポーネントに限ると言われます。
時間がなかったので、このあたりは後ほどまた詳しく調べてみます。
昔わからないと書いた CSSetUnorderedAccessViews() の最後のパラメータは
Append/Consume buffer に関するものでした。
やはり当時のサンプルの使い方が間違っていたのだと思われます。
関連エントリ
・DirectX 11 / Direct3D 11 と RADEON HD 5870 の caps
・Direct3D11/DirectX11 (7) テセレータの流れの基本部分
・その他 Direct3D 11 関連
つまり専用の機能が無くなったということ。
ライティングのための機能、マテリアルの設定などといった目的別の API は
完全に姿を消しています。
では CPU のように完全に何にでも使える、単純でフラットな空間が得られたのかと言えば必ずしも
そうではなく、特にリソースとバッファの扱いはかえって複雑になった印象を受けます。
汎用化や何にでも使えることが、そのまま単純化に繋がるとは限らないためです。
複数の機能の要素をマージしたこと、目的別の機能の代わりにより抽象的な用語や仕様が
増えたことがその要因といえるかもしれません。
DirectX11 はさらに CompteShader が使えるようになり、書き込み可能なバッファが
追加されています。
以下 FEATURE_LEVEL 11.0、 ShaderModel 5.0 を対象としています。
●リソース
Direct3D 11 では View も ShaderObject も種類が増えており何を見て良いのか
わからなくなります。
でもリソースを作る手段は二つだけ。CreateBuffer() か CreateTexture~() です。
Constant も Buffer だし、Vertex や Index もただの Buffer です。
● View
リソースをどう扱うか、それを決めるのが View です。
同じリソースを複数の View を通して見ることが可能で、テクスチャとして参照したり
レンダリングしたり、データとして読み書きするなど流用が可能となります。
View は次の 4種類だけです。
・読み込みのための View、テクスチャやバッファなど
ShaderResourceView (SRV)
・Merger による書き込みのための View
RenderTargetView (RTV)
DepthStencilView (DSV)
・読み書きのための View
UnorderedAccessView (UAV)
VertexBuffer/IndexBuffer/ConstantBuffer は View を用いずに設定します。
●タイプとシェーダーオブジェクト
データをアクセスする方法を決定するのが、バッファ作成時の細かい指定と
DXGI フォーマットタイプです。
Texture Buffer ByteAdddressBuffer StructuredBuffer AppendSturctutedBuffer ConsumeStructuredBuffer RWTexture RWBuffer RWSturcturedBuffer RWByteAddressBuffer
これらのシェーダーオブジェクトは、シェーダーがアクセスする方法を決定しています。
シェーダー側の宣言だけではだめで、リソース生成時にもいろいろとフラグを指定
しておく必要があります。
● UnorderedAccessView
RW がついているものと AppendSturctutedBuffer/ConsumeStructuredBuffer は
書き込み可能なシェーダーオブジェクトです。
これらは UnorderedAccessView (UAV) を使います。
UAV が使えるのは PixelShader と CompteShader だけ。
Compte Shader は Output Merger が無いので、出力のために必ず何らかの UAV を
用いることになります。
実際に使ってみると、出力はすべて UnorderedAccessView に相当することがわかってきます。
つまり RenderTarget も出力先アドレスが固定された UnorderedAccessView 相当だということです。
UAV の設定 API は次の通り
CompteShader → CSSetUnorderedAccessViews() PixelShader → OMSetRenderTargetsAndUnorderedAccessViews()
OMSetRenderTargetsAndUnorderedAccessViews() で一見何のために存在する
パラメータなのかわかりにくいのが UAVStartSlot です。
これは RenderTarget との衝突を避けるためのものでした。
例えば RenderTarget を 2 枚設定した場合は、UnorderedAccessView は必ず
番号 2 から始めなければなりません。
下記のようなリソースを登録する場合
RenderTarget x2
UnorderedAccessView x3
OMSetRenderTargetsAndUnorderedAccessViews() には次のようなパラメータを
渡します。
NumViews = 2 // RTV (RenderTargetView) の個数 UAVStartSlot = 2 // UAV (UnorderedAccessView) の開始番号 == NumViews NumUAVs = 3 // UAV の個数
※ August 2009 のマニュアル OMSetRenderTargetsAndUnorderedAccessViews()
の引数は若干間違いがあります。ヘッダファイルを見た方が正しい宣言です。
シェーダーレジスタはこのように割り当てられます。
o0 = RenderTarget o1 = RenderTarget u2 = UnorderedAccessView u3 = UnorderedAccessView u4 = UnorderedAccessView
つまり o レジスタと u レジスタは同時に同じ番号を使うことが出来ず、
使用可能な View はあわせて 8 個まで。この両者ほぼ同じ扱いです。
UnorderedAccessView は読み書き可能なバッファです。
よく考えると RenderTarget や DepthBuffer と同じこと。
UAV は読み込んだあとにブレンドしたり、判定した結果をまた書き戻すことが出来るわけです。
そのパス内で更新できる OM をプログラムできるなら、いろいろ面白いことが出来そうです。
実際試してみたらエラーで、RW で読み込めるのは sngle コンポーネントに限ると言われます。
時間がなかったので、このあたりは後ほどまた詳しく調べてみます。
昔わからないと書いた CSSetUnorderedAccessViews() の最後のパラメータは
Append/Consume buffer に関するものでした。
やはり当時のサンプルの使い方が間違っていたのだと思われます。
関連エントリ
・DirectX 11 / Direct3D 11 と RADEON HD 5870 の caps
・Direct3D11/DirectX11 (7) テセレータの流れの基本部分
・その他 Direct3D 11 関連
RADEON HD 5870 手に入れました。
とりあえず各機能の対応状況は以下の通り。(caps viewer より)
ShaderModel 5.0 も、ComputeShader も、BC6H/BC7 も、Yes です。
去年末くらいに実験していたテセレータ周りのプログラムもそのまま動きました。
Compute Shader 4~5 も動いてます。OpenCL の方は不明。
唯一 Thread 周りが未対応で、Concurrent Creates と Command Lists が No に
なっています。ドライバがまだ完全ではないのかもしれません。
使用したドライバは Radeon_HD5800_8.66RC6_Vista_Win7_Sep21 。
倍精度演算のコンパイルを試しました。hlsl の出力は下記の通り。
d がついているのが double 命令です。
倍精度演算はレジスタを 2 個使って表現していることがわかります。
レジスタのペアを {} で表現すると
でも実際に走らせると落ちました。
単純な加算や型変換だけなら動くのですが、まだ何らかの問題があるようです。
関連エントリ
・RADEON HD 5870 と DirectX 11
・Direct3D11/DirectX11 (18) GPU を使ったアウトラインフォントの描画の(6)
とりあえず各機能の対応状況は以下の通り。(caps viewer より)
Direct3D 11 Feature Level D3D_FEATURE_LEVEL_11_0 Driver Concurrent Creates No Driver Command Lists No Double-precision Shaders Yes Compute Shader 4.x Yes D3D_FEATURE_LEVEL_11_0 Shader Model 5.0 Geometry Shader Yes Stream Out Yes Compute Shader Yes Hull & Domain Shaders Yes Texture Resource Arrays Yes Cubemap Resource Arrays Yes BC4/BC5 Compression Yes BC6H/BC7 Compression Yes Alpha-to-coverage Yes Extended Formats (BGRA, etc.) Yes 10-bit XR High Color Format Yes
ShaderModel 5.0 も、ComputeShader も、BC6H/BC7 も、Yes です。
去年末くらいに実験していたテセレータ周りのプログラムもそのまま動きました。
Compute Shader 4~5 も動いてます。OpenCL の方は不明。
唯一 Thread 周りが未対応で、Concurrent Creates と Command Lists が No に
なっています。ドライバがまだ完全ではないのかもしれません。
使用したドライバは Radeon_HD5800_8.66RC6_Vista_Win7_Sep21 。
倍精度演算のコンパイルを試しました。hlsl の出力は下記の通り。
cs_5_0 dcl_globalFlags refactoringAllowed | enableDoublePrecisionFloatOps dcl_uav_structured u0, 4 dcl_input vThreadIDInGroupFlattened dcl_input vThreadGroupID.xy dcl_input vThreadIDInGroup.xy dcl_input vThreadID.xy dcl_temps 2 ~ add r0.y, r0.z, r0.y add r0.y, r0.w, r0.y ftod r0.zw, r0.y dmul r0.zw, d(0.000000, 100000.000000), r0.zwzw dadd r0.zw, d(0.000000, 0.000000), r0.zwzw imul null, r1.xy, l(10000, 1000, 0, 0), vThreadIDInGroup.xyxx utof r1.xy, r1.xyxx ftod r1.xyzw, r1.xyxx dadd r0.zw, r0.zwzw, r1.xyxy ~
d がついているのが double 命令です。
倍精度演算はレジスタを 2 個使って表現していることがわかります。
レジスタのペアを {} で表現すると
ftod r0.zw, r0.y ↓ r0.{zw} = FloatToDouble( r0.y ) utof r1.xy, r1.xyxx ftod r1.xyzw, r1.xyxx dadd r0.zw, r0.zwzw, r1.xyxy ↓ r0.x = UIntToFloat( r1.x ) r0.y = UIntToFloat( r1.y ) r1.{xy} = FloatToDouble( r0.x ) r1.{zw} = FloatToDouble( r0.y ) r0.{zw} = r0.{zw} + r1.{xy}
でも実際に走らせると落ちました。
単純な加算や型変換だけなら動くのですが、まだ何らかの問題があるようです。
関連エントリ
・RADEON HD 5870 と DirectX 11
・Direct3D11/DirectX11 (18) GPU を使ったアウトラインフォントの描画の(6)
2009/10/04
ARM Cortex-A8 の NEON と浮動小数演算最適化
ARM も x86 と同じように、これまで様々な命令セットの拡張が行われてきました。
例えば乗算一つとっても多数の命令が存在しており、どれを使えばよいかわからなくなる
ことがあります。
x86 に FPU と SSE 命令が共存しているように、ARM にも VFP や NEON 命令があります。
どちらの命令を使っても、単精度の浮動小数演算が可能です。
Cortex-A8 (ARM v7) の場合: 浮動小数演算
NEON 命令は VFP 命令も混在しており区別が付きにくいことから、それぞれ演算速度を
調べてみました。
下記の表は気になる命令のみピックアップして調べたものです。
4 G 個 (40億個) の命令を実行した場合の時間の実測値です。
ARM レジスタから NEON/VFP レジスタへのデータ転送はいくつかの手段が考えられます。
この命令は NEON の 64bit d0~d31 レジスタと、ARM レジスタ r0~ 2個を相互に
転送するものです。ARM 側は必ず 2個セットの 64bit でなければならず、動作時間も
比較的かかっています。
上の vmov.32 は NEON の d0~d31 レジスタのうち半分、32bit 分のみ転送する命令です。
NEON レジスタは下記のように割り当てられています。
全く同じ領域を VFP レジスタとしてもアクセスすることが出来ます。
VFP ビューでは 32bit のデータを sレジスタとして個別にアクセスできます。
64bit dレジスタの扱いはほぼ同じです。
命令フィールドの制限から、レジスタ番号は 0~31 の範囲でなければなりません。
つまり 32bit レジスタが 64 個あるにもかかわらず、s レジスタとしてアクセス出来る
のは半分の s0~s31 だけです。
NEON ビューの 32bit スカラ要素では、このようなアクセス制限が無いことがわかります。
NEON ビューと VFP ビューの重要かつ大きな違いがもう一つあります。
NEON では dレジスタの 64bit 単位でデータを扱うということ。
VFP は sレジスタ単位、つまり 32bit 単位でデータを扱います。
よって上の命令
は NEON d レジスタの 32bit 分、半分の領域にしか書き込みを行いません。
このとき d レジスタは、残り半分のデータを保存しなければならなくなるため
デステネーションレジスタにも依存が発生します。
いわゆるパーシャルレジスタストールです。
SSE で SS 命令よりも、完全に置き換える PS 命令の方が速いのと同じです。
実際に測定してみると、下記のようにデスティネーション側レジスタに同じ d レジスタ
を指定するとパイプラインストールが発生します。これが 10: の vmov.32 の値です。
レジスタを置き換えた 11: の方では 15 sec → 10 sec と速くなるため、1cycle 分の
遅延が発生していることがわかります。
全く同じことが 03: の vldr.32 s,[r] でも発生していることがわかりました。
03: vldr.32 s,[r] は VFP 命令のはずですが、実行時間を見ると NEON の演算ユニットで
実行しているようです。
NEON には即値アドレスのメモリから 32bit スカラを読み込む命令がないので
この命令を多用しても大丈夫そうです。
s レジスタへの書き込みは上の d[x] と同じように 32bit の部分書き込みに相当します。
実際に下記の通りストールが発生しました。
また sレジスタなので、後半 d16~d31 エリアへ直接 32bit の値をロードすることができません。
VFP 命令が遅いのは、このようなアーキテクチャの違いも一つの要因かもしれません。
レジスタへのアクセス単位が異なるので、パイプラインが矛盾しないように実行が終わるのを
待っている可能性があります。
最初の測定結果から、ARM レジスタから NEON レジスタへのスカラ転送は vdup を
使うのが最も効率がよいことがわかります。
実行速度も速く d レジスタを完全に置き換えるために不要な依存が発生しません。
メモリからの読み込みは vldr.32 s を用います。後半 d16~d31 エリアへの代入が
必要なら vld1.32 を使うことが出来ますが、この場合利用できるアドレッシングモードに
制限があります。
これらの測定データを元に、VFP 命令を NEON 命令に置換する簡単なスクリプトを
作ってみました。命令置換は下記の方針で処理しています。
gcc は VFP でコンパイルし、出力したアセンブラコード (*.s) をいったんスクリプトに通して
可能な部分を NEON 命令に置き換えます。例えば Makefile は下記のような感じで、
opt_neon.pl を通しています。
上のプログラムを実行した結果は次の通り (NetWalker Cortex-A8 800MHz)
スクリプト opt_neon.pl は上のプログラムを正しく変換できるように必要な命令の分しか
作っていません。つまりまだ未完成です。対応してない命令があるのでどんなプログラムでも
変換できるわけではありません。
それでもきちんと効果があるので、このまま対応命令を増やせば浮動小数演算を多用した
アプリケーションも大幅な高速化が期待できそうです。
・opt_neon.pl
将来的にはおそらく gcc の方に、スカラ演算でも NEON コードだけを生成するオプションが
追加されるのではないでしょうか。
試していませんが iPhone 3GS や iPod Touch 3G でももしかしたら NEON 最適化が
有効かもしれません。
関連エントリ
・NetWalker PC-Z1 Cortex-A8 の NEON 命令とメモリ速度
・SSE の浮動小数演算速度
・NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
・NetWalker PC-Z1 Atom と速度比較
例えば乗算一つとっても多数の命令が存在しており、どれを使えばよいかわからなくなる
ことがあります。
x86 に FPU と SSE 命令が共存しているように、ARM にも VFP や NEON 命令があります。
どちらの命令を使っても、単精度の浮動小数演算が可能です。
Cortex-A8 (ARM v7) の場合: 浮動小数演算
VFP 低速 倍精度演算対応 IEEE754 準拠 NEON 高速 単精度のみ SIMD
NEON 命令は VFP 命令も混在しており区別が付きにくいことから、それぞれ演算速度を
調べてみました。
下記の表は気になる命令のみピックアップして調べたものです。
4 G 個 (40億個) の命令を実行した場合の時間の実測値です。
01: vld1.32 {d[0]},[r] 20.16 sec *1 02: vld1.32 {d[0]},[r] 15.14 sec *2 03: vldr.32 s,[r] 15.06 sec *1 04: vldr.32 s,[r] 10.04 sec *2 05: vldr.64 d,[r] 10.18 sec 06: vdup.32 d,r 5.90 sec 07: vdup.32 q,r 5.90 sec 08: vdup.32 d,d[0] 5.02 sec 09: vdup.32 q,d[0] 5.02 sec 10: vmov.32 d[0],r 15.10 sec *1 11: vmov.32 d[0],r 10.08 sec *2 12: vmov.32 r,d[0] 7.03 sec 13: vmov d,r,r 11.79 sec 14: vmov r,r,d 12.05 sec 15: vcvt.s32.f32 d,d 5.01 sec 16: vcvt.s32.f32 q,q 10.03 sec 17: vcvt.f32.s32 s,s 45.30 sec VFP 18: vcvt.f32.s32 d,d 3.76 sec 19: vcvt.f32.s32 q,q 10.04 sec 20: vadd.f32 d,d,d 5.02 sec 21: vadd.f32 q,q,q 10.04 sec 22: vmla.f32 d,d,d 5.02 sec 23: vmla.f32 q,q,q 10.04 sec 24: vrecpe.f32 d,d 5.02 sec 25: vrecpe.f32 q,q 10.10 sec 26: vrecps.f32 d,d,d 5.08 sec 27: vrecps.f32 q,q,q 10.09 sec r = ARM レジスタ r0~ s = VFP レジスタ s0~s31 d = NEON/VFP レジスタ d0~d31 q = NEON レジスタ q0~q15 *1 = インターリーブ無し *2 = インターリーブあり
ARM レジスタから NEON/VFP レジスタへのデータ転送はいくつかの手段が考えられます。
13: vmov d,r,r 11.79 sec 14: vmov r,r,d 12.05 sec
この命令は NEON の 64bit d0~d31 レジスタと、ARM レジスタ r0~ 2個を相互に
転送するものです。ARM 側は必ず 2個セットの 64bit でなければならず、動作時間も
比較的かかっています。
10: vmov.32 d[0],r 15.10 sec *1 11: vmov.32 d[0],r 10.08 sec *2 12: vmov.32 r,d[0] 7.03 sec
上の vmov.32 は NEON の d0~d31 レジスタのうち半分、32bit 分のみ転送する命令です。
NEON レジスタは下記のように割り当てられています。
NEON ビュー +-------------------------------+ | q0 | 128bit ( ~ q15 , 16個 ) +---------------+---------------+ | d0 | d1 | 64bit ( ~ d31 , 32個 ) +-------+-------+-------+-------+ | d0[0] | d0[1] | d1[0] | d1[1] | 32bit ( ~ d31[1] , 64個 ) +-------+-------+-------+-------+
全く同じ領域を VFP レジスタとしてもアクセスすることが出来ます。
VFP ビュー +---------------+---------------+ | d0 | d1 | 64bit ( ~ d31 , 32個 ) +-------+-------+-------+-------+ | s0 | s1 | s2 | s3 | 32bit ( ~ s31 , 32個 ) +-------+-------+-------+-------+
VFP ビューでは 32bit のデータを sレジスタとして個別にアクセスできます。
64bit dレジスタの扱いはほぼ同じです。
命令フィールドの制限から、レジスタ番号は 0~31 の範囲でなければなりません。
つまり 32bit レジスタが 64 個あるにもかかわらず、s レジスタとしてアクセス出来る
のは半分の s0~s31 だけです。
NEON ビューの 32bit スカラ要素では、このようなアクセス制限が無いことがわかります。
NEON ビューと VFP ビューの重要かつ大きな違いがもう一つあります。
NEON では dレジスタの 64bit 単位でデータを扱うということ。
VFP は sレジスタ単位、つまり 32bit 単位でデータを扱います。
よって上の命令
10: vmov.32 d[0],r 15.10 sec *1
は NEON d レジスタの 32bit 分、半分の領域にしか書き込みを行いません。
このとき d レジスタは、残り半分のデータを保存しなければならなくなるため
デステネーションレジスタにも依存が発生します。
いわゆるパーシャルレジスタストールです。
SSE で SS 命令よりも、完全に置き換える PS 命令の方が速いのと同じです。
実際に測定してみると、下記のようにデスティネーション側レジスタに同じ d レジスタ
を指定するとパイプラインストールが発生します。これが 10: の vmov.32 の値です。
; 同じ d0 に部分書き込みを行うためストールする。10: vmov.32 ~ 15.06 sec vmov.32 d0[0],r2 vmov.32 d0[1],r3 vmov.32 d0[0],r2 vmov.32 d0[0],r3 ~
; 異なるレジスタへ交互に書き込む場合。11: vmov.32 ~ 10.04 sec vmov.32 d0[0],r2 vmov.32 d1[0],r2 vmov.32 d2[0],r2 vmov.32 d3[0],r2 ~
レジスタを置き換えた 11: の方では 15 sec → 10 sec と速くなるため、1cycle 分の
遅延が発生していることがわかります。
全く同じことが 03: の vldr.32 s,[r] でも発生していることがわかりました。
03: vldr.32 s,[r] は VFP 命令のはずですが、実行時間を見ると NEON の演算ユニットで
実行しているようです。
NEON には即値アドレスのメモリから 32bit スカラを読み込む命令がないので
この命令を多用しても大丈夫そうです。
s レジスタへの書き込みは上の d[x] と同じように 32bit の部分書き込みに相当します。
実際に下記の通りストールが発生しました。
; ストールする ( s0 も s1 も同じ d0 レジスタに相当するため) vldr.32 s0,[r1] vldr.32 s1,[r1] vldr.32 s0,[r1] vldr.32 s1,[r1]
; ストールしない vldr.32 s0,[r1] vldr.32 s2,[r1] vldr.32 s4,[r1] vldr.32 s6,[r1]
また sレジスタなので、後半 d16~d31 エリアへ直接 32bit の値をロードすることができません。
VFP 命令が遅いのは、このようなアーキテクチャの違いも一つの要因かもしれません。
レジスタへのアクセス単位が異なるので、パイプラインが矛盾しないように実行が終わるのを
待っている可能性があります。
最初の測定結果から、ARM レジスタから NEON レジスタへのスカラ転送は vdup を
使うのが最も効率がよいことがわかります。
実行速度も速く d レジスタを完全に置き換えるために不要な依存が発生しません。
メモリからの読み込みは vldr.32 s を用います。後半 d16~d31 エリアへの代入が
必要なら vld1.32 を使うことが出来ますが、この場合利用できるアドレッシングモードに
制限があります。
これらの測定データを元に、VFP 命令を NEON 命令に置換する簡単なスクリプトを
作ってみました。命令置換は下記の方針で処理しています。
・s0~s31 レジスタは d0~d31 レジスタのスカラにマップする。 ・ARM レジスタとの相互転送は下記の命令を使う。 R ← D vmov.32 r,d[0] D ← R vdup.32 d,r ・一般の演算や浮動小数と整数の変換などは NEON の 64bit (float x2) 演算を用いる。 ・メモリからのロードは vldr.32 s,addr ・d レジスタの奇数要素 d0[1]~d31[1] は一時的なテンポラリに使える。 ・どうしても s レジスタを使わなければならない命令ではレジスタ番号を 2倍する。 例えば double への変換や vldr 命令時。 s1 は d1 = (s2,s3) にマッピングされるため。 ・s レジスタの番号が 32 を超える場合は、テンポラリを経由した複数命令に展開する。やむなし ・倍精度演算命令は置き換えない。
gcc は VFP でコンパイルし、出力したアセンブラコード (*.s) をいったんスクリプトに通して
可能な部分を NEON 命令に置き換えます。例えば Makefile は下記のような感じで、
opt_neon.pl を通しています。
%.o: %.cpp $(CC) $(CFLAGS) $< -S -o $*._temp.s $(PERL) opt_neon.pl $*._temp.s > $*.s $(CC) $(CFLAGS) $*.s -c -o $@
void Loop_main( float dd ) { TimerClass timer; timer.Begin(); float sum= 0.0f; for( int i= 0 ; i< 100000000 ; i++ ){ sum= sum * dd + dd; } timer.End( "end" ); printf( "%f\n", sum ); }
上のプログラムを実行した結果は次の通り (NetWalker Cortex-A8 800MHz)
そのまま VFP を使用した場合 2.39 秒 オプティマイザを通した場合 1.01 秒
スクリプト opt_neon.pl は上のプログラムを正しく変換できるように必要な命令の分しか
作っていません。つまりまだ未完成です。対応してない命令があるのでどんなプログラムでも
変換できるわけではありません。
それでもきちんと効果があるので、このまま対応命令を増やせば浮動小数演算を多用した
アプリケーションも大幅な高速化が期待できそうです。
・opt_neon.pl
将来的にはおそらく gcc の方に、スカラ演算でも NEON コードだけを生成するオプションが
追加されるのではないでしょうか。
試していませんが iPhone 3GS や iPod Touch 3G でももしかしたら NEON 最適化が
有効かもしれません。
NetWalker i.MX515 Cortex-A8 (ARM v7-A) 800MHz AMD Z430 iPhone 3GS S5PC100 Cortex-A8 (ARM v7-A) 600MHz PowerVR SGX 535
関連エントリ
・NetWalker PC-Z1 Cortex-A8 の NEON 命令とメモリ速度
・SSE の浮動小数演算速度
・NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
・NetWalker PC-Z1 Atom と速度比較
2009/10/03
NetWalker PC-Z1 Cortex-A8 の NEON 命令とメモリ速度
最善ケースばかりでなく、実際に使えるプログラムで試してみます。
使っているのは i.MX51 系 i.MX515 を搭載した NetWalker PC-Z1 ですが、
おそらく同じ Cortex-A8 (ARM v7-A) の CPU core を使った iPhone 3GS や
iPod touch 3G でも全く同様ではないかと思います。
NEON のプログラミングは比較的簡単です。
例えば 4x4 matrix の乗算だとこんな感じで書けます。
かなり少ない命令で記述できます。
・3オペランドの命令フォーマットでレジスタの転送が不要
・積和命令がある
・ベクタへのスカラ乗算機能が用意されているため、スカラ要素の複製が不要
・レジスタの数が多い
上のプログラムで配列のような記述をしている d8[0], d8[1] は、d0レジスタに含まれる
2つの float に個別にアクセスしています。
s0~s31 は 32bit の single float レジスタですが、この形式で記述する命令は
VFP 命令なので要注意です。NEON View では d または q レジスタのみ扱い、
single 要素は d レジスタの一部として扱います。
なお VFP 命令も倍精度演算で d レジスタを扱うことがあります。
vmla は積和命令で vmla.f32 q8,q0,d8[1] はシェーダー風の記述を用いるなら
となります。ベクタへのスカラ乗算を簡単に記述できるのは便利です。
SSE だと 3~4命令くらいかかります。
これが SSE なら
長いです。
その代わり Cortex-A8 / NEON はインオーダー実行 HT 無しなので、最適化を考えて
書く必要があります。
最初の NEON の例も、レジスタが多いことを利用すれば次のようにできます。
完全にキャッシュがヒットする前提なら、こちらのコードの方が 1.7倍くらい速くなります。
ただし、実際のアプリケーションで使うとここまで差が出ません。
メモリアクセスの方がずっと低速だからです。
キャッシュがほとんど利かない条件でテストすると、Cortex-A8 + NEON は Atom よりも
かなり低速でした。Matrix 演算ではなく、ただのメモリ転送だけテストしてみたのが下の表です。
Atom Z540 は FSB 533MHz で 64bit 幅あるため、DDR2-533 とするとメモリの転送速度は
最大 4.2GB/sec (PC2-4200) と考えられます。
i.MX515 は mDDR1 or DDR2 200MHz の 16/32bit らしいですが、この辺の具体的な値は
明らかになっていません。ただ考えられる数値よりも測定結果はかなり低いです。
省電力との兼ね合いでしょうか。L2 cache 容量も Atom の半分です。
プロセッサ自体の演算能力はそれなりにあるものの、メモリ上のデータを大量にストリーム処理
するようなケースでは、ほとんど生かし切れていない可能性があります。
同様の core を持つ iPhone 3GS でどの程度動くか試してみたいところです。
関連エントリ
・SSE の浮動小数演算速度
・NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
・NetWalker PC-Z1 Atom と速度比較
使っているのは i.MX51 系 i.MX515 を搭載した NetWalker PC-Z1 ですが、
おそらく同じ Cortex-A8 (ARM v7-A) の CPU core を使った iPhone 3GS や
iPod touch 3G でも全く同様ではないかと思います。
NEON のプログラミングは比較的簡単です。
例えば 4x4 matrix の乗算だとこんな感じで書けます。
inline void Mul_NEON( Float4x4* p3, const Float4x4* p1, const Float4x4* p2 ) { __asm__ __volatile__ ( "\ vldmia %0, {d0-d7} \n\ vldmia %1, {d8-d15} \n\ \n\ vmul.f32 q8,q0,d8[0] \n\ vmla.f32 q8,q1,d8[1] \n\ vmla.f32 q8,q2,d9[0] \n\ vmla.f32 q8,q3,d9[1] \n\ vstmia %2!, {d16,d17} \n\ \n\ vmul.f32 q8,q0,d10[0] \n\ vmla.f32 q8,q1,d10[1] \n\ vmla.f32 q8,q2,d11[0] \n\ vmla.f32 q8,q3,d11[1] \n\ vstmia %2!, {d16,d17} \n\ \n\ vmul.f32 q8,q0,d12[0] \n\ vmla.f32 q8,q1,d12[1] \n\ vmla.f32 q8,q2,d13[0] \n\ vmla.f32 q8,q3,d13[1] \n\ vstmia %2!, {d16,d17} \n\ \n\ vmul.f32 q8,q0,d14[0] \n\ vmla.f32 q8,q1,d14[1] \n\ vmla.f32 q8,q2,d15[0] \n\ vmla.f32 q8,q3,d15[1] \n\ vstmia %2!, {d16,d17} \n\ " : "=&r"( p1 ), "=&r"( p2 ), "=&r"( p3 ) : "0"( p1 ), "1"( p2 ), "2"( p3 ) : "q0", "q1", "q2", "q3", "q4", "q5", "q6", "q7", "q8", "cc", "memory" ); }
かなり少ない命令で記述できます。
・3オペランドの命令フォーマットでレジスタの転送が不要
・積和命令がある
・ベクタへのスカラ乗算機能が用意されているため、スカラ要素の複製が不要
・レジスタの数が多い
d0~d31 : 64bit レジスタ (float x2) 32本 q1~q16 : 128bit レジスタ (float x4) 16本 (d0~d31 と共有)
上のプログラムで配列のような記述をしている d8[0], d8[1] は、d0レジスタに含まれる
2つの float に個別にアクセスしています。
s0~s31 は 32bit の single float レジスタですが、この形式で記述する命令は
VFP 命令なので要注意です。NEON View では d または q レジスタのみ扱い、
single 要素は d レジスタの一部として扱います。
なお VFP 命令も倍精度演算で d レジスタを扱うことがあります。
vmla は積和命令で vmla.f32 q8,q0,d8[1] はシェーダー風の記述を用いるなら
q8.xyzw = q0.xyzw * d8.xxxx + q8.xyzw
となります。ベクタへのスカラ乗算を簡単に記述できるのは便利です。
SSE だと 3~4命令くらいかかります。
これが SSE なら
void __fastcall Mul_SSE( Float4x4* p3, const Float4x4* p1, const Float4x4* p2 ) { __asm { mov eax, dword ptr[esp+4] ; p2 movaps xmm4, xmmword ptr[edx] ; p1->_11_12_13_14 movss xmm0, xmmword ptr[eax] ; p2->_11 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm4 movaps xmm1, xmm0 movaps xmm5, xmmword ptr[edx+16] ; p1->_21_22_23_24 movss xmm0, xmmword ptr[eax+4] ; p2->_12 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm5 addps xmm1, xmm0 movaps xmm6, xmmword ptr[edx+32] ; p1->_31_32_33_34 movss xmm0, xmmword ptr[eax+8] ; p2->_13 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm6 addps xmm1, xmm0 movaps xmm7, xmmword ptr[edx+48] ; p1->_41_42_43_44 movss xmm0, xmmword ptr[eax+12] ; p2->_14 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm7 addps xmm1, xmm0 movaps xmmword ptr[ecx], xmm1 ; p3->_11_12_13_14 movss xmm0, xmmword ptr[eax+16] ; p2->_21 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm4 movaps xmm1, xmm0 movss xmm0, xmmword ptr[eax+20] ; p2->_22 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm5 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+24] ; p2->_23 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm6 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+28] ; p2->_24 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm7 addps xmm1, xmm0 movaps xmmword ptr[ecx+16], xmm1 ; p3->_21_22_23_24 movss xmm0, xmmword ptr[eax+32] ; p2->_31 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm4 movaps xmm1, xmm0 movss xmm0, xmmword ptr[eax+36] ; p2->_32 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm5 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+40] ; p2->_33 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm6 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+44] ; p2->_34 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm7 addps xmm1, xmm0 movaps xmmword ptr[ecx+32], xmm1 ; p3->_31_32_33_34 movss xmm0, xmmword ptr[eax+48] ; p2->_41 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm4 movaps xmm1, xmm0 movss xmm0, xmmword ptr[eax+52] ; p2->_42 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm5 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+56] ; p2->_43 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm6 addps xmm1, xmm0 movss xmm0, xmmword ptr[eax+60] ; p2->_44 shufps xmm0, xmm0, 00000000b mulps xmm0, xmm7 addps xmm1, xmm0 movaps xmmword ptr[ecx+48], xmm1 ; p3->_41_42_43_44 }; }
長いです。
その代わり Cortex-A8 / NEON はインオーダー実行 HT 無しなので、最適化を考えて
書く必要があります。
最初の NEON の例も、レジスタが多いことを利用すれば次のようにできます。
vldmia %0, {d0-d7} vldmia %1, {d8-d15} vmul.f32 q8,q0,d8[0] vmul.f32 q9,q0,d10[0] vmul.f32 q10,q0,d12[0] vmul.f32 q11,q0,d14[0] vmla.f32 q8,q1,d8[1] vmla.f32 q9,q1,d10[1] vmla.f32 q10,q1,d12[1] vmla.f32 q11,q1,d14[1] vmla.f32 q8,q2,d9[0] vmla.f32 q9,q2,d11[0] vmla.f32 q10,q2,d13[0] vmla.f32 q11,q2,d15[0] vmla.f32 q8,q3,d9[1] vmla.f32 q9,q3,d11[1] vmla.f32 q10,q3,d13[1] vmla.f32 q11,q3,d15[1] vstmia %2, {d16-d23}
完全にキャッシュがヒットする前提なら、こちらのコードの方が 1.7倍くらい速くなります。
ただし、実際のアプリケーションで使うとここまで差が出ません。
メモリアクセスの方がずっと低速だからです。
キャッシュがほとんど利かない条件でテストすると、Cortex-A8 + NEON は Atom よりも
かなり低速でした。Matrix 演算ではなく、ただのメモリ転送だけテストしてみたのが下の表です。
Atom Z540 1.86GHz 2.98GB/sec Atom Z540 800MHz 1.91GB/sec (省電力機能で制限をかけたもの) Cortex-A8 800MHz 255MB/sec
Atom Z540 は FSB 533MHz で 64bit 幅あるため、DDR2-533 とするとメモリの転送速度は
最大 4.2GB/sec (PC2-4200) と考えられます。
i.MX515 は mDDR1 or DDR2 200MHz の 16/32bit らしいですが、この辺の具体的な値は
明らかになっていません。ただ考えられる数値よりも測定結果はかなり低いです。
省電力との兼ね合いでしょうか。L2 cache 容量も Atom の半分です。
プロセッサ自体の演算能力はそれなりにあるものの、メモリ上のデータを大量にストリーム処理
するようなケースでは、ほとんど生かし切れていない可能性があります。
同様の core を持つ iPhone 3GS でどの程度動くか試してみたいところです。
関連エントリ
・SSE の浮動小数演算速度
・NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
・NetWalker PC-Z1 Atom と速度比較
2009/10/01
SSE の浮動小数演算速度
Cortex-A8 の NEON のように SSE も試してみました。
やりかたは前回と全く同じように、ひたすら命令を並べてスループットの測定。
今回はスレッドにも対応しておきます。
上記のようにメモリアクセスの無い命令を前回同様に 40 個 (8x5) 並べます。
スレッドの開始と終了待ちはシグナルで同期します。一番遅いスレッドの終了に合わせているため、
速く終了したスレッドがあると若干無駄が生じる可能性があります。
メインスレッドが実行している間に、サブスレッドで何回命令を実行出来たか数えた方が
より正確な結果になるかもしれません。
Core i7 920 2.67GHz を使っています。1 スレッドあたり 1G 回ループします。
40命令 × 1G 回ループなので 40/14.32 = 2.79 。
CPU は 2.67GHz なので 1命令 / 1cycle に近い値だけど少々オーバーしています。
ターボブーストのおかげでしょうか。スレッドを増やしてみます。
core 数 (4) までは大きく増えませんが、8スレッド時は、1スレッドのほぼ 2倍の時間が
かかっています。HT を含めるとハードウエアで 8スレッドですが、パイプラインに空きが
ないためか HT の効果が全く無いことがわかります。
8スレッド時の値を元に計算してみると Core i7 920 2.67GHz は mulps で
40命令 × 1G回ループ × 4 SIMD × 8 Thread / 29.05 秒 = およそ 44.06 G FLOPS
SSE には積和がありませんが SSE4.1 には DPPS があることを思い出しました。
Mul x4 + Add x3 なので試してみます。
mul のちょうど 2倍、スループット 2 で実行しているようです。
2 cycle で 7 float なので演算量自体は mulps に劣ります。
最初の乗算を敢えてストールするように組み替えてみます。
インターリーブ時は 14.32秒だったので、4倍の実行時間がかかるようになりました。
mulps のレイテンシが 4 cycle あることがわかります。パイプラインはがら空きで、
4スレッド時と 8スレッド時の実行速度に差がありません。HT がめいっぱい働いており、
それでもまだ半分ほど無駄が生じていることになります。
同じように dpps もストールさせてみると、こちらはさらに時間がかかります。
5.5 倍かかっているためスループット 2 、レイテンシ 11 で実行しているようです。
Atom でも試してみました。遅いのでループ回数を 1/10 (100M) に減らしています。
乗算は 2 float/cycle、加算で 4 float/cycle 実行しています。
やはりパイプラインを埋めると HT の効果が無いことがわかります。
乗算はストール発生時 2.5 倍遅くなっています。スループット 2/ レイテンシ 5
結果として Atom も Cortex-A8 + NEON も、1 cycle あたり 2乗算or 4加算で同等です で同等、加算は Atom の方が高速です。
ただし積和の場合は Atom は 3cycle かかるところ NEON は 2cycle で済むため、
この組み合わせでは Cortex-A8 + NEON の方が速いことになります。
浮動小数演算も同クロック数の Atom より高速に実行できそうです。
2009/10/02 追記>: 加算が多いと Atom の方が速く、最適化が弱い場合は HT が有効な分 Atom が有利なこともあります。
もちろん他の命令の実行速度にもよるし、メモリアクセスや演算以外の転送命令等も絡んで
くるので一概には言えません。レイテンシも大きめなので最適化をどれだけ出来るかに
かかってきます。
厳密なデータではありませんが、以前の結果を見ても浮動小数演算以外は同サイクルの
Atom より速い速度で実行できているし、浮動小数演算も潜在能力は高そうです。
問題は VFP でしょうか。
関連エントリ
・NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
・NetWalker PC-Z1 Atom と速度比較
やりかたは前回と全く同じように、ひたすら命令を並べてスループットの測定。
今回はスレッドにも対応しておきます。
static void Thread_SSE_MULPS( Thread::Sync* barrier ) { __asm { xorps xmm0, xmm0 ~ }; barrier->Barrier( THREAD_COUNT ); for( int i= 0 ; i< VECTOR_LOOP ; i++ ){ __asm { mulps xmm0, xmm1 mulps xmm1, xmm2 mulps xmm2, xmm3 mulps xmm3, xmm4 mulps xmm4, xmm5 mulps xmm5, xmm6 mulps xmm6, xmm7 mulps xmm7, xmm0 ~ }; } barrier->Barrier( THREAD_COUNT ); } void Start_SSE( void (*func)( Thread::Sync* ), const char* msg ) { MultiThreadClass<THREAD_COUNT-1> thread; Thread::Sync barrier; barrier.Init(); thread.Start( func, &barrier ); TimerClass timer; timer.Begin(); func( &barrier ); timer.End( msg ); barrier.Quit(); thread.Join(); } void SSE_main() { Start_SSE( Thread_SSE_MULPS, "SSE_MULPS" ); }
上記のようにメモリアクセスの無い命令を前回同様に 40 個 (8x5) 並べます。
スレッドの開始と終了待ちはシグナルで同期します。一番遅いスレッドの終了に合わせているため、
速く終了したスレッドがあると若干無駄が生じる可能性があります。
メインスレッドが実行している間に、サブスレッドで何回命令を実行出来たか数えた方が
より正確な結果になるかもしれません。
Core i7 920 2.67GHz を使っています。1 スレッドあたり 1G 回ループします。
SSE_MULPS 14.32 sec Thread x1
40命令 × 1G 回ループなので 40/14.32 = 2.79 。
CPU は 2.67GHz なので 1命令 / 1cycle に近い値だけど少々オーバーしています。
ターボブーストのおかげでしょうか。スレッドを増やしてみます。
SSE_MULPS 16.19 sec Thread x4 SSE_MULPS 29.05 sec Thread x8
core 数 (4) までは大きく増えませんが、8スレッド時は、1スレッドのほぼ 2倍の時間が
かかっています。HT を含めるとハードウエアで 8スレッドですが、パイプラインに空きが
ないためか HT の効果が全く無いことがわかります。
8スレッド時の値を元に計算してみると Core i7 920 2.67GHz は mulps で
40命令 × 1G回ループ × 4 SIMD × 8 Thread / 29.05 秒 = およそ 44.06 G FLOPS
SSE には積和がありませんが SSE4.1 には DPPS があることを思い出しました。
Mul x4 + Add x3 なので試してみます。
__asm { dpps xmm0, xmm1, 255 dpps xmm1, xmm2, 255 dpps xmm2, xmm3, 255 ~
SSE_DPPS 28.74 sec Thread x1 SSE_DPPS 31.02 sec Thread x4 SSE_DPPS 57.73 sec Thread x8
mul のちょうど 2倍、スループット 2 で実行しているようです。
2 cycle で 7 float なので演算量自体は mulps に劣ります。
最初の乗算を敢えてストールするように組み替えてみます。
__asm { mulps xmm0, xmm0 mulps xmm0, xmm0 mulps xmm0, xmm0 ~
SSE_MULPS_ST 57.36 sec Thread x1 SSE_MULPS_ST 57.51 sec Thread x4 SSE_MULPS_ST 58.63 sec Thread x8
インターリーブ時は 14.32秒だったので、4倍の実行時間がかかるようになりました。
mulps のレイテンシが 4 cycle あることがわかります。パイプラインはがら空きで、
4スレッド時と 8スレッド時の実行速度に差がありません。HT がめいっぱい働いており、
それでもまだ半分ほど無駄が生じていることになります。
SSE_DPPS_ST 157.57 sec Thread x1 SSE_DPPS_ST 157.68 sec Thread x4 SSE_DPPS_ST 157.74 sec Thread x8
同じように dpps もストールさせてみると、こちらはさらに時間がかかります。
5.5 倍かかっているためスループット 2 、レイテンシ 11 で実行しているようです。
Atom でも試してみました。遅いのでループ回数を 1/10 (100M) に減らしています。
Atom 1.8GHz SSE_MULPS 4.36 sec Thread x1 Atom 1.8GHz SSE_MULPS 8.70 sec Thread x2 Atom 1.8GHz SSE_ADDPS 2.25 sec Thread x1 Atom 1.8GHz SSE_ADDPS 4.46 sec Thread x1 Atom 800MHz SSE_MULPS 10.15 sec Thread x1 Atom 800MHz SSE_MULPS 20.29 sec Thread x2
乗算は 2 float/cycle、加算で 4 float/cycle 実行しています。
やはりパイプラインを埋めると HT の効果が無いことがわかります。
Atom 1.8GHz SSE_MULPS_ST 10.88 sec Thread x1 Atom 1.8GHz SSE_MULPS_ST 10.92 sec Thread x2 Atom 1.8GHz SSE_MULPS_ST 21.92 sec Thread x4
乗算はストール発生時 2.5 倍遅くなっています。スループット 2/ レイテンシ 5
結果として Atom も Cortex-A8 + NEON も、1 cycle あたり 2乗算
ただし積和の場合は Atom は 3cycle かかるところ NEON は 2cycle で済むため、
この組み合わせでは Cortex-A8 + NEON の方が速いことになります。
浮動小数演算も同クロック数の Atom より高速に実行できそうです。
2009/10/02 追記>: 加算が多いと Atom の方が速く、最適化が弱い場合は HT が有効な分 Atom が有利なこともあります。
もちろん他の命令の実行速度にもよるし、メモリアクセスや演算以外の転送命令等も絡んで
くるので一概には言えません。レイテンシも大きめなので最適化をどれだけ出来るかに
かかってきます。
厳密なデータではありませんが、以前の結果を見ても浮動小数演算以外は同サイクルの
Atom より速い速度で実行できているし、浮動小数演算も潜在能力は高そうです。
問題は VFP でしょうか。
関連エントリ
・NetWalker PC-Z1 Cortex-A8 浮動小数演算の実行速度
・NetWalker PC-Z1 Atom と速度比較