【问题标题】:How much of an array is stored within a cache line?一个缓存行中存储了多少数组?
【发布时间】:2014-09-23 23:01:47
【问题描述】:
#include <iostream>
#include <stdint.h>

using namespace std;

struct UIContainer { 
  uint16_t x, y;  //Position on the screen
  uint16_t h, w;  //Height and width of the UIContainer
  uint16_t color; //Color, rgba such as 0xFF000000 & color is red, 0x00FF0000 is green, 0x0000FF00 is blue, 0x000000FF is alpha
  uint16_t ID;    //Unique ID of the ui container    
}; //16 bytes big


void drawUI(UIContainer _container, SDL_Renderer* _renderer) {
    SDL_Rect rect {.x = _container.x, .y = _container.y, .h = _container.h, .w = _container.w }
    uint8_t r = color & 0xFF000000;
    uint8_t g = color & 0x00FF0000;
    uint8_t b = color & 0x0000FF00;
    uint8_t a = color & 0x000000FF;

    SDL_SetRenderDrawColor(_renderer, r, g, b, a);
    SDL_RenderFillRect(_renderer, &rect); 
}

int main()
{
   UIContainer UIContainers[1024]; //16 * 1024 is 16384 bytes = 16 kilobytes
   SDL_Renderer* renderer; //Pretend it is initialized

   //Draw all the UI
   int i = 0;
   for(i; i < 1024; ++i) {
       drawUI(_container, renderer); 
   }

   return 0;
}

我决定尝试了解数据本地化以及如何提高缓存的利用率。假设 L1 缓存为 64 KB,我假设 UIContainer 的整个数组将被加载到缓存中是正确的,因为 16KB 小于 64KB?如果缓存行是 128 字节,那将是每行 8 个 UIContainer 块?

据我了解,当某些内容当前不在缓存中时,会发生缓存未命中。这也适用于缓存行吗?例如,我在容器 [3] 上操作,然后我想跳到容器 [100],这会导致缓存未命中,因为它必须跳到容器 [100] 所在的任何缓存行?

最后,假设我将 UIContainer 的所有内部部分提取到它们自己的单独数组中,因此代码现在看起来像:

#include <iostream>
#include <stdint.h>

using namespace std;

struct location {
  uint16_t x, y;  //Position on the screen
}; //4 bytes

struct size {
  uint16_t h, w;  //Height and width of the UIContainer
}; //4 bytes

struct color {
  uint32_t color; //Color, rgba such as 0xFF000000 & color is red, 0x00FF0000 is green, 0x0000FF00 is blue, 0x000000FF is alpha
} //4 bytes

struct UIContainer {
  uint32_t ID;    //Unique ID of the ui container    
}; //4 bytes


void drawUI(location l, size s, color c, SDL_Renderer* _renderer) {
    SDL_Rect rect {.x = l.x, .y = l.y, .h = s.h, .w = s.w }
    uint8_t r = c & 0xFF000000;
    uint8_t g = c & 0x00FF0000;
    uint8_t b = c & 0x0000FF00;
    uint8_t a = c & 0x000000FF;

    SDL_SetRenderDrawColor(_renderer, r, g, b, a);
    SDL_RenderFillRect(_renderer, &rect); 
}

int main()
{
   UIContainer UIContainers[1024]; //4 * 1024 is 4048 bytes = 4 kilobytes
   location _location[1024];    //4 KB
   size _size[1024];            //4KB
   color _color[1024];          //4KB
   //////////////////////////////////////// 16 KB Total



   SDL_Renderer* renderer; //Pretend it is initialized

   //Draw all the UI
   int i = 0;
   for(i; i < 1024; ++i) {
       drawUI(_location[i], _size[i], _color[i], renderer); 
   }

   return 0;
}

这会导致缓存未命中吗?我认为不会,因为 _location[]、_size[] 和 _color[] 都在缓存中,并且被线性访问?还是我错过了什么?

