如何在内存中连续分配可变大小的结构?

发布于 2024-08-09 20:11:13 字数 592 浏览 4 评论 0原文

我正在使用 C++,并且具有以下结构:

struct ArrayOfThese {
  int a;
  int b;
};

struct DataPoint {
  int a;
  int b;
  int c;
};

在内存中,我希望在每个 DataPoint 的末尾有 1 个或多个 ArrayOfThese 元素。每个 DataPoint 的 ArrayOfThese 元素数量并不总是相同。

因为我有大量的数据点需要组装然后通过网络进行流式传输,所以我希望所有的数据点及其 ArrayOfThese 元素都是连续的。为固定数量的 ArrayOfThese 元素浪费空间是不可接受的。

在 C 中,我会在 DataPoint 末尾创建一个元素,将其声明为 ArrayOfThese d[0];,为我拥有的 ArrayOfThese 元素分配一个 DataPoint 加上足够的额外字节,并使用用于索引它们的虚拟数组。 (当然,ArrayOfThese 元素的数量必须位于 DataPoint 的字段中。)

在 C++ 中,使用放置 new 和相同的 0 长度数组 hack 是正确的方法吗?如果是这样,放置 new 是否能保证后续从同一内存池调用 new 会连续分配?

I'm using C++, and I have the following structures:

struct ArrayOfThese {
  int a;
  int b;
};

struct DataPoint {
  int a;
  int b;
  int c;
};

In memory, I want to have 1 or more ArrayOfThese elements at the end of each DataPoint. There are not always the same number of ArrayOfThese elements per DataPoint.

Because I have a ridiculous number of DataPoints to assemble and then stream across a network, I want all my DataPoints and their ArrayOfThese elements to be contiguous. Wasting space for a fixed number of the ArrayOfThese elements is unacceptable.

In C, I would have made an element at the end of DataPoint that was declared as ArrayOfThese d[0];, allocated a DataPoint plus enough extra bytes for however many ArrayOfThese elements I had, and used the dummy array to index into them. (Of course, the number of ArrayOfThese elements would have to be in a field of DataPoint.)

In C++, is using placement new and the same 0-length array hack the correct approach? If so, does placement new guarantee that subsequent calls to new from the same memory pool will allocate contiguously?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

赏烟花じ飞满天 2024-08-16 20:11:13

由于您正在处理没有构造函数的普通结构,因此您可以恢复到 C 内存管理:

void *ptr = malloc(sizeof(DataPoint) + n * sizeof(ArrayOfThese));
DataPoint *dp = reinterpret_cast<DataPoint *>(ptr));
ArrayOfThese *aotp = reinterpet_cast<ArrayOfThese *>(reintepret_cast<char *>(ptr) + sizeof(DataPoint));

Since you are dealing with plain structures that have no constructors, you could revert to C memory management:

void *ptr = malloc(sizeof(DataPoint) + n * sizeof(ArrayOfThese));
DataPoint *dp = reinterpret_cast<DataPoint *>(ptr));
ArrayOfThese *aotp = reinterpet_cast<ArrayOfThese *>(reintepret_cast<char *>(ptr) + sizeof(DataPoint));
空气里的味道 2024-08-16 20:11:13

由于您的结构体是 POD,因此您也可以像在 C 中一样进行操作。您唯一需要的就是强制转换。假设 n 是要分配的事物的数量:

DataPoint *p=static_cast<DataPoint *>(malloc(sizeof(DataPoint)+n*sizeof(ArrayOfThese)));

如果您的对象有一个不平凡的构造函数,则放置 new 确实会遇到这种情况。但它不保证任何分配,因为它不分配自身,并且要求内存已经以某种方式分配。相反,它将传入的内存块视为尚未构造的对象的空间,然后调用正确的构造函数来构造它。如果您要使用它,代码可能会像这样。假设 DataPoint 具有您建议的 ArrayOfThese arr[0] 成员:

