在计算机科学和编译器设计领域,回圈嵌套优化(Loop Nest Optimization,简称LNO)是一种optimized技术,其目的是透过一组回圈转换来提升记忆体访问效率,实现并行化或其他回圈开销减少的效果。回圈嵌套,顾名思义,即一个回圈内包含另一个回圈。这项技术的经典应用包括降低记忆体访问延迟以及优化常见线性代数演算法中的缓存带宽需求。
回圈分块(Loop Tiling)是执行回圈嵌套优化的主要手段,它旨在将回圈的迭代空间划分为较小的区块,以确保在回圈中使用的数据保持在缓存中,直至再次使用。
这样的分区能够将一个大的数组划分为更小的区块,从而适应缓存的大小,提升缓存重用并消除对缓存大小的需求。举例来说,一个普通的回圈可以使用块大小 B 来进行阻断,这样可以显著提升效能。
我们来看矩阵-向量相乘的例子。在这个案例中,存在三个数组,每个数组包含100个元素。最初的实现并没有将数组分区。在应用回圈分块技术后,将按 2 x 2 的区块来执行。
原始的回圈迭代空间为 n x n,当 n 值过大而缓存大小又过小时,单次回圈迭代中访问的数组元素可能会跨越缓存行,造成缓存未命中(cache misses)。
这显示出,划分块的选择对性能的影响至关重要。然而,选择最佳块大小并不简单,因为这需要对土机器的缓存大小和回圈中访问的数组区域做出准确的估算。此外,回圈的嵌套顺序也对提升缓存效率有显著影响。
在多数大型数学运算过程中,矩阵相乘的效率直接影响整体计算性能。在进行操作 C = A × B 时,能否有效地重用内存中的数据成为了一大挑战。
记忆体系统的效能常有限制,例如标准 PC 记忆体系统通常每10-30次双精度乘加(multiply-add)操作只能持续一次8字节的记忆体操作。
这意味着加载到缓存中的数据必须重复使用很多次,这就引入了实现回圈优化中的另一个困难点。原始的程式码每次仅计算结果矩阵中的一个条目。若同时计算一小块条目,则可以重用每一个已加载的值,这样回圈内有四次加载与四次乘加的操作。
但什么时候选择的区块大小过大时,系统会因需要更多的暂存器而导致效能下降。这时,编译器可能需要对储存到堆栈的额外负载进行安排,从而降低执行效率。
在执行矩阵乘法等程式时,内存带宽往往成为瓶颈,这意味着更多的暂存器能帮助编译器和程序设计者减少对内存带宽的依赖。而这也是 RISC CPU 供应商设计更平行的变量,如 32 条浮点暂存器文件的原因之一。
这项优化并非没有成本,当内存系统未能与浮点运算单元保持速度一致时,可能会使得程式性能未能达到理想的巅峰。
此外,长期的浮点加法延迟或多加法器的机器可能需要更多的累加器同时运行。厂商非常关注如何透过调整块大小来匹配不同层级的缓存,最大化性能。
尽管通过以上的回圈嵌套优化可有效提高程序性能,但最终,设计缓存无关演算法便成为一种不依赖特定缓存大小的可行策略。这类演算法能有效利用可用的缓存,并能自动适应多层记忆体结构。
这意味着,通过正确地利用缓存空间,能够在不依赖于特定缓存大小的情况下,实现更高效的计算过程。
在数学计算日益增长的今天,回圈嵌套优化其实不只是一种技术,而是提升计算性能的关键。那么,你准备好进一步了解如何透过回圈嵌套优化提升自己程式的运行速度了吗?