【问题讨论】:

  • 你在实现编译器吗?如果不是,您可能不应该在意,只要数据是连续排列的,直到您通过测量证明存在问题。
  • “你可能不关心”实际上是最糟糕的建议,尤其是当“我决定尝试了解数据本地化以及如何提高缓存的利用率。”
  • 缓存行为并非完全可预测且极其复杂。您可能需要查看what every programmer needs to know about memory 以了解其工作原理。
  • 好吧,直到您看到(通过测量)某些代码对于您的特定用例来说太慢了,优化您的特定代码是浪费时间。缓存利用率本身是一个非常复杂的领域(就像所有指令级优化一样),它不(或不应该)依赖于除了连续/非连续数据布局之外的“高级”代码。如果您确实实现了编译器,那么您必须关心,如果没有,编译器应该(并且如果体面的话)会关心您。这实际上与您的特定代码无关,而是与应如何将特定 C++ 语法转换为程序集有关。
  • @Baum 这是一个糟糕的建议。如果你关心高性能,你真的需要在你的算法中考虑缓存行为。假设我要实现软件光栅化器,我知道(无需测量)我应该使用混合纹理布局而不是线性,以获得更好的缓存一致性和性能。编译器不会提高算法的缓存一致性。

标签: c++ arrays caching


【解决方案1】:

第一件事

struct UIContainer  {
  uint16_t x, y;  //Position on the screen
  uint16_t h, w;  //Height and width of the UIContainer
  uint16_t color; //Color 
  uint16_t ID;    //Unique ID of the ui container
}; //16 bytes big
static_assert(sizeof(struct UIContainer) == 12, "12 hmm not the case");
static_assert(sizeof(struct UIContainer) == 16, "16 hmm not the case"); // fails, because the last 2 should be uint32_t???

我决定尝试学习数据本地化以及如何 提高缓存的利用率。假设 L1 缓存为 64 KB, 我假设 UIContainer 的整个数组将是正确的 加载到缓存中,因为 16KB 小于 64KB?

您的 UIContainer 是一个自动变量,并且元素没有构造函数,因此数组不会自动加载到缓存中。

  • 如果它是全局的,容器或元素有一个构造函数,那么它就会被初始化,因此在初始化期间加载到缓存中。
  • 如果它被较早初始化,它也可能被初始化之后但在您的代码之前运行的代码从缓存中清除。

如果缓存行是 128 字节,那将是每行 8 个 UIContainer 块?

  • 如果你有一个 128 字节的缓存行
  • 容器为 16 个字节
  • 它们与容器大小对齐,即。 16 字节

那么每个缓存行将有 8 个容器。

据我了解,缓存未命中会在某些情况下发生 目前在缓存中。这也适用于缓存行吗? 例如,我正在容器 [3] 上操作,然后我想跳到 container[100],这将导致缓存未命中,因为它必须跳过 到任何缓存行容器[100] 所在的位置?

高速缓存由高速缓存行组成,它们是主内存的副本。当您阅读有关加载缓存行的内容时,实际上是在将数据加载到缓存行,因为它们是缓存的物理部分。

  • 缓存未命中是指缓存中没有缓存行包含所请求数据的地址。
  • 缓存然后请求下一个较低的缓存或主内存来转发数据。
  • 带有 DDR3 的主存储器通常会发送 8*8 字节,这也是典型的高速缓存行大小!拥有 128 字节的高速缓存行大小会导致它在连续地址上产生 2 个突发。
  • 内存和较低的缓存非常喜欢连续的地址访问,这提供了最高的吞吐量。
  • L1 缓存在随机访问方面几乎与流式处理一样好。

假设只有你的程序在这个处理器上运行:

  • 您将获得最少的缓存强制未命中,该数量等于所触及的缓存行数。因为您的任何数据都不会在缓存中。
  • 如果您访问的缓存行数超过了缓存所能容纳的数量,您将获得那么多容量未命中
  • 此外,如果您访问同一地址 %CacheLineSize 的次数超过了缓存次数的关联性,您将获得冲突未命中。对于 64K 缓存,您很可能有 8 路或 16 路缓存,因为缓存很可能分为页面大小(4096 字节)部分,在本例中为 16。

