Archives

October 2009 の記事

また今更な古い話なので興味ない方は無視してください。(前回)

当時 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に潜む罠


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 でした。

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


GPU に対応したようです。
RADEON 58 だけでなく 4000 系も含まれています。

ATI Stream Software Development Kit (SDK) v2.0 Beta Program


前回説明したとおり、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 がプリミティブの単位で座標をいじれたり増やしたり出来るのに似ています。

例えばスクリーン座標で左右反転したり、画像を複製したり。

dx11_uav_blend01.jpg

↑これは実際に 1枚のプリミティブを書き込んでいますが、Pixel Shader 内で複製して
100 ピクセル離れた場所にも同時に書き込んでいます。
わかりにくいけど、半透明の背景が同じものになっています。

(2) は現在書き込んでいるフレームバッファの値を事前に読み取って演算出来ることを
意味しています。うまく活用すれば Alpha Blend を自由にシェーダーでプログラム
出来るかもしれません。

結果だけ述べると、さすがに少々無理があったようです。

dx11_uav_blend03.jpg

↑ちらつき

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

dx11_uav_blend02.jpg

↑自分自身との合成でもうまくいくシーン


以下実際に試したプログラムと解説。

フレームバッファ用のテクスチャを 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 フォーマットタイプです。

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 より)

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)


ARM も x86 と同じように、これまで様々な命令セットの拡張が行われてきました。
例えば乗算一つとっても多数の命令が存在しており、どれを使えばよいかわからなくなる
ことがあります。
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 と速度比較


最善ケースばかりでなく、実際に使えるプログラムで試してみます。
使っているのは 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 と速度比較


Cortex-A8 の NEON のように SSE も試してみました。
やりかたは前回と全く同じように、ひたすら命令を並べてスループットの測定。
今回はスレッドにも対応しておきます。

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乗算 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 と速度比較