Векторы STL с неинициализированным хранилищем?

Я пишу внутренний цикл, который должен разместить struct в непрерывном хранилище. Я не знаю, сколько таких struct будет раньше времени. Моя проблема в том, что vector в STL инициализирует свои значения до 0, поэтому, что бы я ни делал, я беру на себя затраты на инициализацию плюс затраты на установку значений членов struct.

Есть ли способ предотвратить инициализацию или существует контейнер, подобный STL, с непрерывным хранилищем изменяемого размера и неинициализированными элементами?

(Я уверен, что эту часть кода нужно оптимизировать, и я уверен, что инициализация требует значительных затрат.)

Также см. Мои комментарии ниже, чтобы уточнить, когда происходит инициализация.

НЕКОТОРЫЙ КОД:

void GetsCalledALot(int* data1, int* data2, int count) {
    int mvSize = memberVector.size()
    memberVector.resize(mvSize + count); // causes 0-initialization

    for (int i = 0; i < count; ++i) {
        memberVector[mvSize + i].d1 = data1[i];
        memberVector[mvSize + i].d2 = data2[i];
    }
}

Примечание. Использование функции reserve () не является решением, поскольку вы не можете легально получить доступ к данным, которые находятся в местоположениях end () и выше.

Jim Hunziker 19.09.2008 00:36

Еще одно уточнение: это не значит, что конструктор инициализирует значения равными 0. Это изменение размера вызывает insert, что и делает.

Jim Hunziker 19.09.2008 00:42

Не могли бы вы также дать нам объявление структуры? Спасибо... :-)

paercebal 19.09.2008 01:00

ПРИМЕЧАНИЕ: вы все равно не можете получить доступ к неинициализированным данным. Та же проблема для vector <T> после .end () и неинициализированных членов T []. Но с вектором, скорее всего, отладочный код говорит вам об этом сейчас. Код массива на ПК клиента не сработает.

MSalters 19.09.2008 15:24

Это хороший вопрос. Для некоторых приложений важно понимать, что std :: vector всегда инициализирует свои элементы, даже если они являются простыми старыми данными (POD).

Brent Bradburn 11.11.2009 06:52

Также см. stackoverflow.com/q/7218574/1969455 для подхода распределителя. (подходит для типов POD)

Brandlingo 31.03.2017 15:12

Суть проблемы в том, что вектор выполняет "нулевая" инициализация по умолчанию, потому что std::allocator имеет только значение и нулевую инициализацию, но не имеет метода для инициализации по умолчанию.

Mooing Duck 06.05.2020 20:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
49
7
26 462
16
Перейти к ответу Данный вопрос помечен как решенный

Ответы 16

Используйте метод std :: vector :: reserve (). Он не изменит размер вектора, но выделит пространство.

Эээ ...

попробуйте метод:

std::vector<T>::reserve(x)

Это позволит вам зарезервировать достаточно памяти для x элементов без их инициализации (ваш вектор все еще пуст). Таким образом, перераспределения не произойдет, пока не будет превышено x.

Во-вторых, вектор не будет инициализировать значения нулями. Вы тестируете свой код при отладке?

После проверки на g ++ следующий код:

#include <iostream>
#include <vector>

struct MyStruct
{
   int m_iValue00 ;
   int m_iValue01 ;
} ;

int main()
{
   MyStruct aaa, bbb, ccc ;

   std::vector<MyStruct> aMyStruct ;

   aMyStruct.push_back(aaa) ;
   aMyStruct.push_back(bbb) ;
   aMyStruct.push_back(ccc) ;

   aMyStruct.resize(6) ; // [EDIT] double the size

   for(std::vector<MyStruct>::size_type i = 0, iMax = aMyStruct.size(); i < iMax; ++i)
   {
      std::cout << "[" << i << "] : " << aMyStruct[i].m_iValue00 << ", " << aMyStruct[0].m_iValue01 << "\n" ;
   }

   return 0 ;
}

дает следующие результаты:

[0] : 134515780, -16121856
[1] : 134554052, -16121856
[2] : 134544501, -16121856
[3] : 0, -16121856
[4] : 0, -16121856
[5] : 0, -16121856

Инициализация, которую вы видели, вероятно, была артефактом.