这会导致缓存未命中吗?我不认为它会,因为 _location[]、_size[] 和 _color[] 都在缓存中,并且被线性访问?还是我错过了什么?

从不知道缓存中存在某些内容,但如果您最近访问过它,则它存在的可能性更高,这称为空间和时间局部性。

如果您测量/分析它,您只能说哪个是最好的,然后可以在代码中进行微小的更改来扭转它。

当您像上一个代码一样流式传输数据时,缓存通常是满意的,但这里有一些问题

  • 您使用 3 个不同的数据源(数组),许多架构一次只支持 2 个预取流。
  • 对于您的小示例应该没有区别,一旦您的活动数据集的大小超过 L1 数据缓存大小,您将受到严重打击。
  • 因为所有访问实际上都是在不同高速缓存行级别(和内存)之间的高速缓存行大小中,使用高速缓存行的一小部分成本与全部使用相同。
    • 在您的两个示例中,对较低级别缓存的任何访问都将很好,因为程序将使用缓存行中的所有内容除了 ID,可能成为第二个程序运行速度更快,因为它需要访问的缓存行减少了 25%,但在最坏的情况下进行测量之前,您无法确定。
    • 使用数组而不是链表通常对任何程序都有很大的改进,所以不要改变它。

更多细节请关注caches

我还没有看到任何 128 字节的缓存线,但我主要还是看到 Intel 和 AMD。

【讨论】:

    【解决方案2】:

    存储在处理器缓存中的数组数量取决于数组的大小(以字节为单位)和处理器数据缓存的容量以及来自其他结构的任何剩余数据。

    抱歉,没有标准的数据缓存大小。就此而言,并不要求所有平台都具有数据缓存。

    通常,数据缓存仅在对同一组数据执行许多数学运算时才重要。例如,对数组的搜索不能证明使用数据缓存是合理的。可以加载数据并且只进行一次传递。其他操作,例如数据平滑、快速傅立叶变换和矩阵旋转,涉及对数据的不止一次访问。性能提升是在数据的第一次传递之后出现的。

    最好的方法是分析您当前的代码,记下平均运行时间。更改代码以更好地使用数据缓存。再次配置文件。将结果与第一个(原始执行)进行比较。

    我在我的一个程序中更改了数据的布局,使其对数据缓存更加友好,并将性能提高了 30%。

    编辑 1:
    为了回答这个问题,根据标题,存储在缓存行中的数据量就是程序正在访问的数据量。有些处理器可能会获取更多,有些可能不会。取决于缓存中已有的内容、处理器的缓存加载算法、缓存行的容量和数据缓存的容量。

    【讨论】:

      【解决方案3】:

      当您第一次访问数据时,您总是会遇到缓存未命中。之后,如果数据仍在缓存中,则取决于缓存的特征(缓存和缓存行大小、关联性等)和内存访问模式(线性、随机等)。通常,最好让您读取的数据彼此靠近,以便在一个缓存行提取中读取尽可能多的有用数据。例如,您有 3 个线性访问的数组(_location、_size、_color),但从缓存的角度来看,使用交错数据格式会更有效,其中您有一个数组,其中位置、大小和颜色数据在每个数组旁边交错其他。虽然理论上您应该在非交错和交错情况下获得相同数量的缓存未命中,但您的 drawUI() 函数可能会导致一些数据从缓存中被逐出,或者在后台运行的一些其他进程可能会刷新数据从缓存中。另外要记住的一点是,处理器将尝试预测您的访问模式并将数据预取到缓存以避免内存停顿。

      话虽如此,您需要考虑增加缓存一致性的潜在复杂性是否真的值得。如果您有一些非常高性能的循环,这些循环可能会对性能产生重大影响,那是肯定的。但在许多情况下,它不会给你带来太多好处,而且不值得付出努力和增加复杂性来过度担心它。有些人会告诉您进行分析,然后在必要时决定优化缓存。然而,我们中的许多人在现实世界中开发软件并没有多次编写相同算法的奢侈,并且必须根据我们的经验对重要的地方进行有根据的猜测。因此,虽然过早的优化是万恶之源,但迟来的悲观化是恶果。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-14
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多