web-dev-qa-db-fra.com

Est-ce une bonne pratique d'utiliser std :: vector comme un simple tampon?

J'ai une application qui effectue un traitement sur certaines images.

Étant donné que je connais la largeur/hauteur/format, etc. (je le sais), et que je pense à définir un tampon pour stocker les données de pixels:

Ensuite, plutôt que d'utiliser new et delete [] sur un unsigned char* et en gardant une note séparée de la taille du tampon, je pense à simplifier les choses en utilisant un std::vector.

Je déclarerais donc ma classe quelque chose comme ceci:

#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. ...
};

Ensuite, lorsque j'ai reçu une nouvelle image (de taille variable - mais ne vous inquiétez pas de ces détails ici), je peux simplement redimensionner le vecteur (si nécessaire) et copier les pixels:

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. ...
}

Cela me semble bien, et j'aime le fait que je n'ai pas à me soucier de la gestion de la mémoire, mais cela soulève quelques questions:

  1. Est-ce une application valide de std::vector ou existe-t-il un conteneur plus adapté?
  2. Suis-je en train de faire la bonne chose en termes de performances en appelant reserveetresize?
  3. Sera-ce toujours que la mémoire sous-jacente soit consécutive pour que je puisse utiliser memcpy_s comme montré?

Tout commentaire, critique ou conseil supplémentaire serait le bienvenu.

50
Roger Rowland
  1. Bien sûr, cela fonctionnera bien. La seule chose dont vous devez vous soucier est de vous assurer que le tampon est correctement aligné, si votre classe s'appuie sur un alignement particulier; dans ce cas, vous pouvez utiliser un vecteur du type de données lui-même (comme float).
  2. Non, la réservation n'est pas nécessaire ici; resize augmentera automatiquement la capacité selon les besoins, exactement de la même manière.
  3. Avant C++ 03, techniquement non (mais en pratique oui). Depuis C++ 03, oui.

Soit dit en passant, memcpy_s n'est pas l'approche idiomatique ici. Utilisation std::copy au lieu. Gardez à l'esprit qu'un pointeur est un itérateur.

À partir de C++ 17, std::byte est l'unité idiomatique de stockage typé de manière opaque comme celle que vous utilisez ici. char fonctionnera toujours, bien sûr, mais permet des utilisations dangereuses (comme char!) que byte ne fait pas.

35
Sneftel

Outre les autres réponses mentionnées, je vous recommande d'utiliser std::vector::assign plutôt que std::vector::resize et memcpy:

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

Cela se redimensionnera si nécessaire, et vous éviterez les inutiles 0 initialisation du tampon provoquée par std::vector::resize.

21
mfontanini

L'utilisation d'un vector dans ce cas est très bien. En C++, le stockage est garanti contigieux.

Je ne voudrais pas à la fois resize et reserve, ni memcpy pour copier les données. À la place, tout ce que vous avez à faire est reserve pour vous assurer vous n'avez pas besoin de réaffecter plusieurs fois, puis effacez vector en utilisant clear. Si vous resize, il passera par et définira les valeurs de chaque élément à leurs valeurs par défaut - ce n'est pas une évidence ici car vous allez juste le remplacer de toute façon.

Lorsque vous êtes prêt à copier les données, n'utilisez pas memcpy. Utilisez copy conjointement avec back_inserter dans un vector vide:

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

Je considérerais cet idiome comme étant beaucoup plus proche de la canonique que la méthode memcpy que vous utilisez. Il peut y avoir des méthodes plus rapides ou plus efficaces, mais à moins que vous ne puissiez prouver qu'il s'agit d'un goulot d'étranglement dans votre code (ce qui ne sera probablement pas le cas, vous aurez des poissons beaucoup plus gros à faire frire ailleurs), je resterais avec des méthodes idiomatiques et je laisserais les micro-optimisations prématurées à quelqu'un d'autre.

15
John Dibling

J'éviterais std :: vector comme conteneur pour stocker un tampon non structuré, car std :: vector est profondément lent lorsqu'il est utilisé comme tampon

Considérez cet exemple:

#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;
}

Production:

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

Même sans écritures ni réallocations, std :: vector est presque 100 000 fois plus lent que d'utiliser simplement un nouveau avec un unique_ptr. Que se passe t-il ici?

