【问题标题】:How much of an object (containing std::vectors) is loaded in to the L1/L2/L3 cache?有多少对象(包含 std::vectors)被加载到 L1/L2/L3 缓存中?
【发布时间】:2014-05-15 21:06:09
【问题描述】:

请参阅以下链接,第 22 页起:

http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf

上面的链接表明我是否有一个包含这样的向量/数组的对象:

class MyClass{
    public:
    double a[1000];
    double b[1000];
};

下面的代码遍历 MyClass 的一个向量并在 std::vector b 上执行一些数学运算:

std::vector<MyClass> y;
y.populateVector();

for(auto x : y){
    //Iterate though x.b and do some math;
    for(int i=0; i<1000; i++){
        std::cout << x.b[i] << std::endl;
    }
}

当我们检索每个 MyClass 对象时,两个数组中的所有数据都将被加载到缓存行中。这是真的?我不认为数据a会被加载到缓存行,因为访问b的地址会被计算和加载。

我想知道与处理所需的有用数据相比,有多少 MyClass 对象被加载到缓存中?

我可以理解第一个 b 元素是否与最后一个 a 元素共享相同的缓存行,但我不认为整个对象会被加载到 L2/L3 缓存中只是为了处理一部分对象?

【问题讨论】:

  • 在您的情况下,双精度数组不直接存储在类中,而是存储在其他内存位置。 std::vector 只存储一个指针。所以访问b数据加载缓存中数据的可能性很小
  • 在您提到的文档中,数据直接存储在结构内部。你可以通过使用 double a[16];而不是 std::vector a;在这种情况下,a 和 b 在内存中是连续的,并且有合理的机会位于同一缓存行中。
  • 我将更改我的示例代码以反映这一点。
  • 据我所知,您的代码没有检索到 MyClass 对象。您只需遍历对它们的引用并访问它们的 b 成员。您认为您的代码如何构成“检索”?
  • @DavidSchwartz 对象的概念不是很松散吗?我刚刚在上面添加了一些代码,要访问 b 数组,我会说正在访问对象 MyClass?

标签: c++ performance optimization cpu data-oriented-design


【解决方案1】:

您的声明:

for(auto x : y) ...

x 声明为值而不是引用。编译器可能会优化将y 的每个元素复制到局部变量x 中,但我不会指望它。

如果你写:

for(auto &x : y) ...

然后循环将处理对y 中对象的引用。我假设这就是你的意思。

具体来说,忽略结构填充:编译器将转换

double temp = y[i].b[j];

变成等价于

double temp = *(
    y.data() + i * sizeof(MyClass) // start of y[i]
    + 1000 * sizeof(double)        // skip over y[i].a
    + j * sizeof(double));         // get to the right place in y[i].b

它会将包含该地址的缓存行大小的块加载到缓存行中。

然后,当您遍历 y[i].b 的更多元素时,其中许多元素已经在缓存中。

由于每个数组包含 1000 个元素,因此它们比典型 CPU 上的缓存线大得多。 1000 个 double 占用 8000 字节,而 Sandy Bridge 架构(例如)上的缓存行是 64 字节。遍历数组将有效地使缓存饱和。您可能会在x.a 的第一个和最后一个元素上浪费部分缓存行,但影响应该很小。 随着数组大小的增加,这些浪费的负载的重要性接近 0。

Playstation 文章讨论了大小与缓存线相当的对象。对于像您这样的大型对象,这些优化并不重要。

【讨论】:

    【解决方案2】:

    取决于系统上内存的组织方式。如果恰好ab 的后备数组在内存中的位置非常接近(因为CPU 通常会发出更大的读取来填充缓存以希望您使用它),那么它们可能会被加载。如果不是,我认为没有理由阅读 b 会暗示与 a 有任何关系,除了尝试从类实际驻留在内存中的位置读取一些指针。

    它确实表明,以随意的方式使用类可能并且将会导致缓存未命中,这仅仅是因为它们驻留在内存中的方式。

    加载到缓存中的一般规则是,如果 CPU 发出读取并错过缓存,它将从主内存加载缓存对齐的块(在示例中为 128 字节)。

    对于您编辑的示例,是的,这些是内存并存的部分,如果仅仅因为它们在内存中的位置而发出对 b 的读取,则可能会加载 a 的部分内容。

    对于您的示例,每个 MyClass 对象都包含一个 2000 * sizeof(double) 字节的连续区域(很可能是对齐的)。这些对象被打包到向量指向的连续内存区域中。访问每个对象的b 成员将导致缓存未命中(如果未缓存)。缓存对齐的内存块的内容将从每次未命中缓存的读取中加载。根据内存对齐约束和缓存大小,a 成员中的某些条目可能会被加载到内存中。甚至可以假设由于填充和对齐,您的任何MyClass a 成员都不会被加载到缓存中(并且没有理由因为它们未被访问)。

    【讨论】:

    • 你看到链接了吗?他们的演示幻灯片意味着加载了整个对象,即加载了向量 a 和 b?
    • 只是为了让你知道我将示例代码从向量更改为静态数组。
    • @user997112 我认为您将对象本身(由一些指针组成)与向量的内容(由这些指针指向)混淆了。如果整个对象是指两个指针,那么是的,它们最有可能被加载到内存中。
    • @user997112 更新了我对静态数组的回答。但不太可能读取所有a,因为它超过了缓存行的大小。
    • 我并没有从他们的示例中真正得到这一点,只是快速阅读它会显示内存中的对象以及被逐个加载到缓存中的对象。
    【解决方案3】:

    在您所指的链接中,ab 两个数组是 4x4 矩阵,这意味着每个矩阵有 16 个元素。由于这是关于视频游戏,它们很可能是浮点数。 16 个浮点数占用 64 个字节。 CPU 高速缓存行是 128 字节。所以很有可能a 的很大一部分与b[0] 在同一缓存行中。据统计,a 的 50% 将与b[0] 在同一缓存行中。读取b[0] 之后会将a 的那部分加载到缓存中。 如果您设法在 128 字节上对齐类/结构,您甚至可以保证 ab 完全适合同一缓存行。

    现在在您的示例中,您使用的不是 16 个浮点数,而是 1000 个双精度数。那是 8000 字节,比典型的高速缓存行大得多。 a 的最后几个元素可能与b[0] 在同一缓存行中,但影响会很小。

    【讨论】:

      猜你喜欢
      • 2014-05-26
      • 2020-05-19
      • 2014-07-04
      • 2017-03-10
      • 2014-02-16
      • 1970-01-01
      • 2018-01-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多