月別アーカイブ: 2013年2月

スレッド同期命令の比較 C++11 とコンパイラ

スレッド間の同期を取る方法は複数あります。
OS のカーネルオブジェクト、ライブラリ関数、OS 依存の API など。
他にも CAS のような最低限の atomic 操作を使って Spinlock を作ることができます。
もちろんハードウエアスレッド環境に限ります。

C++11 対応に伴いコンパイラも新しい atomic 命令に移行しています。
例えば gcc の builtin 命令は __sync_ から __atomic_ となり、
clang にも同様に __c11_atomic_ があります。

Windows の場合は OS に Interlocked~ API がありますが、
VC++ が生成する同名の Intrinsic バージョンもあります。
こちらも C++11 に従いメモリバリアを指定できる命令が追加されているようです。

// CAS
gcc      __atomic_compare_exchange_*()
clang    __c11_atomic_compare_exchange_*()
Windows  _InterlockedCompareExchange_*()

gcc 4.7.2 : Built-in functions for memory model aware atomic operations
Clang 3.3 : Clang Language Extensions
msdn : x86 Intrinsics List

↓各種 API による比較です。

(1) Windows             Win8 x86   Win8 x64
-------------------------------------------------------
SpinLock(intrinsic)         5852       6353
CriticalSection            20894      22610
Win32 Mutex              2537901    3790403


(2) Linux              Linux x86   Linux x64   Linux ARM
-------------------------------------------------------
SpinLock(gcc atomic)      12712       10641       27977
pthread mutex             13693       11796       23807


(3) MacOS X         MacOS X x86  MacOS X x64
-------------------------------------------------------
SpinLock(OSAtomic)         9505        9070
pthread mutex            563013      467785

// 実行時間 単位=us

3つのスレッドで同じカウンタをそれぞれ 50000回カウントしています。
100% 競合する状態で、ロック手段による違いを測定しています。

走らせた PC のスペックが異なることと、数値の変動が大きいため
単純な比較ができないのでご注意ください。
同じ OS 内で API による違いだけ見てください。

(1) Windows (Windows 8 + VS2012 Express)

カーネルオブジェクトである Mutex は非常に遅くなっています。
CriticalSection の方が2桁高速ですが、_Interlocked 命令を使った
SpinLock の方が更に速い結果となっています。

(2) Linux (Ubuntu 12.10 / 13.04)

x86/x64 は VMware Player によるテストなのでご了承ください。
特筆すべき点は pthread_mutex が SpinLock と全く変わらないか、
むしろ速いということです。

SpinLock を作る場合に atomic API の使い方を間違えると、
pthread_mutex よりも遅くなったり、ロックに失敗したりするようです。

X86/x64 の場合 gcc (4.7.2) builtin の __atomic_ でメモリアクセス方法に
__ATOMIC_SEQ_CST を指定すると pthread_mutex よりも低速でした。
__ATOMIC_RELAXED で pthread と同等になります。
また Legacy API ですが __sync_ を使った場合も pthread と同等の
速度となりました。

ARM (Cortex-A9) では RELAXED の場合メモリアクセスが競合し
正しくブロックされません。
上記テストは ARM の場合のみ SEQ_CST を指定しています。
また __sync_ も RELAXED 相当なためかロックされませんでした。

(3) MacOS X (10.8)

OSAtomic と phtread_mutex で大きな隔たりがあります。
同じ pthread の mutex でも、Linux と違い Windows のように
カーネルオブジェクトとして実装されている可能性があります。

API が同一でも OS と CPU によって挙動が違うことがわかりました。
Linux では pthread_mutex が最もよい選択ですが、
MacOS X では逆に pthread_mutex を避けた方が良いようです。

テストプログラムは下記のとおり。

template
class LockTest {
    LockT  lock;
    static int Counter;
    enum {
        MAX_LOOP= 50000
    };
public:
    void thread_func()
    {
        for( int i= 0 ; i< MAX_LOOP ; i++ ){
            lock.Lock();
            Counter++;
            lock.Unlock();
        }
    }
    void Run()
    {
        ClockTimer time;

        Counter= 0;
        Thread t0, t1, t2;

        t0.Run( this, &LockTest::thread_func );
        t1.Run( this, &LockTest::thread_func );
        t2.Run( this, &LockTest::thread_func );

        t0.Join();
        t1.Join();
        t2.Join();

        time.Output();
        assert( Counter == MAX_LOOP * 3 );
    }
};

