【问题标题】:Allocate a struct containing a string in a single allocation在单个分配中分配包含字符串的结构
【发布时间】:2012-06-08 12:11:47
【问题描述】:

我正在开发一个程序,该程序将重要数据结构存储为带有程序定义分隔符的非结构化字符串(因此我们需要遍历字符串并提取我们需要的信息)并且我们想转换将其转换为更结构化的数据类型。

本质上,这将需要一个结构体,其中一个字段描述结构体包含的数据类型,另一个字段是数据本身的字符串。字符串的长度在分配时总是已知的。我们通过测试确定,将每种数据类型所需的分配数量翻倍是不可接受的成本。有没有办法在一次分配中为结构和结构中包含的 std::string 分配内存?如果我们使用 cstrings,我只需要在结构中添加一个 char * 并在为结构和字符串分配足够大的块后将其指向结构的末尾,但如果可能的话,我们更喜欢 std::string。

我的大部分经验都是使用 C,所以请原谅此处显示的任何 C++ 无知。

【问题讨论】:

  • 如果我理解正确的话,字符串在构造后不能增长,内存将由管理整个对象的人管理......既然你要走 C 路,为什么不直接使用 @987654321 @? C 字符串最糟糕的部分是需要管理内存,但在您的情况下,这似乎不是问题,是吗?
  • +1 表示证明在半途而废之前需要进行优化!
  • 如果你真的热衷于使用std::string 而不是c-string,你可以考虑使用自定义分配器来实现你想做的一些事情。这可以让您将字符串集中在一个分配中,但它仍然不会一次性分配所有内容(即您的结构),并且最终可能会成为维护的噩梦。可能最好坚持使用 c-strings。

标签: c++ memory-management stdstring


【解决方案1】:

如果你有如此严格的内存需求,那么你将不得不放弃std::string

最好的替代方案是find 或编写basic_string_ref (a proposal for the next C++ standard library) 的实现,这实际上只是一个字符* 加上一个大小。但它具有std::basic_string 的所有(非变异)功能。然后你使用工厂函数来分配你需要的内存(你的结构体大小+字符串数据),然后使用placement new来初始化basic_string_ref

当然,您还需要一个自定义删除函数,因为您不能只将指针传递给“删除”。


