前言
预取是在处理器实际需要之前,将指令或数据从较慢的内存中提取到较快的cache中,来最大程度地减少cache未命中的延迟。预取器通常能预测下一个使用的缓存行,将他们载入缓存,但是也容易出错导致缓存污染,并给内存子系统带来额外的压力。在高性能处理器中,处理高速缓存未命中或延迟以及正确管理内存带宽,预取是比较有用的方法。在分布式共享内存(DSM)系统中,远程内存访问比本地访问花费更长的时间,因此数据预取对于此类系统非常有效。
预取分为硬件预取和软件预取:基于硬件的预取通常是通过在处理器中具有专用的硬件机制来完成的,该机制监视正在执行的程序所请求的指令或数据流,基于该流识别程序可能需要的数据,然后预取到处理器的缓存中;基于软件的预取通常是通过让编译器分析代码并在编译期间在程序中插入预取指令来完成。
-
硬件预取:流缓冲区是使用中最常见的基于硬件的预取技术之一,该技术最初由 Norman Jouppi提出,它将cache miss的地址提取到单独的深度缓冲区中,与缓存分开。如果预取的块关联的地址与在处理器上执行的程序生成的请求地址匹配,则处理器从流缓冲区中读取数据/指令,如下图所示:
-
软件预取:编译器指导的预取被广泛用于具有大量迭代的循环中,这些预取是非阻塞的内存操作,即这些内存访问不会干扰实际的内存访问,它们不会更改处理器的状态或引起页面错误。软件预取的一个主要优点是,它减少了强制性高速缓存未命中的次数。
硬件预取有较少的CPU开销,且需要程序的实时运行信息以估计预取对象,不可人为控制,而软件预取更方便处理,性能也依赖编译器或源码。
以上硬件、软件预取介绍来自Wiki,更多关于预取的说明请参见:https://en.wikipedia.org/wiki/Cache_prefetching
GCC中的预取
GCC主要在tree-ssa上对循环数组进行预取优化操作,GCC8.2.0中预取相关的选项有:-fprefetch-loop-arrays、prefetch-latency、simultaneous-prefetches、min-insn-to-prefetch-ratio、prefetch-min-insn-to-mem-ratio
。其中-fprefetch-loop-arrays -faggressive-loop-optimizations
选项对benchmark性能有较大影响,尤其是619.lbm_s
。
- fprefetch-loop-arrays:如果目标计算机支持,则生成指令以预取内存,提高访问大型数组循环的性能。此选项可能会生成更好或更差的代码,很大程度上取决于源代码中循环的结构。这是主要的预取选项。
- prefetch-latency:估计预取完成之前执行的平均指令数。预先预取的距离与该常数成比例,增加此数目可能减少预取。
- simultaneous-prefetches:可以同时运行的最大预取数。
- min-insn-to-prefetch-ratio:允许在循环中进行预取的指令数量和预取数量之间的最小比。
- prefetch-min-insn-to-mem-ratio:允许在循环中进行预取的指令数与内存引用数之间的最小比。
GCC中数据的预取分为读预取和写预取。预取受空间局部性和时间局部性影响,属于局部性优化,预取的三个因素为:预取数量、预取距离和预取类型,有效的预取距离起着至关重要的作用。
gcc-8.2.0中预取相关的主要源码在tree-ssa-loop-prefetch.c
中,由pass_loop_prefetch
描述,该pass的源码定义如下:
namespace {
const pass_data pass_data_loop_prefetch =
{
GIMPLE_PASS, /* type */
"aprefetch", /* name */
OPTGROUP_LOOP, /* optinfo_flags */
TV_TREE_PREFETCH, /* tv_id */
( PROP_cfg | PROP_ssa ), /* properties_required */
0, /* properties_provided */
0, /* properties_destroyed */
0, /* todo_flags_start */
0, /* todo_flags_finish */
};
class pass_loop_prefetch : public gimple_opt_pass
{
public:
pass_loop_prefetch (gcc::context *ctxt)
: gimple_opt_pass (pass_data_loop_prefetch, ctxt)
{}
/* opt_pass methods: */
virtual bool gate (function *) { return flag_prefetch_loop_arrays > 0; }
virtual unsigned int execute (function *);
}; // class pass_loop_prefetch
unsigned int
pass_loop_prefetch::execute (function *fun)
{
if (number_of_loops (fun) <= 1)
return 0;
if ((PREFETCH_BLOCK & (PREFETCH_BLOCK - 1)) != 0)
{
static bool warned = false;
if (!warned)
{
warning (OPT_Wdisabled_optimization,
"%<l1-cache-size%> parameter is not a power of two %d",
PREFETCH_BLOCK);
warned = true;
}
return 0;
}
return tree_ssa_prefetch_arrays ();
}
} // anon namespace
gimple_opt_pass *
make_pass_loop_prefetch (gcc::context *ctxt)
{
return new pass_loop_prefetch (ctxt);
}
该pass在passes链表中的位置如下:
POP_INSERT_PASSES ()
NEXT_PASS (pass_parallelize_loops, false /* oacc_kernels_p */);
NEXT_PASS (pass_expand_omp_ssa);
NEXT_PASS (pass_ch_vect);
NEXT_PASS (pass_if_conversion);
/* pass_vectorize must immediately follow pass_if_conversion.
Please do not add any other passes in between. */
NEXT_PASS (pass_vectorize);
PUSH_INSERT_PASSES_WITHIN (pass_vectorize)
NEXT_PASS (pass_dce);
POP_INSERT_PASSES ()
NEXT_PASS (pass_predcom);
NEXT_PASS (pass_complete_unroll);
NEXT_PASS (pass_slp_vectorize);
NEXT_PASS (pass_loop_prefetch);
/* Run IVOPTs after the last pass that uses data-reference analysis
as that doesn't handle TARGET_MEM_REFs. */
NEXT_PASS (pass_iv_optimize);
NEXT_PASS (pass_lim);
NEXT_PASS (pass_tree_loop_done);
POP_INSERT_PASSES ()
此过程插入预取指令,以在循环访问数组时优化高速缓存的使用。
- 它按顺序处理循环并在单个循环中收集所有内存引用。此外使用数据依赖性分析来确定每个引用的距离,直到出现第一次重用为止,该信息用于确定发出的预取指令的时间。
- 对于每个引用,通过具有三种启发式的cost model来确定预取对于给定循环是否有利,并且计算两个值:
PREFETCH_BEFORE
(预取距离)和PREFETCH_MOD
(两次预取相距的迭代次数)。 - 预取多少是通过启发式猜测进行评估的,预取的迭代次数是获取时间/在循环的一次迭代中花费的时间。
- 判断预取对象之后,在同时预取的最大数量范围内预取尽可能多的进行预取(从具有最低
prefetch_mod
的预取开始)。 - 发射预取执行,为满足一定的
PREFETCH_MOD
和PREFETCH_BEFORE
要求,将对循环做unroll和peeling操作,操作完成对预取地址和数量等计算之后再发射预取指令。
ICC中的预取
ICC中预取相关的选项有:-qopt-prefetch[=n](-qno-opt-prefetch)、 -qopt-prefetch-distance=n1[, n2]、qopt-prefetch-issue-excl-hint
。
- qopt-prefetch[=n]:n为0表示禁用预取,与-qno-opt-prefetch相同,n为1-5表示不同的优化级别,使用较低的值可以减少预取的数量。
-qopt-prefetch
默认采用的值为2。 - qopt-prefetch-distance=n1[, n2]:指定在循环内用于编译器生成的预取的预取距离。该选项默认关闭,编译器采用启发式判断预取距离,其中可选参数n1,n2均为非负值,n1 = 0关闭所有从内存到L2的预取,n2 = 0关闭从L2到L1的预取,如果指定了n2并且n1 > 0,则n1应该 >= n2。即n1的值将用作从内存到L2的预取距离(例如vprefetch1指令)。如果指定了n2,它将用作从L2到L1的预取距离(例如,vprefetch0指令)。使用该选项需要开启
-qopt-prefetch
选项。使用示例如下:
-qopt-prefetch-distance = 64,32
:表示编译器对L2预取的内存使用64次迭代的距离,对L2至L1的预取使用32次迭代的距离。
-qopt-prefetch-distance = 24
:表示编译器将24次迭代的距离用于L2预取的内存。L2到L1预取的距离将由编译器确定。
-qopt-prefetch-distance = 0,4
:表示关闭了编译器在循环内插入的L2预取的所有内存。编译器将对L2至L1预取使用4次迭代的距离。
-qopt-prefetch-distance = 16,0
:表示编译器使用16次迭代的距离来存储L2预取的存储器。编译器不会发出L2到L1的循环预取。 - qopt-prefetch-issue-excl-hint:支持intel微体系结构Broadwell及更高版本中的prefetchW指令。该选项默认关闭,预取指令只是一个提示,不会影响程序行为。
相关选项的使用在不同机器平台有较大的差别,参数的选择也不是固定的,需要根据目标平台参数选出最佳选项组合。
AOCC/LLVM中的预取
AOCC2.1中预取相关的选项有:-enable-X86-prefetching,目前这还是一个实验性的选项,它启用x86预取指令的生成。
更多关于局部性优化相关的介绍可以参考编译优化之 - 结构数据布局优化入门
References:
- https://en.wikipedia.org/wiki/Cache_prefetching
- https://software.intel.com/en-us/cpp-compiler-developer-guide-and-reference-qopt-prefetch-qopt-prefetch