web-dev-qa-db-ja.com

std :: vectorを単純なバッファーとして使用するのは良い習慣ですか?

いくつかの画像に対して何らかの処理を実行するアプリケーションがあります。

私は幅/高さ/形式などを知っているので(私は知っています)、ピクセルデータを保存するバッファを定義することだけを考えています:

それから、delete []newunsigned char*を使用し、バッファサイズを個別に記録するのではなく、std::vectorを使用して単純化することを考えています。

したがって、クラスを次のように宣言します。

#include <vector>

class MyClass
{
    // ... etc. ...

public:
    virtual void OnImageReceived(unsigned char *pPixels, 
        unsigned int uPixelCount);

private:
    std::vector<unsigned char> m_pImageBuffer;    // buffer for 8-bit pixels

    // ... etc. ...
};

次に、新しい画像(可変サイズですが、ここではそれらの詳細について心配する必要はありません)を受け取ったときに、ベクトルのサイズを変更して(必要な場合)、ピクセルをコピーします。

void MyClass::OnImageReceived(unsigned char *pPixels, unsigned int uPixelCount)
{
    // called when a new image is available
    if (m_pImageBuffer.size() != uPixelCount)
    {
        // resize image buffer
        m_pImageBuffer.reserve(uPixelCount);
        m_pImageBuffer.resize(uPixelCount, 0);
    }

    // copy frame to local buffer
    memcpy_s(&m_pImageBuffer[0], m_pImageBuffer.size(), pPixels, uPixelCount);

    // ... process image etc. ...
}

これは私には問題ないと思われ、メモリ管理について心配する必要がないという事実が好きですが、いくつかの疑問が生じます。

  1. これはstd::vectorの有効なアプリケーションですか、それともより適切なコンテナがありますか?
  2. reserveandresizeを呼び出して、パフォーマンス面で正しいことをしていますか?
  3. それは常に基になるメモリが連続しているため、示されているようにmemcpy_sを使用できますか?

追加のコメント、批判、アドバイスは大歓迎です。

50
Roger Rowland
  1. 確かに、これは問題なく動作します。心配する必要があることの1つは、クラスが特定の配置に依存している場合に、バッファーが正しく配置されるようにすることです。この場合、データ型自体のベクトル(floatなど)を使用できます。
  2. いいえ、ここでは予約は不要です。サイズ変更は、まったく同じ方法で、必要に応じて容量を自動的に拡張します。
  3. C++ 03より前は、技術的にはそうではありません(実際にはそうです)。 C++ 03以降、はい。

ちなみに、memcpy_sは、ここでの慣用的なアプローチではありません。使用する std::copy代わりに。ポインターはイテレーターであることに注意してください。

C++ 17以降、std::byteは、ここで使用しているような不透明に型指定されたストレージの慣用的な単位です。 charはもちろん動作しますが、charではできない安全でない使用(byte!として)を許可します。

35
Sneftel

他の回答が言及していることに加えて、std::vector::assignおよびmemcpyではなくstd::vector::resizeを使用することをお勧めします。

void MyClass::OnImageReceived(unsigned char *pPixels, unsigned int uPixelCount)
{
    m_pImageBuffer.assign(pPixels, pPixels + uPixelCount);
}

必要に応じてサイズが変更され、0によって引き起こされるバッファの不必要なstd::vector::resize初期化を回避できます。

21
mfontanini

この場合、vectorを使用しても問題ありません。 C++では、ストレージは連続していることが保証されています。

resizereserveの両方も、データをコピーするためにmemcpyもしません。代わりに、確認するためにreserveするだけです。何回も再割り当てする必要はありません。vectorを使用してclearをクリアします。 resizeを使用すると、すべての要素の値がデフォルトに設定されます。とにかく上書きするため、ここでは不要です。

データをコピーする準備ができたら、memcpyを使用しないでください。 copyback_inserterとともに使用して、空のvectorに入れます。

std::copy (pPixels, pPixels + uPixelCount, std::back_inserter(m_pImageBuffer));

