保持类的向量成员与类实例连续

Keep vector members of class contiguous with class instance

本文关键字:实例 成员 连续 向量      更新时间:2023-10-16

我有一个类,它实现了两个简单的、预先确定大小的堆栈;这些被存储为构造函数预先确定大小的类型向量类的成员。它们是小型且缓存行大小友好的对象。这两个堆栈的大小是恒定的,持久化和延迟更新,并且通常通过一些计算成本低廉的方法一起访问,然而,这些方法可以被称为大量(每秒数万到数十万次)。

所有对象都已经处于良好状态(代码是干净的,并且做了它应该做的事情),所有大小都在控制之下(对于包括结果在内的整个操作链,大多数情况下是64k到128K,很少接近256k,所以更糟糕的是L2查找,通常是L1)。

一些自动矢量化开始发挥作用,但除此之外,它始终是单线程代码。

这个类,减去一些次要的东西和填充,看起来是这样的:

class Curve{
private:
    std::vector<ControlPoint> m_controls;
    std::vector<Segment> m_segments;
    unsigned int m_cvCount;
    unsigned int m_sgCount;
    std::vector<unsigned int> m_sgSampleCount;
    unsigned int m_maxIter;
    unsigned int m_iterSamples;
    float m_lengthTolerance;
    float m_length;
}
Curve::Curve(){
    m_controls = std::vector<ControlPoint>(CONTROL_CAP);
    m_segments = std::vector<Segment>( (CONTROL_CAP-3) );
    m_cvCount = 0;
    m_sgCount = 0;
    std::vector<unsigned int> m_sgSampleCount(CONTROL_CAP-3);
    m_maxIter = 3;
    m_iterSamples = 20;
    m_lengthTolerance = 0.001;
    m_length = 0.0;
}
Curve::~Curve(){}

请耐心地说,我正在努力自学,并确保我不会被一些半信半疑的知识所左右:

考虑到在这些操作上运行的操作及其实际使用情况,性能在很大程度上取决于内存I/O。我有几个问题与数据的最佳定位有关,请记住,这是在英特尔CPU(Ivy和一些Haswell)上,使用GCC 4.4,我没有其他用例:

我假设,如果控件和段的实际存储与Curve的实例相邻,那么这是缓存的理想场景(从大小上看,这个块可以很容易地放在我的目标CPU上)。一个相关的假设是,如果向量远离曲线的实例,并且在它们之间,当方法交替访问这两个成员的内容时,将更频繁地驱逐和重新填充L1缓存。

1) 这是正确的吗(从新操作中第一次查找的地址中提取整个缓存大小的数据,而不是在方便的适当大小的多个段中),还是我对缓存机制理解错误,缓存可以提取并保留多个较小的ram?

2) 根据以上内容,在纯粹的情况下,我的所有测试总是以类的实例和向量的连续性结束,但我认为这只是运气不好,无论在统计上多么可能。通常情况下,实例化类只为该对象保留空间,然后将向量分配到下一个可用的空闲连续块中,如果之前在内存中发现了一个较小的空位,则不能保证它靠近我的Curve实例。这是正确的吗?

3) 假设1和2是正确的,或者从功能上讲足够接近,我知道为了保证性能,我必须编写一个分配器,以确保类对象本身在实例化时足够大,然后自己复制其中的向量,并从此引用这些向量。如果这是解决问题的唯一方法,我可能会破解这样的方法,但如果有很好/聪明的方法来解决这样的问题,我宁愿不要破解得很糟糕。任何关于最佳实践和建议方法的指针都会非常有用(除了"不要使用malloc,它不能保证连续",我已经记下了:)。

  1. 如果曲线实例适合一条缓存线,并且两个向量的数据也适合每条缓存线,那么情况就没那么糟糕了,因为那时有四条恒定的缓存线。如果每个元素都是间接访问的,并且是随机放置在内存中的,那么对元素的每次访问都可能会花费一次获取操作,在这种情况下可以避免这种情况。如果Curve及其元素都能容纳不到四个缓存线,那么将它们放入连续存储会带来好处。

  2. 没错。

  3. 如果使用std::array,则可以保证元素嵌入到所属类中,并且不具有动态分配(这本身就要花费内存空间和带宽)。如果您使用一个特殊的分配器将向量内容与Curve实例放在连续存储中,那么您甚至可以避免仍然存在的间接访问。

BTW:简短备注:

Curve::Curve()
{
  m_controls = std::vector<ControlPoint>(CONTROL_CAP, ControlPoint());
  m_segments = std::vector<Segment>(CONTROL_CAP - 3, Segment());
  ...
}

应该这样写:

Curve::Curve():
  m_controls(CONTROL_CAP),
  m_segments(CONTROL_CAP - 3)
{
  ...
}

这被称为"初始值设定项列表",搜索该术语以获得进一步的解释。此外,您作为第二个参数提供的默认初始化元素已经是默认元素,因此无需显式指定。