缓存友好矩阵,用于访问相邻单元格

时间:2016-08-23 17:53:19

标签: c++ caching optimization data-structures

当前设计

在我的程序中,我有一个大的 2-D 网格(1000 x 1000,或更多),每个单元格包含一个小信息。 为了表示这个概念,选择非常简单:矩阵数据结构

对应代码( in C ++ )类似于:

int w_size_grid = 1000;
int h_size_grid = 1000;
int* matrix = new int[w_size_grid * h_size_grid];

你可以注意到我使用过矢量,但原理是一样的。

为了访问网格的一个元素,我们需要一个给定网格中由(x,y)标识的单元格的函数,它返回存储在该单元格中的值。

在数学上: f(x,y) -> Z 明显: f: Z^2 -> Z其中Zinteger numbers的集合。

使用线性函数可以轻松实现。这里是 C ++ 代码表示:

int get_value(int x, int y) {
  return matrix[y*w_size_grid + x];
}

其他实施说明

实际上,设计概念需要一种"循环连续网格" 单元格的访问索引可以超出网格本身的限制

这意味着,例如,特定情况:get_value(-1, -1);仍然有效。该函数将返回与get_value(w_size_grid - 1, h_size_grid -1);相同的值。

实际上这不是实施中的问题:

int get_value(int x, int y) {
  adjust_xy(&x, &y);  // modify x and y in accordance with that rule.
  return matrix[y*w_size_grid + x];
}

无论如何,这只是一个额外的注释,以使场景更清晰。

有什么问题?

上面提到的问题非常简单,易于设计和实施。

我的问题在于矩阵以高频率更新。读取矩阵中的每个单元格,并可能使用新值进行更新。

显然,根据缓存友好的设计,矩阵的解析是通过两个循环完成的:

for (int y = 0; y < h_size_grid; ++y) {
  for (int x = 0; x < w_size_grid; ++x) {
    int value = get_value(x, y);
  }
}

内部周期为x,因为[x-1] [x] [x+1]是连续存储的。实际上,这个循环利用了principle of locality

问题出现了,因为实际上为了更新单元格中的值,它取决于相邻单元格中的值。

每个单元格恰好有八个邻居,即水平,垂直或对角相邻的单元格。

(-1,-1) | (0,-1) | (1,-1)
-------------------------
(-1,0)  | (0,0)  | (0, 1)
-------------------------
(-1,1)  | (0,1)  | (1,1)

所以代码是直观的:

for (int y = 0; y < h_size_grid; ++y) {
  for (int x = 0; x < w_size_grid; ++x) {
    int value = get_value(x, y);
    auto values = get_value_all_neighbours(x, y);  // values are 8 integer
  }
}

函数get_value_all_neighbours(x,y)将相对于y访问矩阵中的一行和一行。 由于矩阵中的一行非常大,所以它涉及缓存未命中,并且它会使缓存本身变脏。

问题

我终于向您介绍了方案和问题,我的问题是如何解决&#34;问题。

使用一些额外的数据结构,或重新组织数据是否有办法利用缓存并避免所有错过?

一些个人考虑因素

我的感受引导我走向战略数据结构。

我已经考虑过重新实现值存储在向量中的顺序,试图将这些单元格作为邻居存储在连续索引中。

这意味着get_value no-more-linear 函数。 经过一番思考后,我认为不可能找到这种非线性函数。

我还想到了一些额外的数据结构,比如 hash-table 来存储每个单元格的相邻值,但我认为在空间中可能更多,也许在CPU周期中也是如此。

2 个答案:

答案 0 :(得分:1)

实际上,数据结构并不简单,尤其是在进行优化时。

要解决两个主要问题:数据内容和数据使用情况。数据内容是数据中的值,用法是数据的存储,检索方式和频率。

数据内容

是否访问了所有值?经常?
不经常访问的数据可以推送到较慢的媒体,包括文件。保留经常访问的数据的快速内存(例如数据缓存)。

数据是否相似?有模式吗?
有许多表示矩阵的替代方法,其中大量数据是相同的(例如稀疏矩阵或下三角矩阵)。对于大型矩阵,可能执行某些检查并返回常量值可能更快或更有效。

数据使用

数据使用是确定数据有效结构的关键因素。即使有矩阵。

例如,对于频繁访问数据, map associative 数组可能更快。

有时,使用许多局部变量(即寄存器)对于处理矩阵数据可能更有效。例如,首先使用值加载寄存器(数据取出),使用寄存器进行操作,然后将寄存器存储回存储器。对于大多数处理器,寄存器是保存数据的最快媒体。

可能需要重新排列数据以有效使用数据缓存和缓存行。数据高速缓存是非常接近处理器核心的高速存储区域。高速缓存行是数据高速缓存中的一行数据。有效矩阵可以适合每个缓存行的一行或多行。

更有效的方法是尽可能多地访问数据缓存行。希望减少重新加载数据缓存的需要(因为索引超出范围)。

可以独立执行操作吗?
例如,缩放矩阵,其中每个位置乘以一个值。这些操作不依赖于矩阵的其他单元。这允许操作在 parallel 中执行。如果它们可以并行执行,则可以将它们委派给具有多个内核的处理器(例如GPU)。

摘要

当程序是数据驱动的时,数据结构的选择并非易事。在选择数据结构以及数据如何对齐时,内容和用法是重要因素。使用和性能要求也将决定访问数据的最佳算法。互联网上已经有很多文章可以优化数据驱动的应用程序和数据缓存的最佳使用。

答案 1 :(得分:1)

让我们假设您确实存在一个无法轻易避免的缓存未命中问题(请参阅此处的其他答案)。

您可以使用space filling curve以缓存友好的方式组织数据。本质上,空间填充曲线将体积或平面(例如矩阵)映射到线性表示,使得在空间中靠近在一起的值(大多数)在线性表示中靠近在一起。实际上,如果将矩阵存储在z有序数组中,则相邻元素很可能位于同一个内存页面上。

Hilbert曲线可以获得最佳的邻近映射,但计算起来很昂贵。更好的选择可能是z-curve(Morton-Order)。它提供了良好的接近性,并且计算速度快。

Z-Curve:基本上,为了获得排序,您必须将x和y坐标的位交错为单个值,称为“z-value&#39;”。此z值确定矩阵值列表中的位置(如果使用数组,甚至只是数组中的索引)。对于完全填充的矩阵(使用每个单元),z值是连续的。相反,您可以对列表中的位置进行反交错(=数组索引)并返回x / y坐标。

两个值的交错位非常快,甚至有专门的CPU指令来执行此操作,只需几个周期。如果你找不到这些(我现在不能),你可以简单地使用一些bit twiddling tricks