[РЕДАКТИРОВАТЬ] После комментария об изменении размера я изменил код, добавив строку изменения размера. Изменение размера эффективно вызывает конструктор по умолчанию объекта внутри вектора, но если конструктор по умолчанию ничего не делает, тогда ничего не инициализируется ... Я все еще считаю, что это был артефакт (мне впервые удалось обнулить весь вектор с помощью следующий код:

aMyStruct.push_back(MyStruct()) ;
aMyStruct.push_back(MyStruct()) ;
aMyStruct.push_back(MyStruct()) ;

Так... : - /

[РЕДАКТИРОВАТЬ 2] Как уже предлагал Аркадий, решение состоит в использовании встроенного конструктора, принимающего желаемые параметры. Что-то вроде

struct MyStruct
{
   MyStruct(int p_d1, int p_d2) : d1(p_d1), d2(p_d2) {}
   int d1, d2 ;
} ;

Вероятно, это будет встроено в ваш код.

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

Я написал заметку выше. Это не конструктор вектора, который инициализируется значением 0. Это делает resize ().

Jim Hunziker 19.09.2008 00:46

В этом случае MyStruct имеет тривиальный конструктор, поэтому ничего не инициализируется. Это может отличаться от ситуации OP.

Greg Rogers 19.09.2008 00:50

Я думаю, ты на правильном пути. У меня нет конструктора, определенного в структуре, поэтому его конструктор по умолчанию (я считаю) инициализируется нулем. Я проверю, решит ли проблему добавление конструктора по умолчанию, который ничего не делает.

Jim Hunziker 19.09.2008 01:00

Если у вас не определен конструктор и все элементы являются типами POD, конструктор ничего не делает. Если элементы не являются POD, он просто вызовет их конструкторы по умолчанию.

Greg Rogers 19.09.2008 01:08

Грег Роджерс прав. Я предполагаю, что память была «нулевой» из-за инициализации некоторого процесса, независимого от кода, который я написал. В C++ вы не платите за то, что не используете. Так что, если вы пишете C-подобный код, у вас не должно быть накладных расходов. И векторы неплохо с этим справляются.

paercebal 19.09.2008 01:20

Похоже, что в данном случае вектор нас подвел. Мы платим за инициализацию, даже если она нам не нужна или не нужна. Это гарантируется семантикой функции insert (), которая вызывается функцией resize (). Значение, используемое для инициализации, основано на том, что происходит в MyStruct, переданном в resize (). Поскольку вы ничего не указали при вызове resize (), использовался конструктор по умолчанию. Поскольку конструктор по умолчанию в этом случае ничего не делает, вы можете получить нули или что-то еще. В любом случае вы платите за инициализацию, выполняемую resize ().

Brent Bradburn 11.11.2009 07:28

@nobar: это зависит от конструктора MyStruct. Если он пуст и встроен, а члены MyStruct имеют конструкторы с нулевой стоимостью, компилятор C++ не оптимизирует его до нуля. Тогда мы не будем за это платить. Только для изменения размера.

paercebal 06.12.2009 02:20

Должны ли сами структуры находиться в непрерывной памяти, или можно обойтись вектором struct *?

Векторы копируют все, что вы к ним добавляете, поэтому использование векторов указателей, а не объектов, является одним из способов повышения производительности.

Они должны быть смежными. Они находятся в буфере, который будет отправлен по сети одним огромным фрагментом.

Jim Hunziker 19.09.2008 00:45

Я не думаю, что STL - ваш ответ. Вам нужно будет создать собственное решение с помощью realloc (). Вам нужно будет сохранить указатель и либо размер, либо количество элементов, и использовать это, чтобы найти, где начать добавлять элементы после realloc ().

int *memberArray;
int arrayCount;
void GetsCalledALot(int* data1, int* data2, int count) {
    memberArray = realloc(memberArray, sizeof(int) * (arrayCount + count);
    for (int i = 0; i < count; ++i) {
        memberArray[arrayCount + i].d1 = data1[i];
        memberArray[arrayCount + i].d2 = data2[i];
    }
    arrayCount += count;
}

Итак, вот проблема, изменение размера вызывает метод insert, который выполняет копирование конструируемого элемента по умолчанию для каждого из вновь добавленных элементов. Чтобы получить нулевую стоимость, вам нужно написать свой собственный конструктор по умолчанию И свой собственный конструктор копирования как пустые функции. Выполнение этого с вашим конструктором копирования - это очень плохая идея, потому что это нарушит внутренние алгоритмы перераспределения std :: vector.

Резюме: вы не сможете сделать это с помощью std :: vector.

Это настоящая проблема. std :: vector должен понимать, что ему не нужно выполнять какую-либо инициализацию, если у T есть тривиальный конструктор по умолчанию. Спасибо, что указали, что конструктор копирования - это то, что делает здесь ненужную работу.

Eric Hein 08.02.2017 03:40

Чтобы уточнить ответы на backup (): вам нужно использовать reserve () вместе с push_back (). Таким образом, для каждого элемента вызывается не конструктор по умолчанию, а конструктор копирования. Вы по-прежнему несете штраф, создавая структуру в стеке, а затем копируя ее в вектор. С другой стороны, возможно, что если вы используете

vect.push_back(MyStruct(fieldValue1, fieldValue2))

компилятор создаст новый экземпляр прямо в памяти, принадлежащей вектору. Это зависит от того, насколько умен оптимизатор. Вам нужно проверить сгенерированный код, чтобы узнать.

Оказывается, оптимизатор gcc на уровне O3 недостаточно умен, чтобы избежать копирования.

Jim Hunziker 19.09.2008 16:28

Судя по вашим комментариям к другим постерам, похоже, что у вас остались malloc () и друзья. Vector не позволит вам иметь неконструктивные элементы.

Из вашего кода похоже, что у вас есть вектор структур, каждая из которых состоит из 2 int. Могли бы вы вместо этого использовать 2 вектора целых чисел? потом

copy(data1, data1 + count, back_inserter(v1));
copy(data2, data2 + count, back_inserter(v2));

Теперь вы не платите за копирование структуры каждый раз.

Интересно. Это может просто сработать - похоже, что это позволит избежать создания промежуточного объекта.

Brent Bradburn 11.11.2009 07:03

Я бы сделал что-то вроде:

void GetsCalledALot(int* data1, int* data2, int count)
{
  const size_t mvSize = memberVector.size();
  memberVector.reserve(mvSize + count);

  for (int i = 0; i < count; ++i) {
    memberVector.push_back(MyType(data1[i], data2[i]));
  }
}

Вам необходимо определить ctor для типа, который хранится в memberVector, но это небольшие затраты, так как это даст вам лучшее из обоих миров; не выполняется ненужная инициализация и перераспределение во время цикла не происходит.

Кажется, это не решает проблему, поскольку он использует временный MyType () и копирует его в вектор. Еще есть двойная инициализация.

Brent Bradburn 11.11.2009 06:58
Ответ принят как подходящий

std::vector должен каким-то образом инициализировать значения в массиве, что означает, что должен быть вызван некоторый конструктор (или конструктор копирования). Поведение vector (или любого класса контейнера) не определено, если вы должны получить доступ к неинициализированной части массива, как если бы он был инициализирован.

Лучше всего использовать reserve() и push_back(), чтобы использовался копирующий конструктор, избегая конструкции по умолчанию.

Используя ваш пример кода:

struct YourData {
    int d1;
    int d2;
    YourData(int v1, int v2) : d1(v1), d2(v2) {}
};

std::vector<YourData> memberVector;

void GetsCalledALot(int* data1, int* data2, int count) {
    int mvSize = memberVector.size();

    // Does not initialize the extra elements
    memberVector.reserve(mvSize + count);

    // Note: consider using std::generate_n or std::copy instead of this loop.
    for (int i = 0; i < count; ++i) {
        // Copy construct using a temporary.
        memberVector.push_back(YourData(data1[i], data2[i]));
    }
}

Единственная проблема с таким вызовом reserve() (или resize()) заключается в том, что вы можете в конечном итоге вызывать конструктор копирования чаще, чем вам нужно. Если вы можете сделать хороший прогноз относительно окончательного размера массива, лучше reserve() пробел один раз в начале. Если вы не знаете окончательный размер, по крайней мере, количество копий в среднем будет минимальным.

В текущей версии C++ внутренний цикл немного неэффективен, поскольку временное значение создается в стеке, копируется в память векторов и, наконец, временное значение уничтожается. Однако в следующей версии C++ есть функция, называемая ссылками на R-значения (T&&), которая поможет.

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

template <typename T>
class my_vector_replacement {

    // ...

    template <typename F>
    my_vector::push_back_using_factory(F factory) {
        // ... check size of array, and resize if needed.

        // Copy construct using placement new,
        new(arrayData+end) T(factory())
        end += sizeof(T);
    }

    char* arrayData;
    size_t end; // Of initialized data in arrayData
};

// One of many possible implementations
struct MyFactory {
    MyFactory(int* p1, int* p2) : d1(p1), d2(p2) {}
    YourData operator()() const {
        return YourData(*d1,*d2);
    }
    int* d1;
    int* d2;
};

void GetsCalledALot(int* data1, int* data2, int count) {
    // ... Still will need the same call to a reserve() type function.

    // Note: consider using std::generate_n or std::copy instead of this loop.
    for (int i = 0; i < count; ++i) {
        // Copy construct using a factory
        memberVector.push_back_using_factory(MyFactory(data1+i, data2+i));
    }
}

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

C++ 0x добавляет новый шаблон функции-члена emplace_back в vector (который полагается на вариативные шаблоны и идеальную пересылку), который полностью избавляется от любых временных файлов:

memberVector.emplace_back(data1[i], data2[i]);

Если вы действительно настаиваете на том, чтобы элементы не инициализировались и жертвовали некоторыми методами, такими как front (), back (), push_back (), используйте вектор ускорения из числа. Это позволяет даже не сохранять существующие элементы при вызове resize () ...

В C++ 11 (и boost) вы можете использовать версию unique_ptr с массивом для выделения неинициализированного массива. Это не совсем stl-контейнер, но он по-прежнему управляется памятью и C++ - иш, чего будет достаточно для многих приложений.

auto my_uninit_array = std::unique_ptr<mystruct[]>(new mystruct[count]);

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

template <typename T>
struct no_init
{
    T value;

    no_init() { static_assert(std::is_standard_layout<no_init<T>>::value && sizeof(T) == sizeof(no_init<T>), "T does not have standard layout"); }

    no_init(T& v) { value = v; }
    T& operator=(T& v) { value = v; return value; }

    no_init(no_init<T>& n) { value = n.value; }
    no_init(no_init<T>&& n) { value = std::move(n.value); }
    T& operator=(no_init<T>& n) { value = n.value; return this; }
    T& operator=(no_init<T>&& n) { value = std::move(n.value); return this; }

    T* operator&() { return &value; } // So you can use &(vec[0]) etc.
};

Использовать:

std::vector<no_init<char>> vec;
vec.resize(2ul * 1024ul * 1024ul * 1024ul);

Вы можете использовать новые элементы boost::noinit_adaptor для инициализация по умолчанию (что не является инициализацией для встроенных типов):

std::vector<T, boost::noinit_adaptor<std::allocator<T>> memberVector;

Пока вы не передаете инициализатор в resize, он по умолчанию инициализирует новые элементы.

Я не уверен во всех ответах, в которых говорится, что это невозможно, или о неопределенном поведении.

Иногда вам нужно использовать std :: vector. Но когда-нибудь вы знаете его окончательный размер. И вы также знаете, что ваши элементы будут построены позже. Пример: когда вы сериализуете содержимое вектора в двоичный файл, а затем читаете его позже. У Unreal Engine есть TArray :: setNumUninitialized, почему бы не std :: vector?

Чтобы ответить на первоначальный вопрос «Есть ли способ предотвратить инициализацию или существует контейнер, подобный STL, с непрерывным хранилищем изменяемого размера и неинициализированными элементами?»

и да и нет. Нет, потому что STL не предлагает способа сделать это.

Да, потому что мы кодируем на C++, а C++ позволяет многое. Если вы готовы быть плохим парнем (и если вы действительно знаете, что делаете). Вы можете угнать вектор.

Вот пример кода, который работает только для реализации STL Windows, для другой платформы, посмотрите, как реализован std :: vector для использования его внутренних членов:

// This macro is to be defined before including VectorHijacker.h. Then you will be able to reuse the VectorHijacker.h with different objects.
#define HIJACKED_TYPE SomeStruct

// VectorHijacker.h
struct VectorHijacker
{
    std::size_t _newSize;
};


template<>
template<>
inline decltype(auto) std::vector<HIJACKED_TYPE, std::allocator<HIJACKED_TYPE>>::emplace_back<const VectorHijacker &>(const VectorHijacker &hijacker)
{
    // We're modifying directly the size of the vector without passing by the extra initialization. This is the part that relies on how the STL was implemented.
    _Mypair._Myval2._Mylast = _Mypair._Myval2._Myfirst + hijacker._newSize;
}

inline void setNumUninitialized_hijack(std::vector<HIJACKED_TYPE> &hijackedVector, const VectorHijacker &hijacker)
{
    hijackedVector.reserve(hijacker._newSize);
    hijackedVector.emplace_back<const VectorHijacker &>(hijacker);
}

Но будьте осторожны, мы говорим об угоне. Это действительно грязный код, и его следует использовать только в том случае, если вы действительно знаете, что делаете. Кроме того, он не переносится и сильно зависит от того, как была реализована реализация STL.

Я не буду советовать вам его использовать, потому что все здесь (включая меня) хорошие люди. Но я хотел сообщить вам, что это возможно, вопреки всем предыдущим ответам, в которых говорилось, что это не так.

Обратите внимание, что я написал код в Visual Studio 2019 Community 16.7.5. И я использовал последний черновик ... И режим соответствия отключен (разрешающий-) (потому что, к сожалению, шаблон и режим соответствия во многих случаях не очень хорошая комбинация). Наконец, он зависит от того, как платформа реализовала STL, поэтому вам нужно будет адаптировать решение, если вы хотите его использовать (но вы не пачкаете руки, не так ли?).

SunlayGGX 05.10.2020 18:05

Другие вопросы по теме