このイディオムは、使用しているmemcpyメソッドよりもはるかに標準に近いと考えます。より高速またはより効率的な方法があるかもしれませんが、これがコードのボトルネックであることを証明できない限り(おそらくそうではありません。他の場所で揚げる魚がはるかに多いでしょう)、私は慣用的な方法に固執し、去ります他の誰かへの時期尚早なマイクロ最適化。

15
John Dibling

Std :: vectorはバッファとして使用すると非常に遅いため、非構造化バッファを格納するためのコンテナとしてstd :: vectorを避ける

この例を考えてみましょう:

#include <chrono>
#include <ctime>
#include <iostream>
#include <memory>
#include <vector>

namespace {
std::unique_ptr<unsigned char[]> allocateWithPtr() {
    return std::unique_ptr<unsigned char[]>(new unsigned char[4000000]);
}

std::vector<unsigned char> allocateWithVector() {
    return std::vector<unsigned char>(4000000); }
}

int main() {
    auto start = std::chrono::system_clock::now();

    for (long i = 0; i < 1000; i++) {
        auto myBuff = allocateWithPtr();
    }
    auto ptr_end = std::chrono::system_clock::now();

    for (long i = 0; i < 1000; i++) {
        auto myBuff = allocateWithVector();
    }
    auto vector_end = std::chrono::system_clock::now();

    std::cout << "std::unique_ptr = " 
              << (ptr_end - start).count() / 1000.0 << " ms." << std::endl;
    std::cout << "std::vector = " 
              << (vector_end - ptr_end).count() / 1000.0 << " ms." << std::endl;
}

出力:

bash-3.2$ time myTest
std::unique_ptr = 0.396 ms.
std::vector = 35341.1 ms.

real    0m35.361s
user    0m34.932s
sys 0m0.092s

書き込みや再割り当てが行われていなくても、std :: vectorは、unique_ptrを指定して新しいものを使用する場合よりもほぼ100,000倍遅くなります。何が起きてる?

@MartinSchlottが指摘しているように、このタスク用には設計されていません。ベクトルは、オブジェクトのセットを保持するためのものであり、非構造化(配列の観点から)バッファではありません。オブジェクトにはデストラクタとコンストラクタがあります。ベクターが破棄されると、ベクター内の各要素のデストラクターが呼び出されます。ベクターでさえ、ベクター内の各文字のデストラクターが呼び出されます。

この例では、このベクトル内の符号なし文字を「破壊」するのにどれだけ時間がかかるかを見ることができます。

#include <chrono>
#include <ctime>
#include <iostream>
#include <memory>
#include <vector>

std::vector<unsigned char> allocateWithVector() {
    return std::vector<unsigned char>(4000000); }
}

int main() {
    auto start = std::chrono::system_clock::now();

    for (long i = 0; i < 100; i++) {
        auto leakThis = new std::vector<unsigned char>(allocateWithVector());
    }
    auto leak_end = std::chrono::system_clock::now();

    for (long i = 0; i < 100; i++) {
        auto myBuff = allocateWithVector();
    }
    auto vector_end = std::chrono::system_clock::now();

    std::cout << "leaking vectors: = " 
              << (leak_end - start).count() / 1000.0 << " ms." << std::endl;
    std::cout << "destroying vectors = " 
              << (vector_end - leak_end).count() / 1000.0 << " ms." << std::endl;
}

出力:

leaking vectors: = 2058.2 ms.
destroying vectors = 3473.72 ms.

real    0m5.579s
user    0m5.427s
sys 0m0.135s

ベクターの破壊を除去したとしても、これらのものを100個作成するのにまだ2秒かかります。

動的なサイズ変更、またはバッファを構成する要素の構築と破棄が必要ない場合は、std :: vectorを使用しないでください。

3
Steve Broberg

std :: vectorは、このような場合に使用するために作成されました。あ、はい。

  1. はい、そうです。

  2. reserveはあなたの場合は不要です。

  3. はい、そうします。

