C ++の偽のコピー操作を見つける方法は?


11

最近、私は次のことをしました

struct data {
  std::vector<int> V;
};

data get_vector(int n)
{
  std::vector<int> V(n,0);
  return {V};
}

このコードの問題は、構造体が作成されるときにコピーが発生し、解決策は代わりにreturn {std :: move(V)}を書き込むことです。

このような偽のコピー操作を検出するリンターまたはコードアナライザーはありますか?cppcheck、cpplint、clang-tidyのいずれも実行できません。

編集:私の質問を明確にするためのいくつかのポイント:

  1. コンパイラエクスプローラを使用したためにコピー操作が発生し、memcpyへの呼び出しが表示されることがわかっています。
  2. コピー操作が発生したことは、標準のyesを確認することで確認できました。しかし、私の最初の間違った考えは、コンパイラーがこのコピーを最適化するというものでした。私は間違っていた。
  3. clangとgccの両方がmemcpyを生成するコードを生成するため、これは(おそらく)コンパイラーの問題ではありません。
  4. memcpyは安いかもしれませんが、メモリをコピーしてオリジナルを削除する方がstd :: moveでポインタを渡すよりも安い状況は想像できません。
  5. std :: moveの追加は基本的な操作です。コードアナライザーがこの修正を提案できると思います。

2
「偽の」コピー操作を検出するための方法/ツールが存在するかどうかは答えられませんが、正直なところ、私はstd::vector、何らかの形でのコピーが意図したものでないことに同意しません。あなたの例は明示的なコピーを示しstd::moveていますが、コピーが必要なものでない場合は、提案したとおりに関数を適用するのが自然で正しいアプローチです(これもimho)。一部のコンパイラでは、最適化フラグがオンになっていて、ベクターが変更されていない場合、コピーが省略されることがあります。
マグナス

このリンタールールを使用可能にするには、不必要なコピー(影響がない可能性がある)が多すぎると思います:-/(rustはデフォルトでmoveを使用するため、明示的なコピーが必要です:))
Jarod42

コードを最適化するための私の提案は、基本的には最適化する関数を逆アセンブルすることであり、余分なコピー操作を発見します
camp0

問題を正しく理解している場合は、オブジェクトが破棄された後、オブジェクトに対してコピー操作(コンストラクターまたは代入演算子)が呼び出されるケースを検出したいとします。カスタムクラスの場合、コピーの実行時にデバッグフラグセットを追加し、他のすべての操作でリセットし、デストラクタでチェックインすることを想像できます。ただし、ソースコードを変更できない限り、非カスタムクラスに対して同じことを行う方法がわかりません。
Daniel Langr

2
私が偽のコピーを見つけるために使用する手法は、コピーコンストラクターを一時的にプライベートにし、アクセス制限のためにコンパイラーがどこに行き着くかを調べることです。(同じ目的は、そのようなタグ付けをサポートするコンパイラーの場合、コピーコンストラクタに非推奨としてタグを付けることによって達成できます。)
Eljay

回答:


2

あなたは正しい観察をしていると思いますが、間違った解釈をしています!

この場合、すべての通常の賢いコンパイラーは(N)RVOを使用するため、値が返されてもコピーは行われません。C ++ 17以降、これは必須であるため、関数からローカルで生成されたベクトルを返すことによってコピーを表示することはできません。

OK、少し遊んでみstd::vectorましょう。建設中に何が起こるか、または段階的にそれを埋めていきましょう。

まず、すべてのコピーまたは移動を次のように見えるようにするデータ型を生成します。

template <typename DATA >
struct VisibleCopy
{
    private:
        DATA data;

    public:
        VisibleCopy( const DATA& data_ ): data{ data_ }
        {
            std::cout << "Construct " << data << std::endl;
        }

        VisibleCopy( const VisibleCopy& other ): data{ other.data }
        {
            std::cout << "Copy " << data << std::endl;
        }

        VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
        {
            std::cout << "Move " << data << std::endl;
        }

        VisibleCopy& operator=( const VisibleCopy& other )
        {
            data = other.data;
            std::cout << "copy assign " << data << std::endl;
        }

        VisibleCopy& operator=( VisibleCopy&& other ) noexcept
        {
            data = std::move( other.data );
            std::cout << "move assign " << data << std::endl;
        }

        DATA Get() const { return data; }

};

そして、いくつかの実験を始めましょう:

using T = std::vector< VisibleCopy<int> >;

T Get1() 
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
    std::cout << "End init" << std::endl;
    return vec;
}   

T Get2()
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec(4,0);
    std::cout << "End init" << std::endl;
    return vec;
}

T Get3()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

T Get4()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.reserve(4);
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

int main()
{
    auto vec1 = Get1();
    auto vec2 = Get2();
    auto vec3 = Get3();
    auto vec4 = Get4();

    // All data as expected? Lets check:
    for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}