void *p=malloc(sizeof(DataPoint)+n*sizeof(ArrayOfThese));
DataPoint *dp=new(p) DataPoint;
for(size_t i=0;i<n;++i)
    new(&dp->arr[i]) ArrayOfThese;

构造的内容必须被破坏,因此如果您这样做,您也应该整理析构函数的调用。

(我个人建议在这种情况下使用 POD,因为它消除了调用构造函数和析构函数的任何需要,但如果您小心的话,这种事情可以相当安全地完成。)

Since your structs are PODs you might as well do it just as you would in C. The only thing you'll need is a cast. Assuming n is the number of things to allocate:

DataPoint *p=static_cast<DataPoint *>(malloc(sizeof(DataPoint)+n*sizeof(ArrayOfThese)));

Placement new does come into this sort of thing, if your objects have a a non-trivial constructor. It guarantees nothing about any allocations though, for it does no allocating itself and requires the memory to have been already allocated somehow. Instead, it treats the block of memory passed in as space for the as-yet-unconstructed object, then calls the right constructor to construct it. If you were to use it, the code might go like this. Assume DataPoint has the ArrayOfThese arr[0] member you suggest:

void *p=malloc(sizeof(DataPoint)+n*sizeof(ArrayOfThese));
DataPoint *dp=new(p) DataPoint;
for(size_t i=0;i<n;++i)
    new(&dp->arr[i]) ArrayOfThese;

What gets constructed must get destructed so if you do this you should sort out the call of the destructor too.

(Personally I recommend using PODs in this sort of situation, because it removes any need to call constructors and destructors, but this sort of thing can be done reasonably safely if you are careful.)

梦初启 2024-08-16 20:11:13

正如阿德里安在他的回答中所说,你在内存中所做的事情不一定与你在流媒体中所做的事情相同通过网络。事实上,明确划分这一点甚至可能会更好,因为如果您以后需要重构数据,那么依赖于以特定方式设计的数据的通信协议会产生巨大的问题。

C++ 连续存储任意数量的元素的方式当然是std::vector。既然你甚至没有考虑到这一点,我认为有一些东西使得这种情况变得不可取。 (您是否只有少量的 ArrayOfThese 并担心与 std::vector 相关的空间开销?)

虽然过度分配零长度数组的技巧可能是不能保证有效,并且从技术上讲可能会引发可怕的未定义行为,这是一种广泛传播的行为。你在什么平台?在 Windows 上,这是在 Windows API 中完成的,因此很难想象供应商提供的 C++ 编译器不支持此功能。

如果可能的 ArrayOfThese 元素数量有限,您还可以使用 fnieto 的技巧指定这几个数字,然后根据运行时编号new生成的模板实例之一:

struct DataPoint {
  int a;
  int b;
  int c;
};

template <std::size_t sz>
struct DataPointWithArray : DataPoint {
  ArrayOfThese array[sz];
};

DataPoint* create(std::size_t n)
{
  switch(n) {
    case 1: return new DataPointWithArray[1];
    case 2: return new DataPointWithArray[2];
    case 5: return new DataPointWithArray[5];
    case 7: return new DataPointWithArray[7];
    case 27: return new DataPointWithArray[27];
    default: assert(false);
  }
  return NULL;
}

As Adrian said in his answer, what you do in memory doesn't have to be the same as what you stream over the network. In fact, it might even be good to clearly divide this, because having a communication protocol relying on your data being designed in a specific way makes huge problem if you later need to refactor your data.

The C++ way to store an arbitrary number of elements contiguously is of course to std::vector. Since you didn't even consider this, I assume that there's something that makes this undesirable. (Do you only have small numbers of ArrayOfThese and fear the space overhead associated with std::vector?)

While the trick with over-allocating a zero-length array probably isn't guaranteed to work and might, technically, invoke the dreaded undefined behavior, it's a widely spread one. What platform are you on? On Windows, this is done in the Windows API, so it's hard to imagine a vendor shipping a C++ compiler which wouldn't support this.

If there's a limited number of possible ArrayOfThese element counts, you could also use fnieto's trick to specify those few numbers and then new one of the resulting template instances, depending on the run-time number:

