1

私は C++ の式テンプレートの概念を理解しようとしています。そのため、サンプル コードなどを組み合わせて単純なベクトルと関連する式テンプレート インフラストラクチャを生成し、バイナリ演算子 (+、-、​​) のみをサポートしています。

すべてがコンパイルされますが、標準の手書きループと式テンプレートのバリアントのパフォーマンスの違いが非常に大きいことに気付きました。ET は手書きの 2 倍近く遅いです。違いを期待していましたが、それほどではありませんでした。

完全なコード リストは次の場所にあります。

https://gist.github.com/BernieWt/769a4a3ceb90bb0cae9e

(乱雑なコードで申し訳ありません。)

.

要するに、私は基本的に次の 2 つのループを比較しています。

ET:

for (std::size_t i = 0 ; i < rounds; ++i)
{
   v4 = ((v0 - v1) + (v2 * v3)) + v4;
   total += v4[0];
}

ハードウェア:

for (std::size_t i = 0 ; i < rounds; ++i)
{
   for (std::size_t x = 0; x < N; ++x)
   {
      v4[x] = (v0[x] - v1[x]) + (v2[x] * v3[x]) + v4[x];
   }
   total += v4[0];
}

出力を逆アセンブルすると、次のようになります。違いは明らかに、追加の memcpy と、ET バリアントのリターン中に発生するいくつかの 64 ビット ロードです。

Standard Loop                           | Expression Template
----------------------------------------+--------------------------------
L26:                                    | L12:
xor   edx, edx                          | xor   edx, edx
jmp   .L27                              | jmp   .L13
L28:                                    | L14:
movsd xmm3, QWORD PTR [rsp+2064+rdx*8]  | movsd xmm3, QWORD PTR [rsp+2064+rdx*8]
L27:                                    | L13:
movsd xmm2, QWORD PTR [rsp+1040+rdx*8]  | movsd xmm1, QWORD PTR [rsp+1552+rdx*8]
movsd xmm1, QWORD PTR [rsp+16+rdx*8]    | movsd xmm2, QWORD PTR [rsp+16+rdx*8]
mulsd xmm2, QWORD PTR [rsp+1552+rdx*8]  | mulsd xmm1, QWORD PTR [rsp+1040+rdx*8]
subsd xmm1, QWORD PTR [rsp+528+rdx*8]   | subsd xmm2, QWORD PTR [rsp+528+rdx*8]
addsd xmm1, xmm2                        | addsd xmm1, xmm2
addsd xmm1, xmm3                        | addsd xmm1, xmm3
movsd QWORD PTR [rsp+2064+rdx*8], xmm1  | movsd QWORD PTR [rsp+2576+rdx*8], xmm1
add   rdx, 1                            | add   rdx, 1
cmp   rdx, 64                           | cmp   rdx, 64
jne   .L28                              | jne   .L14
                                        | mov   dx, 512
                                        | movsd QWORD PTR [rsp+8], xmm0
                                        | lea   rsi, [rsp+2576]
                                        | lea   rdi, [rsp+2064]
                                        | call  memcpy
movsd xmm3, QWORD PTR [rsp+2064]        | movsd xmm0, QWORD PTR [rsp+8]
sub   rcx, 1                            | sub   rbx, 1
                                        | movsd xmm3, QWORD PTR [rsp+2064]
addsd xmm0, xmm3                        | addsd xmm0, xmm3
jne   .L26                              | jne   .L12

私の質問は次のとおりです。この時点で、コピーを削除する方法に行き詰まっています。基本的に、コピーなしで v4 をその場で更新したいと考えています。これを行う方法についてのアイデアはありますか?

注 1: GCC 4.7/9、Clang 3.3、VS2010/2013 を試しました。言及されているすべてのコンパイラでほぼ同じパフォーマンス プロファイルが得られます。

注2: vecのbin_expを前方宣言してから、次の代入演算子を追加し、bin_expから変換演算子を削除しようとしましたが、役に立ちませんでした

template<typename LHS, typename RHS, typename Op>
inline vec<N>& operator=(const bin_exp<LHS,RHS,Op,N>& o)
{
   for (std::size_t i = 0; i < N; ++i)  { d[i] = o[i]; }
   return *this;
}

更新注 2 に示されている解決策は、実際には正しいものです。コンパイラは、手書きのループとほぼ同じコードを生成します。

.

別のメモとして、ET バリアントのユースケースを次のように書き直すと、次のようになります。

auto expr = ((v0 - v1) + (v2 * v3)) + v4;

//auto& expr = ((v0 - v1) + (v2 * v3)) + v4;   same problem
//auto&& expr = ((v0 - v1) + (v2 * v3)) + v4;   same problem

for (std::size_t i = 0 ; i < rounds; ++i)
{
   v4 = expr
   total += v4[0];
}

クラッシュは、ET のインスタンス化中に生成される一時 (右辺値) が割り当て前に破棄されるために発生します。C++11 を使用してコンパイラ エラーを引き起こす方法があるかどうか疑問に思っていました。

4

2 に答える 2

0

式テンプレートのポイントは、部分式の評価によって、コストが発生し、メリットがない一時的な式が発生する可能性があることです。コードでは、リンゴとリンゴを実際に比較していません。比較する2つの選択肢は次のとおりです。

// Traditional
vector operator+(vector const& lhs, vector const& rhs);
vector operator-(vector const& lhs, vector const& rhs);
vector operator*(vector const& lhs, vector const& rhs);

演算のこれらの定義により、解決したい式は次のようになります。

v4 = ((v0 - v1) + (v2 * v3)) + v4;