関連?エントリ
C++11 Rvalue Reference 右辺値参照

OpenGL ES 3.0 と GPU

Khronos Conformant Products に OpenGL ES 3.0 の名前が
登場しています。

Intel HD Graphics 4000/2500
PowerVR Rouge Hood (PowerVR Series 6)
Snapdragon MSM8974 (Adreno 330)
Snapdragon MSM8064 (Adreno 320)

OpenGLES 3.0 API は GL 4.3 の互換モードや Emulator ですでにテスト可能(詳細)
ですが、モバイルデバイスでも HW の対応が進行しています。
ARM Mali はもちろん Vivante もすでに 3.0 に対応しているとのことです。

Vivante

↑の i.MX6 Quad といえば thanko Android SmartTV Quad-core に使われています。

唯一対応が未定で 2.0 のままなのは Tegra シリーズだけとなっています。
NVIDIA には Desktop GPU があるため余裕があるのかもしれません。

関連エントリ
OpenGL 4.3/GLES 3.0 次の圧縮テクスチャ ASTC
OpenGL ES 3.0 と OpenGL ES 2.0 の互換性
OpenGL ES 3.0 と Shader Model の関係、まとめ wiki の更新
OpenGL ES 2.0/3.0 Emulator
OpenGL 4.3 と GL_ARB_ES3_compatibility

Nexus 7 の Ubuntu で ARM の abi softfp と hard-float を比べる

Nexus 7 の Ubuntu 13.04 は armhf (hard-float) です。
Android NDK は softfp なので、
Ubuntu の方が関数呼び出しが効率化されていると考えられます。
比べてみました。

// 元のソース
float func2( float a, float b, float c )
{
    return  a * b + c;
}

float func3( float a, float b, float c )
{
    return  a + b - c;
}

float func1( float a, float b )
{
    return  a * b + a;
}

↓gcc によるコンパイル結果 (softfp)

// -marm -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3

    fmsr    s14, r0
    fmsr    s13, r2
    fmsr    s15, r1
    fmacs   s13, s14, s15
    fmrs    r0, s13
    bx  lr

    fmsr    s13, r0
    fmsr    s14, r1
    fadds   s15, s13, s14
    fmsr    s13, r2
    fsubs   s13, s15, s13
    fmrs    r0, s13
    bx  lr

    fmsr    s13, r0
    fmsr    s15, r1
    fmacs   s13, s13, s15
    fmrs    r0, s13
    bx  lr

soft といっても浮動小数点演算をエミュレーション実行しているわけではなく、
上記のように VFP や NEON 等の HW 演算ユニットが使われています。

あくまで ABI (Calling Convention) の話で、関数の呼び出し時のレジスタの
使われ方が異なります。
softfp の場合は FPU (VFP) が無い場合 (soft) と互換性が取れるように、
レジスタ渡しの場合に浮動小数点値も整数レジスタ(r)に入ります。

上の結果でも、毎回整数レジスタ(r)に入った引数を VFP レジスタ(s)へ
コピーしていることがわかります。

-mfloat-abi=hard を指定すると↓下記のように不要な転送が無くなりました。
引数や戻り値としてそのまま VFP レジスタ(s)が使われています。

// -marm -march=armv7-a -mfloat-abi=hard -mfpu=vfpv3

    fmacs   s2, s0, s1
    fcpys   s0, s2
    bx  lr

    fadds   s0, s0, s1
    fsubs   s0, s0, s2
    bx  lr

    fmacs   s0, s0, s1
    bx  lr

↓さらに vfpv4 を指定すると、fmacs の代わりに vfma が使われていることがわかります。

// -marm -march=armv7-a -mfloat-abi=hard -mfpu=vfpv4

    vfma.f32    s2, s0, s1
    fcpys   s0, s2
    bx  lr

    fadds   s0, s0, s1
    fsubs   s0, s0, s2
    bx  lr

    vfma.f32    s0, s0, s1
    bx  lr

NEON 命令も試してみました。

// 元のソース
#include   

float32x4_t func4( float32x4_t a, float32x4_t b )
{
    return  a * b + a;
}