struct DataPoint {
  int a;
  int b;
  int c;
};

template <std::size_t sz>
struct DataPointWithArray : DataPoint {
  ArrayOfThese array[sz];
};

DataPoint* create(std::size_t n)
{
  switch(n) {
    case 1: return new DataPointWithArray[1];
    case 2: return new DataPointWithArray[2];
    case 5: return new DataPointWithArray[5];
    case 7: return new DataPointWithArray[7];
    case 27: return new DataPointWithArray[27];
    default: assert(false);
  }
  return NULL;
}
饮湿 2024-08-16 20:11:13

在 C++0X 之前,该语言没有内存模型可言。对于新标准,我不记得有任何关于连续性保证的讨论。

关于这个特定问题,听起来好像您想要的是一个池分配器,存在很多这样的例子。例如,现代 C++ 设计,作者:Alexandrescu。小对象分配器讨论是您应该关注的内容。

Prior to C++0X, the language had no memory model to speak of. And with the new standard, I don't recall any talk of guarantees of contiguity.

Regarding this particular question, it sounds as if what you want is a pool allocator, many examples of which exist. Consider, for instance, Modern C++ Design, by Alexandrescu. The small object allocator discussion is what you should look at.

偷得浮生 2024-08-16 20:11:13

我认为 boost::variant 可能会实现这一点。我还没有机会使用它,但我相信它是联合的包装器,因此它们的 std::vector 应该是连续的,但当然每个项目都会占用更大的空间在这两种大小中,您不能拥有具有不同大小元素的向量。

看一下比较boost::variant 和 boost::any

如果您希望每个元素的偏移量取决于前面元素的组成,则必须编写自己的分配器和访问器。

I think boost::variant might accomplish this. I haven't had an opportunity to use it, but I believe it's a wrapper around unions, and so a std::vector of them should be contiguous, but of course each item will take up the larger of the two sizes, you can't have a vector with differently-sized elements.

Take a look at the comparison of boost::variant and boost::any.

If you want the offset of each element to be dependent on the composition of the previous elements, you will have to write your own allocator and accessors.

花开雨落又逢春i 2024-08-16 20:11:13

似乎分配一个指针数组并使用它比使用新的放置更简单。这样你就可以将整个数组重新分配到新的大小,而运行时成本却很小。另外,如果使用新的放置,则必须显式调用析构函数,这意味着在单个数组中混合非放置和放置是危险的。阅读 http://www.parashift.com/c++-faq-lite/dtors .html 在你做任何事情之前。

Seems like it would be simpler to allocate an array of pointers and work with that rather than using placement new. That way you could just reallocate the whole array to the new size with little runtime cost. Also if you use placement new, you have to explicitly call destructors, which means mixing non-placement and placement in a single array is dangerous. Read http://www.parashift.com/c++-faq-lite/dtors.html before you do anything.

太傻旳人生 2024-08-16 20:11:13

不要混淆程序内部的数据组织和序列化数据组织:它们没有相同的目标。

对于通过网络进行流式传输,您必须考虑通道的两侧,即发送方和接收方:接收方如何区分 DataPoint 和 ArrayOfThese ?接收方如何知道在 DataPoint 之后附加了多少个 ArrayOfThese ? (还要考虑:每一侧的字节顺序是什么?数据类型在内存中的大小是否相同?)

就我个人而言,我认为您需要一个不同的结构来流式传输数据,在其中添加要发送的 DataPoint 的数量以及每个 DataPoint 之后的 ArrayOfThese 数量。我也不关心数据在我的程序中已经组织的方式,并重新组织/重新格式化以适合我的协议而不是我的程序。之后编写一个用于发送的函数和另一个用于接收的函数就不是什么大问题了。

don't confuse data organisation inside your program and data organisation for serialization: they do not have the same goal.

for streaming across a network, you have to consider both side of the channel, the sending and the receiving side: how does the receiving side differentiate between a DataPoint and an ArrayOfThese ? how does the receiving side know how many ArrayOfThese are appended after a DataPoint ? (also to consider: what is the byte ordering of each side ? does data types have the same size in memory ?)