鉴于previously linked to implementation of basic_string_ref(及其关联的typedef,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

【讨论】:

    【解决方案2】:

    如果你使用std::string,你不能真正为结构和字符串做一次分配,你也不能将两者都分配为一个大块。不过,如果您使用的是旧的 C 风格字符串,则可以。

    【讨论】:

      【解决方案3】:

      如果我对您的理解正确,您是说通过分析您已经确定您必须在数据结构中分配 string 和另一个数据成员这一事实给您的应用程序带来了不可接受的成本。

      如果确实如此,我可以想到几个解决方案。

      1. 您可以在程序启动之前预先分配所有这些结构。将它们保存在某种固定的集合中,这样它们就不会被复制构造,并且 reserve 在您的 strings 中有足够的缓冲区来保存您的数据。
      2. 虽然看起来有争议,但您可以使用旧的 C 风格 char 数组。似乎您首先忽略了使用strings 的大部分原因,即内存管理。但是,在您的情况下,由于您在启动时知道所需的缓冲区大小,因此您可以自己处理。如果您喜欢 string 提供的其他设施,请记住,&lt;algorithm&gt;s 中仍然可以使用其中的大部分功能。

      【讨论】:

        【解决方案4】:

        看看Variable Sized Struct C++ - 简短的回答是在普通 C++ 中没有办法做到这一点。

        你真的需要在堆上分配容器结构吗?将它们放在堆栈上可能更有效,因此根本不需要分配它们。

        【讨论】:

          【解决方案5】:

          确实,两个分配看起来太高了。不过有两种方法可以减少它们:

          • 进行一次分配
          • 进行一次动态分配

          可能看起来并没有那么不同,所以让我解释一下。

          1.您可以在 C++ 中使用struct hack

          • 是的,这不是典型的 C++
          • 是的,这需要特别小心

          技术上需要:

          • 禁用复制构造函数和赋值运算符
          • 制作构造函数和析构函数private,并提供用于分配和释放对象的工厂方法

          老实说,这是一条艰难的道路。

          2.您可以避免动态分配外部struct

          足够简单:

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

          然后在堆栈上传递M 的实例。移动操作应保证std::string 不会被复制(您可以随时禁用复制以确保这一点)。

          这个解决方案要简单得多。唯一(轻微)的缺点是内存局部性......但另一方面,堆栈的顶部已经在 CPU 缓存中。

          【讨论】:

            【解决方案6】:

            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++ 编译器一起工作。 (这是在 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,但将类型信息存储在第一个条目(或前几个)中。当然,这需要在您访问数据时考虑到这一点。

            【讨论】:

            • "按需创建一个 std::string 会很有效。"如何?每次你这样做,你都需要另一块内存。
            • 这真的取决于应用程序。正如我所提到的,性能损失可能不是因为std::string 进行分配,而是因为它对您的数据进行了碎片化。另一方面,创建短暂的std::string 临时对象不会使您的数据碎片化,并且如果运气好的话,可以快速分配并在缓存中分配。您甚至可以使用structured_data_reference::copy_into_string(std::string &amp;x) 之类的方法来避免分配,该方法将x 的内容替换为_data 中的数据副本。
            • 为了进一步解释,您的结构解决方案中的std::string 有两个问题。第一个是它执行 2 次分配(可能是 3 次),正如您所提到的。第二个是你的结构和字符串的内容可能位于内存的不同部分。当后者发生时,每次使用该结构时,都必须触及内存的两个不同部分。由于结构本身很小,如果内存中的结构附近没有任何其他立即有用的数据,则缓存和 TLB 效率都很低,并且在某些应用程序中,this 是主要的性能损失跨度>
            【解决方案7】:

            我不确定这是否能完全解决您的问题。您可以通过使用预分配缓冲区然后使用“放置新”运算符来优化 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 的评论,问题更清楚了。我决定再写一个答案,尽管实际上它并不比使用原始字符数组更有优势。但是,我仍然相信这完全在规定的限制范围内。 想法是为此SO question 中指定的字符串类提供自定义分配器。 在allocate方法的实现中,使用placement new as

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

            约束是,要使新的放置工作,分配器在运行时应该知道原始字符串地址。这可以通过在创建新字符串成员之前的外部显式设置来实现。然而,这并不是那么优雅。

            【讨论】:

            • 那没有用。这仅决定string 的分配位置,而不是string内容
            • 问题是“有没有办法在一次分配中为结构和结构中包含的 std::string 分配内存?”。问题的原因是“将这些数据类型中的每一种所需的分配数量增加一倍是不可接受的成本。”。我认为由于内存分配,性能很差。在这里,避免了分配(对于字符串和结构)。 (我同意如果字符串操作会降低性能,那么这还不够好。但是,从问题中并不清楚)
            • 不,不是。 std::string在内部分配内存。 是他想要消除的第二个内存分配。
            • @NicolBolas :我明白你在说什么,我想我应该修改我的答案
            【解决方案8】:

            本质上,这将需要一个结构体,其中包含一个描述结构体包含的数据类型的字段,以及另一个包含数据本身的字符串的字段。

            我有一种感觉,你可能没有在这里充分利用 C++ 的类型系统。它的外观和感觉都非常 C-ish(我知道,这不是一个恰当的词)。我没有具体的例子可以在这里发布,因为我对您要解决的问题一无所知。

            有没有办法在一次分配中为结构和结构中包含的 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;
             }
            

            【讨论】:

            • -1:“我相信你是担心结构分配后跟把字符串拷贝给结构成员?”他说的是分配within@ 987654322@.
            • @NicolBolas:我仍然不明白他在字符串中所说的位置。我相当肯定情况并非如此,因为字符串是固定宽度的,除非您和我对 字符串内 有不同的理解。
            • 他说的是两个内存分配。显然这是指包含std::string的类型的构造,以及std::string的字符串内容。
            • 这不正是我所指的,即结构分配(第一个)和将字符串原样复制到结构成员(第二个以及关于字符串内的 )?
            • 你不能从char*移动;您只能从另一个std::string 移动,而他几乎可以肯定没有,因为那将支付他不想支付的分配费用。您可以进入std::string 的事实与他试图解决的问题无关。他想要一个分配,而不是两个。
            猜你喜欢
            • 2021-11-30
            • 2019-05-01
            • 2017-02-15
            • 2016-05-16
            • 2021-02-05
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多