Выделить структуру, содержащую строку, в одном выделении

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

По сути, для этого потребуется структура с полем, описывающим, какие данные содержит структура, и еще одно поле, представляющее собой строку с самими данными. Длина строки всегда будет известна во время распределения. В результате тестирования мы определили, что удвоение количества выделений, необходимых для каждого из этих типов данных, является неприемлемой ценой. Есть ли способ выделить память для структуры и std::string, содержащейся в структуре, за одно выделение? Если бы мы использовали cstrings, я бы просто добавил char * в структуру и указал бы его на конец структуры после выделения блока, достаточно большого для структуры и строки, но мы бы предпочли std::string, если это возможно.

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


person Shea Levy    schedule 08.06.2012    source источник
comment
Если я правильно понимаю, строка не может расти после построения, и памятью будет управлять тот, кто управляет всем объектом... поскольку вы идете по пути C, почему бы просто не использовать char*? Худшие части строк C — это необходимость управлять памятью, но кажется, что в вашем случае это не проблема, не так ли?   -  person David Rodríguez - dribeas    schedule 08.06.2012
comment
+1 за то, что доказал необходимость оптимизации перед тем, как выйти из строя!   -  person John Dibling    schedule 08.06.2012
comment
Если вы действительно стремитесь использовать std::string, а не c-строку, вы можете рассмотреть возможность использования пользовательского распределителя для достижения того, что вы хотите сделать. Это может позволить вам объединить ваши строки в одном выделении, но все равно не будет выделено все (то есть ваша структура) за один раз, и это, вероятно, станет кошмаром обслуживания. Вероятно, лучше всего придерживаться c-strings.   -  person Component 10    schedule 08.06.2012


Ответы (8)


Если у вас такие строгие потребности в памяти, вам придется отказаться от std::string.

Лучшей альтернативой является найти или написать реализацию basic_string_ref (предложение для следующей стандартной библиотеки C++), который на самом деле представляет собой просто char* в сочетании с размером. Но у него есть все (не мутирующие) функции std::basic_string. Затем вы используете фабричную функцию для выделения необходимой памяти (размер вашей структуры + строковые данные), а затем используете новое размещение для инициализации basic_string_ref.

Конечно, вам также понадобится пользовательская функция удаления, так как вы не можете просто передать указатель на «удалить».


Учитывая ранее связанный с реализацией basic_string_ref (и связанные с ним определения типов, string_ref), вот фабричный конструктор/деструктор для некоторого типа T, в котором должна быть строка:

template<typename T> T *Create(..., const char *theString, size_t lenstr)
{
  char *memory = new char[sizeof(T) + lenstr + 1];
  memcpy(memory + sizeof(T), theString, lenstr);

  try
  {
    return new(memory) T(..., string_ref(theString, lenstr);
  }
  catch(...)
  {
    delete[] memory;
    throw;
  }
}

template<typename T> T *Create(..., const std::string & theString)
{
  return Create(..., theString.c_str(), theString.length());
}

template<typename T> T *Create(..., const string_ref &theString)
{
  return Create(..., theString.data(), theString.length());
}

template<typename T> void Destroy(T *pValue)
{
  pValue->~T();

  char *memory = reinterpret_cast<char*>(pValue);
  delete[] memory;
}

Очевидно, вам нужно будет заполнить другие параметры конструктора самостоятельно. И конструктор вашего типа должен будет принять string_ref, который ссылается на строку.

person Nicol Bolas    schedule 08.06.2012

Если вы используете std::string, вы не можете сделать одно выделение как для структуры, так и для строки, и вы также не можете сделать выделение обоих одним большим блоком. Однако, если вы используете старые строки в стиле C, это возможно.

person Some programmer dude    schedule 08.06.2012

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

Если это действительно так, я могу придумать пару решений.

  1. Вы можете предварительно выделить все эти структуры заранее, до запуска вашей программы. Храните их в какой-то фиксированной коллекции, чтобы они не были созданы путем копирования, и reserve в ваших string будет достаточно буфера для хранения ваших данных.
  2. Как бы спорно это ни звучало, вы можете использовать старые массивы char в стиле C. Похоже, вы не понимаете большую часть причины использования strings в первую очередь, а именно управления памятью. Однако в вашем случае, поскольку вы знаете необходимые размеры буфера при запуске, вы можете справиться с этим самостоятельно. Если вам нравятся другие возможности, предоставляемые string, имейте в виду, что многое из этого все еще доступно в <algorithm>s.
person John Dibling    schedule 08.06.2012

Взгляните на Variable Sized Struct C++ - краткий ответ заключается в том, что это невозможно сделать. в ванильном С++.

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

person ecatmur    schedule 08.06.2012

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

  • Выполните однократное распределение
  • Выполните одно динамическое размещение

Может показаться, что это не так уж и отличается, поэтому позвольте мне объяснить.

1. Вы можете использовать struct хак в C++

  • Да, это не типичный C++
  • Да, это требует особого ухода

Технически требуется:

  • отключение конструктора копирования и оператора присваивания
  • создание конструктора и деструктора private и предоставление фабричных методов для выделения и освобождения объекта

Честно говоря, это трудный путь.

2. Вы можете избежать динамического выделения внешнего struct

Достаточно просто:

struct M {
    Kind _kind;
    std::string _data;
};

а затем передать экземпляры M в стек. Операции перемещения должны гарантировать, что std::string не будет скопирован (вы всегда можете отключить копирование, чтобы убедиться в этом).

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

person Matthieu M.    schedule 08.06.2012

Строки в стиле C всегда можно преобразовать в std::string по мере необходимости. На самом деле, есть хороший шанс, что ваши наблюдения из профилирования связаны с фрагментацией ваших данных, а не просто с количеством распределений, и создание std::string по требованию будет эффективным. Конечно, не зная вашего фактического приложения, это всего лишь предположение, и на самом деле никто не может этого знать, пока оно не будет проверено. Я представляю себе класс

class my_class {
    std::string data() const { return self._data; }
    const char* data_as_c_str() const // In case you really need it!
    { return self._data; }
private:
    int _type;
    char _data[1];
};

Примечание. Я использовал стандартный хитрый прием C для размещения данных: _data имеет длину, которую вы хотите, если ваша фабричная функция выделяет для него дополнительное пространство. IIRC, C99 даже предоставил для него специальный синтаксис:

struct my_struct {
    int type;
    char data[];
};

который имеет хорошие шансы работать с вашим компилятором C++. (Это в стандарте С++ 11?)

Конечно, если вы сделаете это, вам действительно нужно сделать все конструкторы частными и дружественными к вашей фабричной функции, чтобы убедиться, что фабричная функция является единственным способом фактического создания экземпляра my_class — он будет сломан без дополнительной памяти для массив. Вам обязательно нужно сделать operator= закрытым или иным образом тщательно реализовать его.


Переосмысление ваших типов данных, вероятно, является хорошей идеей.

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

class structured_data_reference {
public:
    structured_data_reference(const char *data):_data(data) {}
    std::string get_first_field() const {
        // Do something interesting with _data to get the first field
    }
private:
    const char *_data;
};

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


Другой возможный хак — просто использовать std::string, но хранить информацию о типе в первой записи (или в первых нескольких). Конечно, это требует учета этого всякий раз, когда вы получаете доступ к данным.

person Community    schedule 08.06.2012
comment
создание std::string по запросу будет эффективным. Как? Каждый раз, когда вы это делаете, вам нужен еще один блок памяти. - person Nicol Bolas; 08.06.2012
comment
Это действительно зависит от приложения. Как я уже упоминал, потеря производительности может быть вызвана не тем, что std::string выполняет распределение, а тем, что он фрагментирует ваши данные. С другой стороны, создание недолговечных std::string временных файлов не приведет к фрагментации ваших данных, и, если повезет, их можно будет быстро выделить и разместить в кеше. Вы даже можете избежать распределения, используя такой метод, как structured_data_reference::copy_into_string(std::string &x), который заменит содержимое x копией данных в _data. - person ; 09.06.2012
comment
Чтобы объяснить далее, std::string в вашем структурном решении имеет две проблемы. Во-первых, он выполняет 2 распределения (возможно, 3), как вы упомянули. Во-вторых, ваша структура и содержимое строки могут располагаться в очень разных частях памяти. Когда происходит последнее, каждый раз, когда вы используете структуру, вам приходится касаться двух разных частей памяти. Поскольку сама структура крошечная, это неэффективно как для кэша, так и для TLB, если рядом со структурой в памяти нет других немедленно полезных данных, а в некоторых приложениях это является доминирующим снижением производительности. - person ; 09.06.2012

Я не уверен, что это точно решение вашей проблемы. Одним из способов оптимизации выделения памяти в C++ является использование предварительно выделенного буфера, а затем использование оператора «размещение нового». Я попытался решить вашу проблему, как я ее понял.

 unsigned char *myPool = new unsigned char[10000];
 struct myStruct
 {
    myStruct(char* aSource1, char* aSource2)
    {
        original = new (myPool) string(aSource1); //placement new
        data = new (myPool) string(aSource2); //placement new
    }
    ~myStruct()
    {
        original = NULL; //no deallocation needed
        data = NULL; //no deallocation needed
    }
    string* original;
    string* data;
};

int main()
{
    myStruct* aStruct = new (myPool) myStruct("h1", "h2");

    //  Use the struct

    aStruct = NULL; //  No need to deallocate
    delete [] myPool;

    return 0;
}

[править] После комментария от NicolBolas проблема немного прояснилась. Я решил написать еще один ответ, хотя на самом деле это не так много преимуществ, как использование необработанного массива символов. Но я все еще считаю, что это хорошо в пределах заявленных ограничений. Идея заключалась бы в том, чтобы предоставить настраиваемый распределитель для класса строк, как указано в этом ТАК вопрос. При реализации метода allocate используйте новое размещение как

pointer allocate(size_type n, void * = 0) 
{
    // fail if we try to allocate too much
    if((n * sizeof(T))> max_size()) { throw std::bad_alloc(); }

    //T* t = static_cast<T *>(::operator new(n * sizeof(T)));
    T* t = new (/* provide the address of the original character buffer*/) T[n];
    return t;
}

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

person PermanentGuest    schedule 08.06.2012
comment
Это не поможет. Это решает только то, где размещается string, а не содержимое string. - person Nicol Bolas; 08.06.2012
comment
Вопрос: есть ли способ выделить память для структуры и std::string, содержащейся в структуре, за одно выделение? И причина вопроса в том, что удвоение количества выделений, необходимых для каждого из этих типов данных, является неприемлемой стоимостью. Я предполагаю, что производительность плохая из-за выделения памяти. Здесь избегают распределения (как для строки, так и для структуры). (Я согласен, что если манипуляции со строками снижают производительность, то этого недостаточно. Но это не ясно из вопроса) - person PermanentGuest; 08.06.2012
comment
Нет, это не так. std::string выделяет память внутри. Это второе выделение памяти, от которого он хочет избавиться. - person Nicol Bolas; 08.06.2012
comment
@NicolBolas: я понимаю, что вы говорите, я думаю, что должен изменить свой ответ - person PermanentGuest; 08.06.2012

По сути, для этого потребуется структура с полем, описывающим, какие данные содержит структура, и еще одно поле, представляющее собой строку с самими данными.

У меня есть ощущение, что вы не используете систему типов C++ в полной мере здесь. Он выглядит и ощущается очень по-стильски (это неправильное слово, я знаю). У меня нет конкретных примеров для размещения здесь, так как я понятия не имею о проблеме, которую вы пытаетесь решить.

Есть ли способ выделить память для структуры и std::string, содержащейся в структуре, за одно выделение?

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

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

жизненно важная структура данных в виде неструктурированной строки с программно-определяемыми разделителями

Один вопрос: эта строка изменяема? Если нет, вы можете использовать немного другую структуру данных. Не храните копии частей этой жизненно важной структуры данных, а скорее индексы/итераторы этой строки, которые указывают на разделители.

 // assume that !, [, ], $, % etc. are your program defined delims
 const std::string vital = "!id[thisisdata]$[moredata]%[controlblock]%";

 // define a special struct
 enum Type { ... }; 
 struct Info {
     size_t start, end;
     Type type;
     // define appropriate ctors
 };

 // parse the string and return Info obejcts
 std::vector<Info> parse(const std::string& str) {
      std::vector<Info> v;
      // loop through the string looking for delims
      for (size_t b = 0, e = str.size(); b < e; ++b) {
            // on hitting one such delim create an Info
            switch( str[ b ] ) {
                case '%':
                  ... 
                case '$;:    
                // initializing the start and then move until
                // you get the appropriate end delim
            }
            // use push_back/emplace_back to insert this newly
            // created Info object back in the vector
            v.push_back( Info( start, end, kind ) );
      }
      return v;
 }
person dirkgently    schedule 08.06.2012
comment
-1: я полагаю, что вы беспокоитесь о распределении структуры, за которой следует копия строки в элемент структуры? Он говорит о распределении внутри std::string. - person Nicol Bolas; 08.06.2012
comment
@NicolBolas: я до сих пор не понимаю, что он говорит в строке. Я почти уверен, что это не так, поскольку строки имеют фиксированную ширину, если только вы и я не понимаем внутри строки. - person dirkgently; 08.06.2012
comment
Он говорит о двух распределениях памяти. Очевидно, это относится к конструкции типа, содержащего std::string, и строковому содержимому std::string. - person Nicol Bolas; 08.06.2012
comment
Разве это не именно то, что я имел в виду, то есть выделение структуры (первое) и копирование строки, как в оригинале, в элемент структуры (второе, а также о внутри строки)? - person dirkgently; 08.06.2012
comment
Вы не можете двигаться от char*; вы можете перейти только из другого std::string, которого у него почти наверняка нет, потому что это будет платить за распределение, за которое он не хочет платить. Тот факт, что вы можете переехать в std::string, не имеет отношения к проблеме, которую он пытается решить. Он хочет одно распределение, а не два. - person Nicol Bolas; 08.06.2012
comment
Я упомянул перемещение ctors, потому что созданные элементы структуры/строки, вероятно, нужно будет передавать (и, таким образом, могут быть введены временные элементы). Вот почему я сказал дополнительные копии строк. - person dirkgently; 09.06.2012