personally, i think you need a different structure for streaming your data, in which you add the number of DataPoint you are sending as well as the number of ArrayOfThese after each DataPoint. i would also not care about the way data is already organized in my program and reorganize/reformat to suit my protocol and not my program. after that writing a function for sending and another for receiving is not a big deal.

染柒℉ 2024-08-16 20:11:13

为什么不让 DataPoint 包含 ArrayOfThese 项目的可变长度数组?这适用于 C 或 C++。如果任一结构体包含非基本类型,则会出现一些问题,

但对结果使用 free() 而不是 delete

struct ArrayOfThese {
  int a;
  int b;
};


struct DataPoint {
  int a;
  int b;
  int c;
  int length;
  ArrayOfThese those[0];
};

DataPoint* allocDP(int a, int b, int c, size_t length)
{
    // There might be alignment issues, but not for most compilers:
    size_t sz = sizeof(DataPoint) + length * sizeof(ArrayOfThese);
    DataPoint dp = (DataPoint*)calloc( sz );
    // (Check for out of memory)
    dp->a = a; dp->b = b; tp->c = c; dp->length = length;
}

然后您可以在循环中“正常”使用它,其中DataPoint 知道它的长度:

DataPoint *dp = allocDP( 5, 8, 3, 20 );

for(int i=0; i < dp->length; ++i)
{
    // Initialize or access: dp->those[i]
}

Why not have DataPoint contain a variable-length array of ArrayOfThese items? This will work in C or C++. There are some concerns if either struct contains non-primitive types

But use free() rather than delete on the result:

struct ArrayOfThese {
  int a;
  int b;
};


struct DataPoint {
  int a;
  int b;
  int c;
  int length;
  ArrayOfThese those[0];
};

DataPoint* allocDP(int a, int b, int c, size_t length)
{
    // There might be alignment issues, but not for most compilers:
    size_t sz = sizeof(DataPoint) + length * sizeof(ArrayOfThese);
    DataPoint dp = (DataPoint*)calloc( sz );
    // (Check for out of memory)
    dp->a = a; dp->b = b; tp->c = c; dp->length = length;
}

Then you can use it "normally" in a loop where the DataPoint knows its length:

DataPoint *dp = allocDP( 5, 8, 3, 20 );

for(int i=0; i < dp->length; ++i)
{
    // Initialize or access: dp->those[i]
}
橘和柠 2024-08-16 20:11:13

您能否将它们制作成具有相同超类的类,然后使用您最喜欢的选择的 stl 容器,并使用超类作为模板?

Could you make those into classes with the same superclass and then use your favourite stl container of choice, using the superclass as the template?

弥繁 2024-08-16 20:11:13

两个问题:

  1. ArrayOfThese 和 DataPoint 之间的相似性是真实的,还是发布的简化?即真正的区别只是一个整数(或任意数量的相同类型的项目)?
  2. 与特定 DataPoint 关联的 ArrayOfThese 数量在编译时是否已知?

如果第一个是正确的,我会认真考虑简单地为一个 DataPoint+N ArrayOfThese 分配一个包含尽可能多的项目的数组。然后,我将构建一段快速代码来重载operator[]以返回项目N+3,并重载a()、b()和c()以返回前三个项目。

如果第二个是真的,我基本上会建议我看到 fnieto 刚刚发布的内容,所以我不会详细说明。

就放置新而言,它并不能真正保证有关分配的任何信息 - 事实上,放置新的整个想法是它与内存分配完全无关。相反,它允许您在已分配的内存块中的任意地址(受对齐限制)创建对象。

Two questions:

  1. Is the similarity between ArrayOfThese and DataPoint real, or a simplification for posting? I.e. is the real difference just one int (or some arbitrary number of the same type of items)?
  2. Is the number of ArrayOfThese associated with a particular DataPoint known at compile time?