Comme le souligne @MartinSchlott, il n'est pas conçu pour cette tâche. Un vecteur sert à contenir un ensemble d'objets, pas un tampon non structuré (du point de vue d'un tableau). Les objets ont des destructeurs et des constructeurs. Lorsque le vecteur est détruit, il appelle le destructeur pour chaque élément qu'il contient, même le vecteur appellera un destructeur pour chaque caractère de votre vecteur.

Vous pouvez voir combien de temps il faut juste pour "détruire" les caractères non signés dans ce vecteur avec cet exemple:

#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;
}

Production:

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

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

Même en supprimant la destruction du vecteur, il faut toujours 2 secondes pour construire 100 de ces choses.

Si vous n'avez pas besoin de redimensionnement dynamique, ni de construction et de destruction des éléments composant votre tampon, n'utilisez pas std :: vector.

3
Steve Broberg

std :: vector a été FAIT pour être utilisé dans de tels cas. Donc oui.

  1. Oui, ça l'est.

  2. reserve n'est pas nécessaire dans votre cas.

  3. Oui, il sera.

3
Ivan Ishchenko

De plus - pour assurer un minimum de mémoire allouée:

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

vector :: assign ne change pas la quantité de mémoire allouée, si la capacité est supérieure à la quantité nécessaire:

Effets: effacer (début (), fin ()); insert (begin (), first, last);

2
user2249683

Veuillez considérer ceci:

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. ...
}

Mon point est que si vous avez une grande image et avez besoin d'une image plus grande, votre ancienne image sera copiée pendant la réservation et/ou redimensionnée dans la nouvelle mémoire allouée, l'excès de mémoire initialisé, puis réécrit avec la nouvelle image. Vous coludez directement, mais vous ne pourrez plus utiliser les informations dont vous disposez sur la nouvelle taille pour éviter des réallocations possibles (peut-être que l'implémentation de assign est déjà optimisée pour ce cas simple ????).

2
qPCR4vir

Ça dépend. Si vous accédez aux données uniquement par le biais d'itérateurs et de l'opérateur [], vous pouvez utiliser un vecteur.

Si vous devez donner un pointeur sur des fonctions qui attendent un tampon de par ex. octets. Ce n'est pas à mon avis. Dans ce cas, vous devez utiliser quelque chose comme

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

est-il aussi sauvegardé qu'un vecteur, mais au lieu d'un vecteur, vous avez un contrôle maximum du tampon. Un vecteur peut réallouer un tampon ou pendant un appel de méthode/fonction, vous pouvez involontairement faire une copie de votre vecteur entier. Une erreur facilement commise.

La règle (pour moi) est. Si vous avez un vecteur, utilisez-le comme un vecteur. Si vous avez besoin d'un tampon mémoire, utilisez un tampon mémoire.

Comme indiqué dans un commentaire, le vecteur a une méthode de données. C'est C++. La liberté d'utiliser un vecteur comme tampon brut ne signifie pas que vous devez l'utiliser comme tampon brut. À mon humble avis, l'intention d'un vecteur était d'avoir un tampon de sauvegarde de type avec un système d'accès de sauvegarde de type. Pour des raisons de compatibilité, vous pouvez utiliser le tampon interne pour les appels. L'intention n'était pas d'utiliser le vecteur comme conteneur tampon de pointeur intelligent. Pour cela, j'utilise les modèles de pointeurs, signalant à un autre utilisateur de mon code que j'utilise ce tampon de manière brute. Si j'utilise des vecteurs, je les utilise de la façon dont ils sont destinés, et non des façons possibles qu'ils offrent.

COMME j'ai été blâmé ici pour mon opinion (pas une recommandation), je veux ajouter quelques mots au problème réel décrit par l'op.

S'il s'attend toujours à la même taille d'image, il devrait, à mon avis, utiliser un unique_ptr, car c'est ce qu'il en fait à mon avis. En utilisant

 m_pImageBuffer.resize(uPixelCount, 0);

remet à zéro le tampon avant de lui copier le pPixel, une pénalité de temps inutile.

Si les images qu'il attend de taille différente, il ne devrait pas, à mon avis, utiliser un vecteur pendant la raison suivante. Surtout dans son code:

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

il redimensionnera le vecteur, qui est en fait un malloc et le copiera tant que les images grossissent. Une réallocation de mon expérience conduit toujours à malloc et copie.

C'est la raison pour laquelle je recommande, en particulier dans cette situation, l'utilisation d'un unique_ptr au lieu d'un vecteur.

0
Martin Schlott