SSE 操作在 2D 数组上实现循环,其中每个输出取决于包含它的 3x3 正方形(生命游戏)
Posted
技术标签:
【中文标题】SSE 操作在 2D 数组上实现循环,其中每个输出取决于包含它的 3x3 正方形(生命游戏)【英文标题】:SSE operations to implement a loop over a 2D array where each output depends on the 3x3 square that contains it (Game of Life) 【发布时间】:2018-08-24 00:59:11 【问题描述】:我需要对这个 C 模块实现 SSE(向量运算),但无法获得足够的该技术信息,有什么线索或解决方案吗?
另外,如果您对此处的代码有任何提示,我正在倾听。
void evolution(void *u, int w, int h)
//check_args(c, v);
unsigned (*univ)[w] = u;
unsigned new[h][w];
int itis = args.ITERATIONS;
int actualIteration = 0;
while(itis>0)
int thisGenerationSeeds = 0;
for(int y=0;y<h;y++)
for(int x=0;x<w;x++)
int n = 0;
for(int y1=y-1;y1<=y+1;y1++)
for(int x1=x-1;x1<=x+1;x1++)
if(univ[(y1+h)%h][(x1+w)%w])
n++;
thisGenerationSeeds++;
if(univ[y][x]==1)
n--;
new[y][x] = (n==3 || (n==2 && univ[y][x]));
//thisGenerationSeeds++ = (n==3 || (n==2 && univ[y][x]));
itis--;
actualIteration++;
printf("\nIteration:_%d, \n Sec Living Seeds:_%d,\n Par Living Seeds:,\n Vec Living Seeds%d",actualIteration, thisGeneration
for(int y=0;y<h;y++)
for(int x=0;x<w;x++)
univ[y][x] = new[y][x];
【问题讨论】:
@PeterCordes 感谢您的回答!这是“生命游戏”代码上的“进化”功能。 @PeterCordes 你有什么信息来源可以让我学习语法吗? ***.com/tags/sse/info 与使用 Intel 的内部函数有一些很好的联系。 它不是作为 SIMD 简介编写的,更像是“这是我如何使用可用的内在函数”。请参阅github.com/lemire/SIMDgameoflife/blob/master/include/… 中的computecounts8vec()
。
@PeterCordes 有了这个我可以做点什么,感谢你的努力:) 谢谢
【参考方案1】:
您的总体方法可能应该是计算新元素的整个 SIMD 向量(或并行计算多行的多个向量),而不是在继续之前尝试为一个元素做所有事情。如果您在局部变量(寄存器)中保留了足够的负载,您可能能够沿行就地工作。也许将 4 行中的 1 行复制到临时缓冲区。
您可能应该使用int8_t
元素来获得比unsigned
多4 倍的每个向量,特别是如果您可以使用SSSE3 _mm_shuffle_epi8
或_mm_alignr_epi8
,而不仅仅是SSE2 _mm_shuffle_epi32
或psrldq
(字节移位)或未对齐的加载以获得多个偏移量。
您只能处理从 -1 到 9 的整数,所以这就是 _mm_cmpgt_epi8 所需的全部内容。你可以使用 0 / -1 作为你的真/假状态,这样你就可以直接使用 SIMD 比较结果,而不用屏蔽来获得 0 / 1。(通过减法将它们相加,即_mm_sub_epi8
。或者只是添加然后否定比较的常量)。
每个元素实际上只有 1 个有效位,但能够将相同向量宽度内的 9 个元素相加是有优势的。 SIMD 比较只得到字节那么小。尽管如此,半字节可以有效地解包,并使您的数据密度加倍(将您的内存带宽减半)。
Dan Lemire 发布了一个使用 AVX2 内在函数 (blog post) 的 Life 工作实现。他在他未指定的机器上获得了 25 倍的速度提升与标量 C(当然启用了优化);我想我记得当我谈到他的 UTF-8 验证器时,他说他有一个 Haswell。
正如他所指出的,3x3 访问模式与许多图像处理卷积过滤器相同。看起来他不像你那样实现了圆形边界条件。您可以让您的 SIMD 循环开始/结束 1 远离边界,并通过环绕来对这些元素进行标量处理。
他的computecounts8vec()
https://github.com/lemire/SIMDgameoflife/blob/master/include/basicautomata.h#L94 使用未对齐负载来获取偏移向量。因此,x1+1
32 字节负载在下一次迭代中使x1-1
负载过载 2 个字节。对于 16 字节向量,使用一些 _mm_alignr_epi8
从两个对齐的负载创建这些移位的窗口可以节省未对齐的负载,这可能是一个好主意,但对于 32 字节向量 (__m256i
),通道内行为使其几乎无法用于最初的目的。
一次计算两三行将允许更多的数据重用(一个输出行的中间行是下一个输出行的顶部源行)。甚至重用低+ mid sum 作为下一行的 mid+hi 和。
也许对一到两行使用palignr
策略,对另外两行使用未对齐加载策略,在您一次完成的每个 3 或 4 行的条带中使用。
我们不会从 L1d 获得通常的 2-vectors-per-clock 负载带宽与未对齐的负载,因为跨越缓存线边界的速度变慢。它仍然只有 1 uop,但需要两次访问 L1d。因此,用 shuffle uop 替换一些负载 uop 在一定程度上是好的,尤其是在 AMD CPU 上,shuffle 吞吐量更高,并且只有在不需要额外的 shuffle 的情况下才值得 32 字节向量。 (与 Intel 不同,它们在 AMD 上被分成两个 16 字节的操作。)
我想知道我们是否可以使用具有填充或重复每个块的元素的低密度编码,以允许仅加载单个向量并移动它以获得偏移量。
或者可能是我们必须扩展的更密集的编码(如打包位),因此我们加载了额外的数据。
代码审查
在 C 中使用 new 作为变量名对同样了解 C++ 的程序员来说是非常不利的。我建议使用另一个名称,因此对于在 C++ 中实现 C99 VLA 的编译器,此代码也是有效的 C++。另外,让调用者传入源缓冲区和目标缓冲区,并使用unsigned *__restrict dst[h]
、unsigned *__restrict src[h]
,这样编译器就知道它们不重叠。然后,您可以在两个缓冲区之间交替,而不是每次都进行复制和复制 + 复制回。
特别处理包装边界条件,而不是将(x1+w)%w
放入最内层循环。这很可能会编译为硬件idiv
指令,因为编译器可能不知道它只能换行一次,而w
不是编译时常量。 (除非是在内联之后。)这可能是标量代码的主要瓶颈。
【讨论】:
我确实相信每单元位的打包方式,您将世界存储在(任何两倍大小的)字宽条带中,其中最不重要的半字节从最重要的半字节复制而来下一个条带产生出色的密度和计算效率:您所需要的只是二进制 AND、OR 和 NOT 运算、加法(没有半字节会溢出)和位移。不需要查找或条件。 “诀窍”是在四个单独的部分中计算每个结果字,对位进行去交错。以上是关于SSE 操作在 2D 数组上实现循环,其中每个输出取决于包含它的 3x3 正方形(生命游戏)的主要内容,如果未能解决你的问题,请参考以下文章
对于在数组中找到零并切换标志+更新另一个数组的循环的SSE优化
Swift 5:在使用协议实现 Equatable 的结构上实现通用数组操作