我有一棵巨大的树,可能需要几千兆字节。节点结构如下。您会注意到我将最后一个成员设置为大小为1的数组。原因是我可以过度分配具有灵活大小的Node
。类似于C本身作为灵活阵列成员支持的内容。我可以改用std::unique_ptr<T[]>
或std::vector<T>
,但问题是每个树节点都有双重动态分配,双重间接和额外的高速缓存未命中。在我的上一次测试中,这使我的程序花费了大约50%的时间,这对我的应用程序来说是不可接受的。
template<typename T>
class Node
{
public:
Node<T> *parent;
Node<T> *child;
/* ... */
T &operator[](int);
private;
int size;
T array[1];
};
实现operator[]
的最简单方法就是这样做。
template<typename T>
T &Node::operator[](int n)
{
return array[n];
}
它应该在大多数理智的C ++实现中正常工作。但是,由于C ++标准允许疯狂的实现进行数组边界检查,因为我知道这在技术上调用了未定义的行为。那我可以这样做吗?
template<typename T>
T &Node::operator[](int n)
{
return (&array[0])[n];
}
我在这里有点困惑。原始类型的[]
运算符只是*
的语法糖。因此,(&array[0])[n]
相当于(&*(array + 0))[n]
,我认为可以将其清除为array[n]
,使所有内容与第一个相同。好的,但我仍然可以这样做。
template<typename T>
T &Node::operator[](int n)
{
return *(reinterpret_cast<T *>(reinterpret_cast<char *>(this) + offsetof(Node<T>, array)) + n);
}
我希望我现在摆脱可能未定义的行为。也许内联汇编会更好地展示我的意图。但我真的必须走这么远吗?有人可以向我澄清事情吗?
顺便说一句T
始终是POD类型。整个Node
也是POD。
答案 0 :(得分:1)
“越界”数组访问的主要问题是没有任何对象存在。这不是导致问题的越界索引本身。 现在在你的情况下,可能是预定位置的原始内存。这意味着你实际上可以通过赋值在那里创建一个POD对象。任何后续的读取访问都将在那里找到对象。
根本原因是C实际上没有数组边界。 a[n]
只是*(a+n)
,按照定义。所以前两个提议的表格已经相同了。
我会更担心T array[1]
背后的任何填充,您将作为array[1]
的一部分访问。
答案 1 :(得分:1)
首先,一个实现可以自由地重新排序所有但很简单的情况下的类成员。您的案例并不简单,因为它具有访问说明符。除非你让你的POD类,或者它在C ++ 11中调用的任何东西(平凡的布局?),你不能保证你的数组实际上是最后布局的。
当然,C ++中不存在灵活成员。
然而,一切都没有丢失。分配一大块内存足够容纳你的类和你的数组,然后在开头放置你的类,并解释对象后面的部分(以及任何paddibg以确保正确对齐)作为数组。如果您有this
,则可以使用
reinterpret_cast<T*>(
reinterpret_cast<char*>(this) +
sizeof(*this) + padding))
选择adfing以使sizeof(T)
除以sizeof(*this) + padding
。
获取灵感,请查看std :: make_shared`。它还将两个对象打包到一个已分配的内存块中。
答案 2 :(得分:0)
您还想知道是否有替代方法。鉴于您最近关于“无重新分配”的评论,我将数组数据存储为指向堆分配存储的指针,但是:
树具有可预测的访问模式,从根到儿童。因此,我有一个Node::operator new
并确保子节点直接在其父节点之后分配。这为您在走树时提供了参考位置。其次,我有另一个数组数据的分配器,并为父数组和它们的第一个子进程返回连续的内存(当然后面是它的第一个孙子)。
结果是节点及其数组之间没有引用的局部性,而是获得树图和相关数组数据的引用局部性。
阵列数据分配器很可能是树的简单池分配器。只需一次分配256 KB块,并一次将它们分成几个整数。您需要跟踪的整个状态是您已经分配了多少256 kB。这比std::vector<T, std::allocator>
快得多,因为只要树生存,它就无法知道记忆的存在。