-ffast-math を付けると neon 命令に変換できます。

↓softfp では 128bit x2 の値をレジスタだけで渡すことができません。
2つ目の引数が stack に入っています。

// -marm -march=armv7-a -mfloat-abi=softfp -mfpu=neon -ffast-math

	vmov	d16, r0, r1  @ v4sf
	vmov	d17, r2, r3
	vld1.64	{d18-d19}, [sp:64]
	vmla.f32	q8, q9, q8
	vmov	r0, r1, d16  @ v4sf
	vmov	r2, r3, d17
	bx	lr

↓hard の場合すべてレジスタで受け渡し可能となります。

// -marm -march=armv7-a -mfloat-abi=hard -mfpu=neon -ffast-math

	vmla.f32	q0, q1, q0
	bx	lr

以上より hard-float の場合に下記の 2つのメリットあるようです。
(他にもあるかもしれません)

・整数レジスタとの転送が不要となる
・引数として利用可能なレジスタの個数が増える

具体的な速度は測定していませんが、より高速に実行できると考えられます。

Android NDK だけでなく iOS の新しいアーキテクチャ armv7s でも softfp
相当となっているようです。

関連エントリ
Nexus 7 上に開発環境をつくる (3) Ubuntu
Nexus 7 上に開発環境をつくる (2) Bluetooth と OpenGL ES 2.0
Android Tablet Nexus 7 上に開発環境をつくる (Ubuntu)

Nexus 7 上に開発環境をつくる (3) Ubuntu

Nexus 7 に Ubuntu 13.04を入れています。
Bluetooth が使えるようになりかなり実用度が増しました。
下記はあくまで 2013/02/14 現在のものです。
日々更新されていますので、そのまま鵜呑みにせずその時点での
最新情報を探すようにしてください。

●オンラインストレージ

NetWalker の時は ARM で動く Dropbox が無く不便だったのですが、
今では Ubuntu One が入っているので全く困らなくなりました。
同様に使えるオンラインストレージで、もちろん ARM で動きます。
5GB まで無料。

● WiMAX / 有線LAN

WiMAX ルーターに Wi-Fi でつながるのは当たり前ですが、
USB による有線接続も可能でした。試したのは下記の 2機種。

・NEC Aterm WM3600R
・NEC Aterm WM3800R

USB Host にケーブルでつなぐだけで認識します。
駅前など Wi-Fi が混雑していて安定しない場所で使えるかもしれません。
有線 LAN アダプタ LUA3-U2-ATX も使えています。

●テザリング

同様に SB iPhone5, au HTC J butterfly HTL21 も USB 接続しましたが
こちらはつながりません。
もちろん Wi-Fi や Bluetooth ではテザリングできます。

Bluetooth の場合はペアリングだけではだめで、
「Use your mobile phone as a network device (PAN/NAP)」
のチェックが必要です。
その後端末側から接続を選ぶ→右上の Network のポップアップから
端末名を選択する流れになります。

●スクリーンキーボード

リサイズ出来ます。
横画面で大きめにしておくとタッチしやすくなります。

マウスポインタアイコンで拡張パネルが開き、右ボタン等の操作が可能ですが、
複雑な操作をしていると固まる事が多いです。
マウスは使えるので、タッチが反応しない場合や突然ロック画面に飛ばされた
場合は、マウスをつないで再起動した方が良いです。

●armhf

こちらのコメントにも書きましたが hard-float なので、
NDK より呼び出し効率が上がっているものと考えられます。
続きます。

関連エントリ
Android Tablet Nexus 7 上に開発環境をつくる (Ubuntu)
Nexus 7 上に開発環境をつくる (2) Bluetooth と OpenGL ES 2.0

C++11 Rvalue Reference 右辺値参照

C++11 の新機能を使うとオブジェクトの無駄なコピーを減らすことができます。

記述が簡単になるなど、C++11 にはいろいろ便利に使える追加機能があります。
Rvalue Reference の場合はむしろコードが増えるのですが、
うまく使うことでプログラムの動作を効率化することができます。
とりあえず適当に文字列型を作ってみます。

