如何有效地为具有不同自由度的单元构建内存中的模拟数据?

计算科学 高性能计算 效率 不连续-galerkin 数据结构
2021-12-22 02:42:44

对于不连续的基于 Galerkin 的模拟,我需要将基于单元的模拟数据存储在内存中。由于多项式近似的阶数可能在单元之间有所不同,我想知道如果典型的访问模式是对所有单元的循环,最有效的数据结构是什么?这些是我到目前为止的想法:Np

1)使用单元对象数组,每个对象都有一个指向其(单独分配的)模拟数据数组的指针
Pro:非常容易在算法上处理,在为数据存储预分配内存时不头疼,非常容易存储单元之间的拓扑关系(即邻居信息),直接直接访问特定小区。
缺点:循环遍历所有单元格时非常(非常?)慢,因为每次都可能缓存未命中。

2)使用单元对象数组,每个对象都有一个指向其模拟数据数组的指针(内存预分配和单元数据连续存储)
Pro:同上。也可能更快,因为数据现在在内存中是连续的。
缺点:仍然比仅使用数组慢,因为每个单元访问仍然需要的知识。Np

3) 为每组具有公共Np
Pro 的单元格使用一个数组(或一个 STL 容器):每个容器可以连续包含数据,因此如果先遍历,然后再遍历单元格,将会有很多缓存命中。缺点:如果需要内存预分配,则很困难。仅使用小区 ID 的随机小区访问变得非常昂贵。Np

有没有人有上述方法之一的经验,或者有更好的选择?如果您需要有关例如数据访问模式的更多信息,请告诉我。

注意:这个问题与 Stackoverflow 上的这个问题类似但经过改写和简化。

3个回答

deal.II 使用您的选项 (2) 的变体。当然,这本质上也是存储稀疏矩阵的方式,并且非常有效,因为您只有两个数组(每个单元格存储与该单元格对应的数据的开始索引的数组,以及连续存储所有所有单元格的数据)。您按顺序遍历两个数组,产生很少的缓存未命中。您的两个选项要么效率极低(选项 1),要么非常浪费非连续访问(选项 3)。

我使用选项 3 的变体,即:N 个独立容器,每个单元格/元素类型/顺序一个,以便相同类型的所有单元格在内存中对齐。当我想对所有单元(积分、求积、演化、数值通量...)执行 x 操作时,我会:

for each container<cell_type> in cell_containers
  for each cell in container<cell_type>
     do x<cell_type>(cell)

其中 x 是针对每种细胞类型的不同的、专门的和优化的函数。为了允许随机访问,存储了一个指向所有单元容器的小指针向量,在每个容器内都有可用的随机访问迭代器(这应该至少与第二种方法一样快,但是在这里你在每个容器内的内存跳跃是恒定的,因为元素相同类型的按顺序排列)。如果您需要全局排序来表示空间填充曲线,我看不出如何实现它来迭代 elements[order][id] 应该比 elements[id] 更困难,但我还没有尝试过。

主要优点是编译器对正在发生的事情了解很多并且可以安全地优化(没有虚拟调度,没有别名,没有循环中断,数字内核中的类型和元素顺序都是编译时常量。 ..)。作为一个很好的副作用,使用这种方法混合 MPI 代码就像加载 std 并行库 (*) 并使用 OpenMP 编译一样简单,不需要额外的代码。对于边界元素,通常必须执行其他操作并存储不同的数据。我使用相同的方法分别存储它们并为它们定义专门的内核。

主要缺点是您需要非常小心地执行内存分配。问题是,事先你唯一知道的是有多少内存可用,但你不知道你需要多少个元素。特别是,假设您为某种类型的单元分配了一定数量的内存,并且您想要超过它。这将要求您至少有足够的空闲内存来复制整个单元格向量加上一个额外的单元格。它还涉及将所有单元格从该向量复制到新向量,但这很快。这并不聪明,您可能想为多个额外的单元分配内存,但要分配多少?您可能不想为两倍的单元分配内存。如果你想以一种聪明的方式来做这件事,你真的需要你自己的分配器,它知道你的数据结构、你的系统和正在进行的模拟。

优点

  • 需要在所有单元(数字内核)上执行的操作非常快。
  • 随机访问是可用的,并且与“一个单元数据向量/一个单元点向量”方法一样快。

缺点

  • 必须非常小心地执行内存分配(但情况并非总是如此?)。

* 使用 TBB 需要用 parallel_for_each 替换你的 for_each 循环,或者你可以只实现你自己的 for_each ,它采用并行化策略(或宏它:请不要这样做)。

还要考虑一次计算通量并更新左右单元格的面循环(矩阵中的贡献 LL、LR、RL 和 RR)。