何を観察できるか:

例1)イニシャライザリストからベクトルを作成し、4回の構成と4回の移動が見込まれることを期待します。しかし、4つのコピーを取得します!少し不思議に聞こえますが、その理由は初期化リストの実装です!リストからのイテレータはaでconst T*あるため、リストから要素を移動することができないため、リストから移動することはできません。このトピックに関する詳細な回答は、こちらにあります:initializer_listおよびmove semantics

例2)この場合、最初の構成と値の4つのコピーを取得します。それは特別なことではなく、私たちが期待できることです。

例3)ここでも、予想どおり建設と移動が行われます。私のstl実装では、ベクトルは毎回2倍に増加します。したがって、最初の構成と別の構成がわかります。ベクトルのサイズが1から2に変更されているため、最初の要素の動きがわかります。3つの要素を追加する際、最初の2つの要素の移動が必要な2から4へのサイズ変更を確認します。すべてが期待通りです!

例4)スペースを予約し、後で埋めます。これで、コピーも移動もなくなりました!

すべての場合において、ベクターを呼び出し元に戻すことによる移動やコピーはまったく見られません!(N)RVOが実行中であり、このステップでこれ以上のアクションは必要ありません。

あなたの質問に戻ります:

「C ++の疑似コピー操作を見つける方法」

上記のように、デバッグの目的でプロキシクラスを導入できます。

コピートラクターをプライベートにしても、必要なコピーと非表示のコピーが存在する可能性があるため、多くの場合機能しない可能性があります。上記のように、例4のコードのみがプライベートコピートラクターで動作します。そして、例4が最速の問題である場合、私たちは平和を平和で満たすため、質問に答えることはできません。

ここでは、「不要な」コピーを見つけるための一般的な解決策を提供できません。の呼び出しのためにコードを掘ったmemcpyとしてもmemcpy、最適化されてすべてを見つけることはできず、ライブラリmemcpy関数を呼び出さずにジョブを実行しているアセンブラー命令がいくつか直接表示されます。

私のヒントは、そのような小さな問題に焦点を当てることではありません。実際のパフォーマンスの問題がある場合は、プロファイラーを使用して測定します。潜在的なパフォーマンスキラーが非常に多いため、偽のmemcpy使用に多くの時間を費やすことは、それほど価値のあるアイデアではないようです。


私の質問は一種の学問です。はい、コードを遅くする方法はたくさんありますが、これは当面の問題ではありません。ただし、コンパイラエクスプローラを使用してmemcpy操作を見つけることができます。だから、間違いなく方法があります。しかし、それは小さなプログラムに対してのみ実行可能です。私のポイントは、コードを改善する方法についての提案を見つけるコードの興味があるということです。バグやメモリリークを検出するコードアナライザーがあります。
Mathieu Dutour Sikiric

「コードを改善する方法に関する提案を見つけるコード。」これはすでにコンパイラー自体で実行および実装されています。(N)RVO最適化は1つの例にすぎず、上記のように完璧に機能します。「不要なmemcpy」を検索しているため、memcpyをキャッチしても役に立ちませんでした。「バグやメモリリークを検出するコードアナライザーがありますが、そのような問題はありませんか?」多分それは(一般的な)問題ではありません。また、「速度」の問題を見つけるためのはるかに一般的なツールもすでに存在します。プロファイラーです。私の個人的な感じは、あなたが今日の実際のソフトウェアの問題ではない学術的なことを探しているということです。
クラウス

1

コンパイラエクスプローラを使用してmemcpyへの呼び出しを表示したため、コピー操作が発生したことがわかっています。

完全なアプリケーションをコンパイラエクスプローラーに入れましたか?最適化を有効にしましたか?そうでない場合は、コンパイラエクスプローラで表示されたものが、アプリケーションで何が起こっているのかではない可能性があります。

投稿したコードの1つの問題は、最初にを作成しstd::vector、それをのインスタンスにコピーすることですdata。ベクトルで初期化 dataする方が良いでしょう:

data get_vector(int n)
{
  return {std::vector<int> V(n,0)};
}

また、コンパイラエクスプローラにdataand の定義のみを与えるだけでget_vector()、それ以上のものは期待できません。実際にを使用する ソースコードを提供する場合get_vector()、そのソースコードに対して生成されるアセンブリを確認します。上記の変更と実際の使用法およびコンパイラーの最適化によってコンパイラーが生成する可能性があるものについては、この例を参照してください。


私はコンピューターエクスプローラーに上記のコード(memcpyが含まれている)のみを挿入しました。それはあなたの答えはより良いコードを生成するさまざまな方法を示すのに優れていると言われています。静的を使用する方法と、コンストラクタを出力に直接配置する方法の2つがあります。したがって、これらの方法はコードアナライザーによって提案できます。
Mathieu Dutour Sikiric
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.