If the first is true, I'd think hard about simply allocating an array of as many items as necessary for one DataPoint+N ArrayOfThese. I'd then build a quick bit of code to overload operator[] for that to return item N+3, and overload a(), b() and c() to return the first three items.

If the second is true, I was going to suggest essentially what I see fnieto has just posted, so I won't go into more detail.

As far as placement new goes, it doesn't really guarantee anything about allocation -- in fact, the whole idea about placement new is that it's completely unrelated to memory allocation. Rather, it allows you to create an object at an arbitrary address (subject to alignment restrictions) in a block of memory that's already allocated.

因为看清所以看轻 2024-08-16 20:11:13

这是我最终编写的代码:

#include <iostream>
#include <cstdlib>
#include <cassert>
using namespace std;

struct ArrayOfThese {
  int e;
  int f;
};

struct DataPoint {
  int a;
  int b;
  int c;
  int numDPars;
  ArrayOfThese d[0];

  DataPoint(int numDPars) : numDPars(numDPars) {}

  DataPoint* next() {
    return reinterpret_cast<DataPoint*>(reinterpret_cast<char*>(this) + sizeof(DataPoint) + numDPars * sizeof(ArrayOfThese));
  }

  const DataPoint* next() const {
    return reinterpret_cast<const DataPoint*>(reinterpret_cast<const char*>(this) + sizeof(DataPoint) + numDPars * sizeof(ArrayOfThese));
  }
};

int main() {
  const size_t BUF_SIZE = 1024*1024*200;

  char* const buffer = new char[BUF_SIZE];
  char* bufPtr = buffer;

  const int numDataPoints = 1024*1024*2;
  for (int i = 0; i < numDataPoints; ++i) {
    // This wouldn't really be random.
    const int numArrayOfTheses = random() % 10 + 1;

    DataPoint* dp = new(bufPtr) DataPoint(numArrayOfTheses);

    // Here, do some stuff to fill in the fields.
    dp->a = i;

    bufPtr += sizeof(DataPoint) + numArrayOfTheses * sizeof(ArrayOfThese);
  }

  DataPoint* dp = reinterpret_cast<DataPoint*>(buffer);
  for (int i = 0; i < numDataPoints; ++i) {
    assert(dp->a == i);
    dp = dp->next();
  }

  // Here, send it out.

  delete[] buffer;

  return 0;
}

Here's the code I ended up writing:

#include <iostream>
#include <cstdlib>
#include <cassert>
using namespace std;

struct ArrayOfThese {
  int e;
  int f;
};

struct DataPoint {
  int a;
  int b;
  int c;
  int numDPars;
  ArrayOfThese d[0];

  DataPoint(int numDPars) : numDPars(numDPars) {}

  DataPoint* next() {
    return reinterpret_cast<DataPoint*>(reinterpret_cast<char*>(this) + sizeof(DataPoint) + numDPars * sizeof(ArrayOfThese));
  }

  const DataPoint* next() const {
    return reinterpret_cast<const DataPoint*>(reinterpret_cast<const char*>(this) + sizeof(DataPoint) + numDPars * sizeof(ArrayOfThese));
  }
};

int main() {
  const size_t BUF_SIZE = 1024*1024*200;

  char* const buffer = new char[BUF_SIZE];
  char* bufPtr = buffer;

  const int numDataPoints = 1024*1024*2;
  for (int i = 0; i < numDataPoints; ++i) {
    // This wouldn't really be random.
    const int numArrayOfTheses = random() % 10 + 1;

    DataPoint* dp = new(bufPtr) DataPoint(numArrayOfTheses);

    // Here, do some stuff to fill in the fields.
    dp->a = i;

    bufPtr += sizeof(DataPoint) + numArrayOfTheses * sizeof(ArrayOfThese);
  }

  DataPoint* dp = reinterpret_cast<DataPoint*>(buffer);
  for (int i = 0; i < numDataPoints; ++i) {
    assert(dp->a == i);
    dp = dp->next();
  }

  // Here, send it out.

  delete[] buffer;

  return 0;
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文