class MyString {
    char*   Name;
private:
    void Clear()
    {
        delete[] Name;
        Name= NULL;
    }
    void Copy( const char* str )
    {
        Clear();
        size_t  length= strlen(str)+1;
        Name= new char[length];
        memcpy( Name, str, sizeof(char)*length );
    }
    void DeepCopy( const MyString& src )
    {
        Copy( src.Name );
    }
public:
    MyString() : Name( NULL ) {}
    MyString( const MyString& src ) : Name( NULL )
    {
        DeepCopy( src );
    }
    MyString( const char* str ) : Name( NULL )
    {
        Copy( str );
    }
    ~MyString()
    {
        Clear();
    }
    MyString& operator=( const MyString& src )
    {
        DeepCopy( src );
        return  *this;
    }
};

copy 時に文字列バッファの複製が発生します。
例えばこんな感じ。

MyString  str1;
MyString  str2;
str1= "abcdefg";              // -- (1)
str2= MyString( "ABCDEF" );   // -- (2)
str1= str2;                   // -- (3)

C++11 の Rvalue Reference を使うと特定のケースで copy を move に変換出来ます。
対応コードを追加したのが下記の形。

#include    
#include    

#ifndef IS_CPP11
# define    IS_CPP11    (__cplusplus > 199711L)
#endif

int  AllocCount;
int  FreeCount;

class MyString {
    char*   Name;
private:
    void Clear()
    {
        if( Name ){
            FreeCount++;
        }
        delete[] Name;
        Name= NULL;
    }
    void Copy( const char* str )
    {
        Clear();
        size_t  length= strlen(str)+1;
        Name= new char[length];
        memcpy( Name, str, sizeof(char)*length );
        AllocCount++;
    }
    void DeepCopy( const MyString& src )
    {
        Copy( src.Name );
    }
public:
    MyString() : Name( NULL ) {}
    MyString( const MyString& src ) : Name( NULL )
    {
        DeepCopy( src );
    }
    MyString( const char* str ) : Name( NULL )
    {
        Copy( str );
    }
    ~MyString()
    {
        Clear();
    }
    MyString& operator=( const MyString& src )
    {
        DeepCopy( src );
        return  *this;
    }
    //-- ↓ここから追加分
#if IS_CPP11
    MyString( MyString&& src )
    {
        Name= src.Name;
        src.Name= NULL;
    }
    MyString& operator=( MyString&& src )
    {
        char*   tmp= Name;
        Name= src.Name;
        src.Name= tmp;
        return  *this;
    }
#endif
};

これで転送元を捨てても構わない場合に copy ではなく破壊転送 (move) が行われます。
上の場合はメモリを確保し直す必要がないので効率が上がります。

転送元を捨てても構わないケースの代表が、
一時確保されたオブジェクトである rvalue 右辺値です。
また明示的に std::move() をつければ任意の値を破壊転送することができます。

これで C++11 の場合は (1),(2) において copy が消えます。
どちらも一時的な object が作られているからです。

さらに適当なベクターを作ってみます。

template
class MyVector {
    T*      Buffer;
    T*      Ptr;
    size_t  BufferSize;
private:
    void Clear()
    {
        delete[] Buffer;
        Buffer= Ptr= NULL;
    }
public:
    MyVector() : Buffer( NULL ), Ptr( NULL ), BufferSize( 0 ) {};
    MyVector( size_t size ) : BufferSize( size )
    {
        Ptr= Buffer= new T[size];
    }
    ~MyVector()
    {
        Clear();
    }
    size_t Size() const
    {
        return  Ptr - Buffer;
    }
    T& operator[]( size_t index )
    {
        return  Buffer[index];
    }
    const T& operator[]( size_t index ) const
    {
        return  Buffer[index];
    }
    void PushBack( const T& src )
    {
        assert( Ptr < Buffer + BufferSize );
        *Ptr++= src;
    }
#if IS_CPP11
    void PushBack( T&& src )
    {
        assert( Ptr < Buffer + BufferSize );
        *Ptr++= std::forward(src);
    }
#endif
};

ループさせてどの程度高速化されるか測ってみます。

// main.cpp
int main()
{
    AllocCount= 0;
    FreeCount= 0;
    {
        const int   LOOP_MAX= 100000;
        MyVector  string_list( LOOP_MAX );

        for( int i= 0 ; i< LOOP_MAX ; i++ ){
            string_list.PushBack( "12345" );
        }
    }
    printf( "alloc=%d  free=%d\n", AllocCount, FreeCount );
    return  0;
}