なります(すべての一時的な名前を提供します):

auto __tmp1 = v0 - v1;
auto __tmp2 = v2 * v3;
auto __tmp3 = __tmp1 + __tmp2;
auto __tmp4 = __tmp3 + v4;
// assignment is not really part of the expression
v4 = __tmp4;

ご覧のとおり、4 つの一時オブジェクトがありますが、式テンプレートを使用すると、これらの操作のいずれかが場違いな値を生成するため、単一の一時オブジェクトになります。

手巻きバージョンのコードでは、同じ操作を実行していません。ループ全体をアンロールし、完全な操作の知識を利用していますが、実際には同じ操作ではありません。最後に割り当てることを知っているためです。式を要素の 1 つに変換すると、式は次のように変換されます。

v4 += ((v0 - v1) + (v2 * v3));

ここで、式の一部となるベクトルの 1 つに代入する代わりに、新しい vector を作成するとどうなるかを考えてみてくださいv5。次の式を試してください。

auto v5 = ((v0 - v1) + (v2 * v3)) + v4;

式テンプレートの魔法は、手動で実装するのと同じくらい効率的な、テンプレートで動作する演算子の実装を提供できることです。ユーザー コードははるかに単純で、エラーが発生しにくくなっています (すべての要素を反復処理する必要はありません)。エラーの可能性があるベクトルの数、またはベクトルの内部表現としてのメンテナンスのコストは、算術演算が実行される各場所で知る必要があります)

基本的に、コピーなしでv4をその場で更新したい

式テンプレートとベクターの現在のインターフェイスを使用すると、一時的なものとコピーにお金がかかります。その理由は、式の (概念的な) 評価中に新しいベクトルが作成されるためです。これv4 = ... + v4;は と同等であることは明らかなように思われるかもしれませんv4 += ...が、変換はコンパイラまたは式テンプレートでは実行できません。一方、式テンプレートを取り、その場で操作を行うvector::operator+=(おそらく) のオーバーロードを提供することもできます。operator=


式テンプレートから代入する代入演算子を提供し、g++4.7 -O2 でビルドすると、これは両方のループに対して生成されたアセンブリです。

    call    __ZNSt6chrono12system_clock3nowEv   |    call    __ZNSt6chrono12system_clock3nowEv  
    movl    $5000000, %ecx                      |    movl    $5000000, %ecx                     
    xorpd   %xmm0, %xmm0                        |    xorpd   %xmm0, %xmm0                       
    movsd   2064(%rsp), %xmm3                   |    movsd   2064(%rsp), %xmm3                  
    movq    %rax, %rbx                          |    movq    %rax, %rbx                         
    .align 4                                    |    .align 4                                   
L9:                                             |L15:                                           
    xorl    %edx, %edx                          |    xorl    %edx, %edx                         
    jmp L8                                      |    jmp L18                                    
    .align 4                                    |    .align 4                                   
L32:                                            |L16:                                           
    movsd   2064(%rsp,%rdx,8), %xmm3            |    movsd   2064(%rsp,%rdx,8), %xmm3           
L8:                                             |L18:                                           
    movsd   1552(%rsp,%rdx,8), %xmm1            |    movsd   1040(%rsp,%rdx,8), %xmm2           
    movsd   16(%rsp,%rdx,8), %xmm2              |    movsd   16(%rsp,%rdx,8), %xmm1             
    mulsd   1040(%rsp,%rdx,8), %xmm1            |    mulsd   1552(%rsp,%rdx,8), %xmm2           
    subsd   528(%rsp,%rdx,8), %xmm2             |    subsd   528(%rsp,%rdx,8), %xmm1            
    addsd   %xmm2, %xmm1                        |    addsd   %xmm2, %xmm1                       
    addsd   %xmm3, %xmm1                        |    addsd   %xmm3, %xmm1                       
    movsd   %xmm1, 2064(%rsp,%rdx,8)            |    movsd   %xmm1, 2064(%rsp,%rdx,8)           
    addq    $1, %rdx                            |    addq    $1, %rdx                           
    cmpq    $64, %rdx                           |    cmpq    $64, %rdx                          
    jne L32                                     |    jne L16                                    
    movsd   2064(%rsp), %xmm3                   |    movsd   2064(%rsp), %xmm3                  
    subq    $1, %rcx                            |    subq    $1, %rcx                           
    addsd   %xmm3, %xmm0                        |    addsd   %xmm3, %xmm0                       
    jne L9                                      |    jne L15                                    
    movsd   %xmm0, (%rsp)                       |    movsd   %xmm0, (%rsp)                      
    call    __ZNSt6chrono12system_clock3nowEv   |    call    __ZNSt6chrono12system_clock3nowEv  
于 2013-11-21T03:39:18.770 に答える
0

C++11 では、不必要なコピーの数を減らすためにムーブ セマンティクスが導入されました。

あなたのコードはかなり難読化されていますが、これでうまくいくはずです

あなたのstruct vec代わりに

value_type d[N];

std::vector<value_type> d;

d(N)コンストラクターの初期化リストに追加します。std::array当然の選択ですが、それは各要素を移動することを意味します (つまり、回避しようとしているコピー)。

次に移動コンストラクターを追加します。

vec(vec&& from): d(std::move(from.d))
{
}

move コンストラクターにより、新しいオブジェクトは古いオブジェクトの内容を「盗む」ことができます。つまり、ベクトル (配列) 全体をコピーする代わりに、配列へのポインターのみがコピーされます。

于 2013-11-21T03:24:37.030 に答える