Является ли хорошей практикой использование std:: vector в качестве простого буфера?



У меня есть приложение, которое выполняет некоторую обработку некоторых изображений.



Учитывая, что я знаю ширину / высоту / формат и т. д. (Я делаю), и думаю только об определении буфера для хранения пиксельных данных:



Тогда, вместо того чтобы использовать new и delete [] на unsigned 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. ...
}


Мне это кажется прекрасным, и мне нравится тот факт, что мне не нужно беспокоиться об управлении памятью, но это вызывает некоторые вопросы:


    Является ли это допустимым применением std::vector или есть более подходящий контейнер?
  1. правильно ли я поступаю с точки зрения производительности, позвонив reserve и еще resize?

  2. воля это всегда случай, когда основная память последовательна, поэтому я могу использовать memcpy_s , как показано?


Любые дополнительные замечания, критические замечания или советы будут весьма приветствоваться.
533   8  

8 ответов:

  1. Конечно, это сработает отлично. Единственное, о чем вам нужно беспокоиться, - это убедиться, что буфер правильно выровнен, если ваш класс полагается на определенное выравнивание; в этом случае вы можете использовать вектор самого типа данных (например, float).
  2. нет, резерв здесь не нужен; resize автоматически увеличит емкость по мере необходимости, точно таким же образом.
  3. до C++03, технически нет (но на практике да). Начиная с C++03, Да.

Кстати, хотя, memcpy_s здесь не идиоматический подход. Вместо этого используйте std::copy. Имейте в виду, что указатель является итератором.

Начиная с C++17, std::byte является идиоматической единицей непрозрачно типизированного хранилища, такого как вы используете здесь. char все еще будет работать, конечно, но допускает небезопасное использование (как char!) которого byte нет.

Помимо того, что другие ответы упоминают, я бы рекомендовал вам использовать std::vector::assign, а не std::vector::resize и memcpy:

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

, который при необходимости изменит размер, и вы избежите ненужной 0 инициализации буфера, вызванной std::vector::resize.

Использование vector в этом случае прекрасно. В C++ хранилище гарантированно будет смежным.

Я бы не стал ни resize, ни reserve, Ни memcpy копировать данные. Вместо этого все, что вам нужно сделать, это reserve, чтобы убедиться, что вам не придется перераспределять много раз, а затем очистить vector с помощью clear. Если вы resize, он пройдет и установит значения каждого элемента по умолчанию - это излишне, потому что вы просто собираетесь перезаписать его в любом случае.

Когда вы готовы скопировать данные, не используйте memcpy. Используйте copy в сочетании с back_inserter в пустой vector:

std::copy (pPixels, pPixels + uPixelCount, std::back_inserter(m_pImageBuffer));
Я бы счел эту идиому гораздо более близкой к канонической, чем метод memcpy, который вы используете. Возможно, есть более быстрые или более эффективные методы, но если вы не можете доказать, что это узкое место в вашем коде (чего, скорее всего, не будет; у вас будет гораздо больше рыбы, чтобы жарить в другом месте) , я бы придерживался идиоматических методов и оставил преждевременные микрооптимизации для кого-то еще.

Std:: vector был создан для использования в таких случаях. Так что да.

  1. Да, это так.

  2. reserve в вашем случае это излишне.

  3. Да, так и будет.

Кроме того-для обеспечения минимального объема выделенной памяти:

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

Vector:: assign не изменяет объем выделенной памяти, если емкость больше необходимого объема:

Эффекты: стереть (begin (), end()); вставить (begin (), first, last);

Пожалуйста, подумайте вот о чем:

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

Моя точка зрения заключается в том, что если у вас есть большая картинка и вам нужен более крупный снимок, ваш старый снимок будет скопирован во время резервирования и/или изменения размера в новый выделенный memmory, избыток memmory инициализируется, а затем перезаписывается с новым рисунком. Вы колуд непосредственно ассинг, но тогда вы не сможете использовать информацию, которую вы имеете о новом размере, чтобы избежать возможных перераспределений (возможно, реализация assign уже оптимизирована для этого простого случая ????).

Это зависит. Если вы получаете доступ к данным только через итераторы и оператор [], то можно использовать вектор.

Если вы должны дать указатель на функции, которые ожидают буфер, например, байт. На мой взгляд, это не так. В этом случае вы должны использовать что-то вроде

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

Это как сохранить как вектор, но вместо вектора у вас есть максимальный контроль над буфером. Вектор может перераспределить буфер или во время вызова метода / функции вы можете непреднамеренно сделать копию вашего весь вектор. Легко допустимая ошибка.

Правило (для меня) таково. Если у вас есть вектор, используйте его как вектор. Если вам нужен буфер памяти, используйте буфер памяти.

Как указано в комментарии, Вектор имеет метод данных. Это C++. Свобода использования вектора в качестве необработанного буфера не означает, что вы должны использовать его в качестве необработанного буфера. По моему скромному мнению, намерение вектора состояло в том, чтобы иметь буфер сохранения типа с системой доступа сохранения типа. Для совместимости вы можете использовать внутренний буфер для вызовов. Цель состояла не в том, чтобы использовать вектор в качестве буферного контейнера интеллектуального указателя. Для этого я использую шаблоны указателей, сигнализируя другому пользователю моего кода, что я использую этот буфер необработанным способом. Если я использую векторы, я использую их так, как они предназначены, а не так, как они предлагают.

Поскольку я получил некоторую вину здесь за мое мнение (а не рекомендацию), я хочу добавить несколько слов к фактической проблеме, описанной ОП.

Если он ожидает всегда одну и ту же картину размер, он должен, на мой взгляд, использовать 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);
}
Он будет изменять размер вектора, который на самом деле является маллоком, и копировать до тех пор, пока изображения становятся больше. Один по моему опыту, перераспределение всегда приводит к маллоку и копированию.

Именно поэтому я, особенно в этой ситуации, рекомендую использовать unique_ptr вместо вектора.

Я бы не стал использовать 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 почти в 100 000 раз медленнее, чем просто использование нового с unique_ptr. Что здесь происходит?

Как указывает @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

Даже при удалении разрушения вектора, все равно требуется 2 секунды, чтобы просто построить 100 из все эти вещи.

Если вам не нужно динамическое изменение размера или построение и разрушение элементов, составляющих ваш буфер, не используйте std::vector.

Comments

    Ничего не найдено.