差が出るように作ったので C++11 でコンパイルした方が速いのは当然なのですが、
実際にメモリ確保の回数が半減していることがわかります。

Prog  time         output
c03   0m0.015s     alloc=200000  free=200000
c11   0m0.009s     alloc=100000  free=100000
# Makefile
mac:
	clang++ -std=c++11 -stdlib=libc++  -O4 main.cpp  -o c11
	clang++ -std=c++03 -stdlib=libc++  -O4 main.cpp  -o c03

linux:
	g++ -std=c++11   -O4 main.cpp  -o g11
	g++ -std=c++03   -O4 main.cpp  -o g03
	clang++ -std=c++11 -O3 main.cpp  -o c11
	clang++ -std=c++03 -O3 main.cpp  -o c03

win:
	cl /O2 main.cpp /Fev11.exe -DIS_CPP11=1
	cl /O2 main.cpp /Fev03.exe -DIS_CPP11=0

特定のケースで明らかな無駄を省いているだけなので、通常はここまで差がでません。
例えば (3) の場合何もせずに copy を減らすことはできません。

move の場合は結局 move 用に別の API を設けていることになります。
またオブジェクトだけでなく、copy が発生する API を経由する場合も
copy の他に move 用の別のルートを追加する必要があります。
途中で move() や forward() を付け忘れると、オブジェクトまで到達しないで
途切れてしまいます。

上の MyVector 自体も複製できるようにしてみます。

template
class MyVector {
    T*      Buffer;
    T*      Ptr;
    size_t  BufferSize;
private:
    void Clear()
    {
        delete[] Buffer;
        Buffer= Ptr= NULL;
    }
#if IS_CPP11
    void Move( MyVector&& src )
    {
        Clear();
        BufferSize= src.BufferSize;
        Ptr= Buffer= new T[BufferSize];
        for( int i= 0 ; i< BufferSize ; i++ ){
            *Ptr++= std::move(src[i]);  //-- (4)
        }
    }
#endif
    void DeepCopy( const MyVector& src )
    {
        Clear();
        BufferSize= src.BufferSize;
        Ptr= Buffer= new T[BufferSize];
        for( int i= 0 ; i< BufferSize ; i++ ){
            *Ptr++= src[i];
        }
    }
public:
    MyVector() : Buffer( NULL ), Ptr( NULL ), BufferSize( 0 ) {};
    MyVector( size_t size ) : BufferSize( size )
    {
        Ptr= Buffer= new T[size];
    }
    ~MyVector()
    {
        Clear();
    }
    size_t Size() const
    {
        return  Ptr - Buffer;
    }
    void PushBack( const T& src )
    {
        assert( Ptr < Buffer + BufferSize );
        *Ptr++= src;
    }
#if IS_CPP11
    void PushBack( T&& src )
    {
        assert( Ptr < Buffer + BufferSize );
        *Ptr++= std::forward(src);
    }
#endif
    T& operator[]( size_t index )
    {
        return  Buffer[index];
    }
    const T& operator[]( size_t index ) const
    {
        return  Buffer[index];
    }


    MyVector& operator=( const MyVector& src )
    {
        DeepCopy( src );
        return  *this;
    }
#if IS_CPP11
    MyVector& operator=( MyVector&& src )
    {
        Move( std::move(src) );
        return  *this;
    }
#endif
};

下記のように MyVector を代入できるようになりました。
この (5) のケースでは完全な move となり string のメモリ確保が発生しません。

int main()
{
    AllocCount= 0;
    FreeCount= 0;
    {
        const int   LOOP_MAX= 100000;
        MyVector  string_list( LOOP_MAX );

        for( int i= 0 ; i< LOOP_MAX ; i++ ){
            string_list.PushBack( "12345" );
        }

        MyVector  string_list2( LOOP_MAX );
        string_list2= std::move(string_list);    // -- (5)
    }
    printf( "alloc=%d  free=%d\n", AllocCount, FreeCount );
    return  0;
}

ですが std::move() を付け忘れると move でなく MyString の copy になります。
下記のように alloc/free が増えます。(5) でも同じです。

c03    alloc=300000  free=300000
c11    alloc=200000  free=200000   ( (5) or (6) の move 無し )
c11    alloc=100000  free=100000