3
Ivan Ishchenko

さらに-割り当てられたメモリの最小値を確保するには:

void MyClass::OnImageReceived(unsigned char *pPixels, unsigned int uPixelCount)
{
    m_pImageBuffer.swap(std::vector<unsigned char>(
         pPixels, pPixels + uPixelCount));
    // ... process image etc. ...
}

vector :: assignは、容量が必要な量よりも大きい場合、割り当てられたメモリの量を変更しません。

効果:erase(begin()、end()); insert(begin()、first、last);

2
user2249683

これを考慮してください:

void MyClass::OnImageReceived(unsigned char *pPixels, unsigned int uPixelCount)
{
    // called when a new image is available
    if (m_pImageBuffer.size() != uPixelCount) // maybe just <  ??
    {
        std::vector<unsigned char> temp;
        temp.reserve(uPixelCount);        // no initialize
        m_pImageBuffer.swap(temp) ;       // no copy old data
    }

    m_pImageBuffer.assign(pPixels, pPixels + uPixelCount);  // no reallocate

    // ... process image etc. ...
}

私のポイントは、大きな写真があり、大きなゴミが必要な場合、予約時に古い写真がコピーされ、新しい割り当てられたメモリにサイズ変更され、過剰なメモリが初期化され、新しい写真で書き換えられることです。直接評価しますが、新しいサイズに関する情報を使用して再割り当てを回避することはできません(この単純なケースでは、assignの実装はすでに最適化されているのでしょうか????)。

2
qPCR4vir

場合によります。反復子と[]演算子のみを使用してデータにアクセスする場合は、ベクトルを使用しても問題ありません。

たとえば、バッファを期待する関数へのポインタを与える必要がある場合バイト。私の意見ではありません。この場合、次のようなものを使用する必要があります

unique_ptr<unsigned char[]> buf(new unsigned char[size])

それはベクトルとして保存されますが、ベクトルの代わりにバッファを最大限に制御できます。ベクターはバッファーを再割り当てするか、メソッド/関数の呼び出し中に意図せずにベクター全体のコピーを作成する場合があります。間違いを犯しやすい。

ルールは(私にとって)です。ベクターがある場合は、ベクターのように使用します。メモリバッファが必要な場合は、メモリバッファを使用します。

指摘されたコメントのように、ベクターにはデータメソッドがあります。これはC++です。ベクトルを生のバッファとして使用する自由は、ベクトルを生のバッファとして使用する必要があることを意味しません。私の謙虚な意見では、ベクトルの意図は、タイプ保存アクセスシステムを備えたタイプ保存バッファを持つことでした。互換性のために、呼び出しに内部バッファーを使用できます。意図は、ベクターをスマートポインターバッファーコンテナーとして使用することではありませんでした。そのために、ポインターテンプレートを使用して、このバッファーを生の方法で使用することをコードの他のユーザーに通知します。ベクトルを使用する場合、提供される可能性のある方法ではなく、意図した方法で使用します。

ここで自分の意見(推奨ではない)で非難されたので、私はopが説明した実際の問題にいくつかの言葉を加えたい。

彼は常に同じ画像サイズを期待している場合、私の意見では、unique_ptrを使用する必要があります。を使用して

 m_pImageBuffer.resize(uPixelCount, 0);

pPixelをコピーする前に最初にバッファをゼロにします。これは不必要な時間のペナルティです。

写真のサイズが異なる場合は、次の理由でベクトルを使用しないでください。特に彼のコードでは:

// called when a new image is available
if (m_pImageBuffer.size() != uPixelCount)
{
    // resize image buffer
    m_pImageBuffer.reserve(uPixelCount);
    m_pImageBuffer.resize(uPixelCount, 0);
}

彼はベクトルのサイズを変更します。これは実際にはmallocであり、画像が大きくなる限りコピーします。私の経験では、reallocは常にmallocとcopyにつながります。

それが私が、特にこの状況で、ベクターではなくunique_ptrの使用を推奨する理由です。

0
Martin Schlott