37

私が理解している限り、移動セマンティクスを追加する目的の 1 つは、「一時的な」オブジェクトをコピーするための特別なコンストラクターを呼び出してコードを最適化することです。たとえば、この回答では、そのようなものを最適化するために使用できることがわかりますstring a = x + y。x+y は右辺値式であるため、ディープ コピーの代わりに、文字列へのポインターと文字列のサイズのみをコピーできます。しかし、ご存知のように、最新のコンパイラは戻り値の最適化をサポートしているため、移動セマンティクスを使用しないと、コードはコピー コンストラクターをまったく呼び出しません。

それを証明するために、次のコードを書きます。

#include <iostream>

struct stuff
{
        int x;
        stuff(int x_):x(x_){}
        stuff(const stuff & g):x(g.x)
        {
                std::cout<<"copy"<<std::endl;
        }
};   
stuff operator+(const stuff& lhs,const stuff& rhs)
{
        stuff g(lhs.x+rhs.x);
        return g;
}
int main()
{
        stuff a(5),b(7);
        stuff c = a+b;
}

そして、最適化モードで VC++2010 と g++ で実行した後、空の出力が得られます。

それがなくてもコードがより速く動作する場合、それはどのような最適化ですか? 私の理解が間違っていることを説明していただけますか?

4

8 に答える 8

27

移動セマンティクスは、最適化デバイスとして使用できる場合でも、最適化デバイスと考えるべきではありません。

オブジェクト (関数パラメーターまたは戻り値のいずれか) のコピーが必要な場合は、RVO とコピー省略が可能な場合にジョブを実行します。Move セマンティクスが役に立ちますが、それよりも強力です。

Move セマンティクスは、渡されたオブジェクトが一時的なオブジェクト (右辺値参照にバインド) であるか、名前付きの "標準" オブジェクト (いわゆるconst lvalue ) であるかにかかわらず、何か異なることをしたい場合に便利です。たとえば、一時オブジェクトのリソースを盗む場合は、セマンティクスを移動する必要があります (例: a が指すコンテンツを盗むことができます)。std::unique_ptr

Move セマンティクスを使用すると、現在の標準では不可能なコピー不可能なオブジェクトを関数から返すことができます。また、コピーできないオブジェクトを他のオブジェクトの中に入れることができ、それらのオブジェクトは、含まれているオブジェクトが含まれている場合、自動的に移動可能になります。

コピー不可能なオブジェクトは、エラーが発生しやすいコピー コンストラクターの実装を強制しないため、優れています。多くの場合、コピー セマンティクスはあまり意味がありませんが、ムーブ セマンティクスは意味があります (考えてみてください)。

これにより、コピー可能でない場合でも可動std::vector<T>クラスを使用できます。Tクラス テンプレートは、std::unique_ptrコピー不可能なオブジェクト (ポリモーフィック オブジェクトなど) を扱う場合にも優れたツールです。

于 2011-02-17T16:46:35.243 に答える
11

掘り下げた後、 Stroustrup's FAQで右辺値参照を使用した最適化のこの優れた例を見つけました。

はい、スワップ機能:

    template<class T> 
void swap(T& a, T& b)   // "perfect swap" (almost)
{
    T tmp = move(a);    // could invalidate a
    a = move(b);        // could invalidate b
    b = move(tmp);      // could invalidate tmp
}

これにより、あらゆる種類の型に対して最適化されたコードが生成されます (ムーブ コンストラクターがあると仮定します)。

編集:また、RVOはこのようなものを最適化できません(少なくとも私のコンパイラでは):

stuff func(const stuff& st)
{
    if(st.x>0)
    {
        stuff ret(2*st.x);
        return ret;
    }
    else
    {
        stuff ret2(-2*st.x);
        return ret2;
    }
}

この関数は常にコピー コンストラクターを呼び出します (VC++ で確認)。クラスを移動コンストラクタよりも高速に移動できる場合は、最適化が行われます。

于 2011-02-17T17:02:51.630 に答える
7

あなたのものが文字列のようにヒープに割り当てられたメモリを持つクラスであり、それが容量の概念を持っていたと想像してください。容量を幾何学的に増大させる演算子+=を指定します。C ++ 03では、これは次のようになります。

#include <iostream>
#include <algorithm>

struct stuff
{
    int size;
    int cap;

    stuff(int size_):size(size_)
    {
        cap = size;
        if (cap > 0)
            std::cout <<"allocating " << cap <<std::endl;
    }
    stuff(const stuff & g):size(g.size), cap(g.cap)
    {
        if (cap > 0)
            std::cout <<"allocating " << cap <<std::endl;
    }
    ~stuff()
    {
        if (cap > 0)
            std::cout << "deallocating " << cap << '\n';
    }

    stuff& operator+=(const stuff& y)
    {
        if (cap < size+y.size)
        {
            if (cap > 0)
                std::cout << "deallocating " << cap << '\n';
            cap = std::max(2*cap, size+y.size);
            std::cout <<"allocating " << cap <<std::endl;
        }
        size += y.size;
        return *this;
    }
};

stuff operator+(const stuff& lhs,const stuff& rhs)
{
    stuff g(lhs.size + rhs.size);
    return g;
}

また、一度に2つ以上のものを追加したいとします。

int main()
{
    stuff a(11),b(9),c(7),d(5);
    std::cout << "start addition\n\n";
    stuff e = a+b+c+d;
    std::cout << "\nend addition\n";
}

私の場合、これは次のように出力されます。

allocating 11
allocating 9
allocating 7
allocating 5
start addition

allocating 20
allocating 27
allocating 32
deallocating 27
deallocating 20

end addition
deallocating 32
deallocating 5
deallocating 7
deallocating 9
deallocating 11

計算する3つの割り当てと2つの割り当て解除をカウントします。

stuff e = a+b+c+d;

次に、移動セマンティクスを追加します。

    stuff(stuff&& g):size(g.size), cap(g.cap)
    {
        g.cap = 0;
        g.size = 0;
    }

..。

stuff operator+(stuff&& lhs,const stuff& rhs)
{
        return std::move(lhs += rhs);
}

もう一度実行すると、次のようになります。

allocating 11
allocating 9
allocating 7
allocating 5
start addition

allocating 20
deallocating 20
allocating 40

end addition
deallocating 40
deallocating 5
deallocating 7
deallocating 9
deallocating 11

私は今、2つの割り当てと1つの割り当て解除になっています。これは、より高速なコードに変換されます。

于 2011-02-17T17:29:05.030 に答える
4

多くの場所があり、そのうちのいくつかは他の回答で言及されています。

大きな問題の 1 つは、サイズを変更するときにstd::vector、元のメモリをコピーして破棄するのではなく、移動対応オブジェクトを古いメモリ ロケーションから新しいメモリ ロケーションに移動することです。

さらに、右辺値参照は可動型の概念を可能にします。これはセマンティックの違いであり、単なる最適化ではありません。unique_ptrC++03 では不可能でしauto_ptrた。

于 2011-02-17T19:00:58.673 に答える
1

この特定のケースが既存の最適化によって既にカバーされているからといって、右辺値参照が役立つ他のケースが存在しないという意味ではありません。

Move 構築により、インライン化できない関数から一時が返された場合でも最適化できます (おそらく、仮想呼び出しまたは関数ポインターを介して)。

于 2011-02-17T16:45:05.597 に答える
1

投稿された例は const 左辺値参照のみを使用するため、移動セマンティクスを明示的に適用することはできません。そこには単一の右辺値参照がないためです。右辺値参照なしで型を実装した場合、セマンティクスを移動するとコードがどのように高速化されるのでしょうか?

さらに、コードはすでに RVO と NRVO でカバーされています。Move セマンティクスは、これら 2 つよりもはるかに多くの状況に適用されます。

于 2011-02-17T16:50:20.573 に答える
0

この行は、最初のコンストラクターを呼び出します。

stuff a(5),b(7);

Plus 演算子は、明示的な共通左辺値参照を使用して呼び出されます。

stuff c = a + b;

演算子のオーバーロード メソッド内では、コピー コンストラクターが呼び出されていません。ここでも、最初のコンストラクターのみが呼び出されます。

stuff g(lhs.x+rhs.x);

代入は RVO で行うため、コピーは不要です。返されたオブジェクトから「c」へのコピーは必要ありません。

stuff c = a+b;

参照がないためstd::cout、コンパイラcは値が使用されないように注意します。次に、プログラム全体が取り除かれ、空のプログラムになります。

于 2013-09-07T21:33:27.730 に答える