diff --git a/articles/20230617-software-prefetch.md b/articles/20230617-software-prefetch.md
new file mode 100644
index 0000000000000000000000000000000000000000..4611826099f031f33e59bfa630eb8dbe559d1c17
--- /dev/null
+++ b/articles/20230617-software-prefetch.md
@@ -0,0 +1,219 @@
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1 - [tounix spaces codeinline tables urls pangu autocorrect]
+> Author: Kepontry
+> Date: 2023/6/17
+> Revisor: Falcon ; Walimis
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [VisionFive 2 开发板软硬件评测及软件 gap 分析](https://gitee.com/tinylab/riscv-linux/issues/I64ESM)
+> Sponsor: PLCT Lab, ISCAS
+
+# 高速缓存的软件预取调优
+
+## 前言
+
+在 [上篇文章][002] 中介绍了缓存预取(Cache Prefetching),一种提前将指令或数据取到 Cache 中以提升性能的技术。根据预取数据的不同类型,缓存预取可以划分为指令预取和数据预取。而数据预取根据实现方式的不同,可以分为软件预取和硬件预取。上篇文章介绍了 RISC-V SoC JH7110 上的硬件预取,本篇文章将主要关注软件预取。
+
+软件预取是由编译器或编程人员向程序中插入预取指令而实现的,这类指令类似于 load 指令,会将缓存行大小的数据从内存中取出并放入 Cache 中,但并不会因为等待访存结果而阻塞流水线。对于收到的软件预取指令,CPU 将在访存通路空闲时发送预取请求。而硬件预取由模式匹配策略而不是指令触发,当某一模式匹配成功时,由 CPU 中的预取器自动进行预取。
+
+## X86 指令集的软件预取
+
+### 预取指令介绍
+
+下表中列举了 X86 指令集定义的软件预取指令,PREFETCHT0、PREFETCHT1 和 PREFETCHT2 指令可以指定预取到的 Cache 层级,而 PREFETCHNTA 指令针对的是短时间内不会被复用的数据,需要减少其对 Cache 的污染。需要注意的是,这些指令的实现与芯片架构相关,功能细节会有变化。此外,一些架构支持更多的预取指令,具体可以参考对应架构的手册。
+
+将数据预取到离 CPU 越近的 Cache 层级,例如 L1,获得的性能收益越大,但预取地址错误或及时性差所带来的性能损失也越大。所以 L1 软件预取需要准确率高且预取及时性好,如果及时性不能保证且程序是访存密集型应用,应尽量避免将数据预取到 L1。
+
+| 预取指令 | 作用 |
+|-------------|--------------------------------------------------|
+| PREFETCHT0 | 将数据预取到所有级别的高速缓存 |
+| PREFETCHT1 | 将数据预取到除 L1 外所有级别的高速缓存 |
+| PREFETCHT2 | 将数据预取到除 L1 和 L2 外所有级别的高速缓存 |
+| PREFETCHNTA | 将数据预取到非临时缓冲结构中,以减少对 Cache 的污染 |
+
+在 X86 架构下,可以使用 `_mm_prefetch` 函数在代码中插入软件预取。该函数第一个参数为待预取数据的指针,第二个参数可以配置为 `_MM_HINT_T0`, `_MM_HINT_T1`, `_MM_HINT_T2` 和 `_MM_HINT_NTA` 等,分别与上表中的预取指令一一对应。
+
+### 软件预取实验
+
+下面将以一个简单的链表遍历程序为例来说明软件预取的使用方式与效果。在该程序中,startCounter 和 getCounter 函数分别用于初始化定时器和获取计时值。createLinkedList、destroyLinkedList 和 traverseLinkedList 函数分别用于创建、销毁和遍历链表。链表的长度设置为 1M,分别统计加入软件预取前后的链表遍历耗时,比较性能差异。需要注意的是,结构体 Node 的末尾有一个 int 类型的数组,这是为了模拟结构体中的其它成员变量,设置为 14 是为了保证两个 Node 尽量位于不同的缓存行上。本次实验使用 `_mm_prefetch` 函数,预取链表的下一个元素所在的缓存行到 L2 和 L3 Cache 中。
+
+```C
+// linkedlist.c
+#include
+#include
+#include
+#include
+#include
+
+#define K 1000
+#define M 1000 * K
+
+struct timespec time1,time2;
+void startCounter() {
+ clock_gettime(CLOCK_REALTIME, &time1);
+}
+
+double getCounter() {
+ clock_gettime(CLOCK_REALTIME, &time2);
+ return (time2.tv_sec - time1.tv_sec) + \
+ (double)(time2.tv_nsec - time1.tv_nsec) / 1000000000;
+}
+
+typedef struct Node {
+ int data;
+ struct Node* next;
+ int arr[14];
+} Node;
+
+Node* createLinkedList(int n) {
+ Node* head = NULL;
+ Node* prev = NULL;
+
+ for (int i = 0; i < n; i++) {
+ Node* newNode = (Node*)malloc(sizeof(Node));
+ newNode->data = i;
+ newNode->next = NULL;
+ if (prev != NULL) {
+ prev->next = newNode;
+ } else {
+ head = newNode;
+ }
+ prev = newNode;
+ }
+ return head;
+}
+
+void destroyLinkedList(Node* head) {
+ Node* current = head;
+
+ while (current != NULL) {
+ Node* next = current->next;
+ free(current);
+ current = next;
+ }
+}
+
+void traverseLinkedList(Node* head, bool withPref) {
+ Node* current = head;
+ while (current != NULL) {
+ if(withPref)
+ _mm_prefetch(current->next, _MM_HINT_T1);
+ current = current->next;
+ }
+}
+
+int main() {
+ int n = 1 * M;
+ Node* head = createLinkedList(n);
+ printf("Linked list length: %d.\n", n);
+
+ startCounter();
+ traverseLinkedList(head, true);
+ double resultWithPref = getCounter();
+ printf("Time with prefetch: %f\n", resultWithPref);
+
+ startCounter();
+ traverseLinkedList(head, false);
+ double resultWithoutPref = getCounter();
+ printf("Time without prefetch: %f\n", resultWithoutPref);
+
+ destroyLinkedList(head);
+ return 0;
+}
+```
+
+编译上述代码后,使用 objdump 命令可以查看程序的汇编指令。从输出中可以看出,预取指令 prefetcht1 表示将数据预取到 L2 和 L3 Cache 中,与前面设置的 `_MM_HINT_T1` 参数对应。
+
+```shell
+$ gcc linkedlist.c -o linkedlist
+$ objdump -d linkedlist| grep prefetch
+ 1312: 0f 18 10 prefetcht1 (%rax)
+```
+
+运行编译后的程序,从结果中可以看出,添加软件预取后,链表的遍历耗时减少。
+
+```shell
+$ ./linkedlist
+Linked list length: 1000000.
+Time with prefetch: 0.007642
+Time without prefetch: 0.008526
+```
+
+### 软件预取优缺点分析
+
+软件预取的优点有:在源码级确定预取地址,能够处理更加复杂的访存相关;显式预取,编程人员可见等。
+
+当访存地址存在明显规律时,例如按一定步幅递增(数组遍历),访存部件能够较为容易地发现规律,现代处理器一般能够通过硬件预取自动进行优化。但当规律不明显时,例如下一次访存的地址是当前的访存值加上一定的偏移量(链表遍历),硬件预取出于实现开销考虑,往往不具备发现这类规律的能力,软件预取在源码层面能够非常方便地获得预取地址,如上面例子所示。
+
+软件预取的缺点为:不便于判断预取及时性,存在取指、译码等指令执行开销等。
+
+预取及时性指的是预取数据进入 Cache 的时机不能过早或过晚。由于源码级缺乏运行时信息,编程人员或编译器很难准确判断及时性,往往只能通过比较预取指令放在不同位置的性能收益来进行判断。
+
+## RISC-V 指令集的软件预取
+
+### CMO 扩展介绍
+
+RISC-V 指令集中的软件预取指令包含在缓存管理操作(Cache-management operation,CMO)指令中。该扩展指令集标准已被批准,包括了 Zicbom、Zicboz 和 Zicbop 扩展,最新版本为 [v1.0.1][004]。
+
+* Zicbom 扩展定义了 cbo.inval、cbo.clean 和 cbo.flush 等缓存块管理指令。其中,cbo.inval 指令用于无效缓存行,cbo.clean 指令用于清除缓存行的脏位,如果脏位置位,则将缓存行写回内存,cbo.flush 指令则先对缓存行做 flush 操作,再做 invalidate 操作。
+
+* Zicboz 扩展定义了 cbo.zero 指令,用于向缓存行中写 0。
+
+* Zicbop 扩展定义了 prefetch.i、prefetch.r 和 prefetch.w 指令,分别用于指令读、数据读和数据写的预取。
+
+由于该扩展指令集在 2021 年底才被批准,许多以前的 RISC-V CPU 并不支持该扩展。不过根据 [社区讨论][001],平头哥新推出的 C920 处理器核与 Intel 的 [Nios V 处理器核][007] 提供对 CMO 指令扩展的支持。
+
+### 指定预取 Cache 层级的方式
+
+RISC-V 在 Zihintntl 扩展指令集中提供了 NTL(Non-Temporal Locality)指令,表明目标指令(即下一条指令)的显式内存访问的时间局部性较差。这类指令是提供给处理器的提示,不影响体系结构状态,具体实现由微架构决定。微架构可以使根据 NTL 指令决定将数据分配到哪一级缓存。例如,在一种实现中,ntl.p1 实现为不在私有 L1 Cache 中为该数据分配缓存行,而应该在 L2 中分配。在另一个实现中,ntl.p1 实现为在 L1 中分配缓存行,但是会被尽快替换出去。
+
+如下表所示,该扩展共提供 4 条指令,分别定义了目标指令在共享和私有 Cache、最内层和所有级别 Cache 中的局部性。
+
+| 指令 | 作用 |
+|----------|---------------------------------------------------|
+| ntl.p1 | 目标指令在最内层私有 Cache 中没有表现出时间局部性 |
+| ntl.pall | 目标指令在任何级别的私有 Cache 中没有表现出时间局部性 |
+| ntl.s1 | 目标指令在最内层共享 Cache 中没有表现出时间局部性 |
+| ntl.all | 目标指令在任何级别的共享 Cache 中没有表现出时间局部性 |
+
+如下表所示,对于不同内存架构,NTL 指令所对应的 Cache 层级也不一样。以私有 L1/L2,共享 L3 为例,ntl.p1 对应 L1,ntl.pall 对应 L2,ntl.s1 与 ntl.all 均对应 L3。更详细的介绍可以参见 [Zihintntl 扩展指令集文档][010]。
+
+| 内存架构 | ntl.p1 | ntl.pall | ntl.s1 | ntl.all |
+|--------------|--------|----------|--------|---------|
+| 私有 L1,共享 L2 | L1 | L1 | L2 | L2 |
+| 私有 L1,共享 L2/L3 | L1 | L1 | L2 | L3 |
+| 私有 L1/L2,共享 L3 | L1 | L2 | L3 | L3 |
+
+NTL 指令可以影响除 Zicbom 扩展中的缓存管理指令之外的所有内存访问指令。例如在 “私有 L1/L2,共享 L3” 的内存架构中执行 cbo.zero 指令,如果前面执行过 ntl.pall 指令,则表示该缓存行应在 L3 中分配并清零,而不是在 L1 或 L2 中分配。因为根据上表,其在 L2 Cache 中的局部性差。
+
+### 使用方法
+
+LLVM 和 GCC 目前已经支持 CMO 扩展,以 Zicbop 扩展为例,相关 patch 如下。
+
+* [[RISCV] Add support for llvm.prefetch to use Zicbop instructions][012]
+* [[RISCV] Implement support for the Zicbop extension][011]
+* [RISC-V: Cache Management Operation instructions][008]
+* [RISC-V: Cache Management Operation instructions testcases][009]
+
+在 GCC 中,可以调用内置函数 `__builtin_prefetch (const void *addr[, rw[, locality]])` 进行预取,第一个参数 addr 为预取地址,第二个参数 rw 用 0 和 1 分别表示读和写,第三个参数 locality 用于指定局部性,从 0-3 局部性逐渐增加,与前面介绍的 NTL 指令思想类似。其中,第二、三个参数是可选的。
+
+## 总结
+
+本文简要介绍了软件预取的原理和优缺点,并分析了 X86 和 RISC-V 架构下的软件预取,此外还给出了 X86 架构下的程序优化案例,为大家实际编程提供参考。
+
+## 参考资料
+
+- [X86 架构预取内建函数][006]
+- [RISC-V 近期批准的扩展指令集][005]
+- [RISC-V CMO 扩展标准][003]
+
+[001]: https://forum.rvspace.org/t/milk-v-pioneer/2838
+[002]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20230509-vf2-hw-prefetch.md
+[003]: https://github.com/riscv/riscv-CMOs
+[004]: https://github.com/riscv/riscv-CMOs/blob/master/specifications/cmobase-v1.0.1.pdf
+[005]: https://wiki.riscv.org/display/HOME/Recently+Ratified+Extensions
+[006]: https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html#ig_expand=154,5184
+[007]: https://www.intel.com/content/www/us/en/docs/programmable/683632/23-1/data-cache.html
+[008]: https://gcc.gnu.org/git?p=gcc.git;a=commit;h=3df3ca9014f94fe4af07444fea19b4ab29ba8e73
+[009]: https://gcc.gnu.org/git?p=gcc.git;a=commit;h=d44e471cf041d5a304f2b2bbc7d104fa17f0e9da
+[010]: https://github.com/riscv/riscv-isa-manual/blob/main/src/zihintntl.adoc
+[011]: https://reviews.llvm.org/D117433
+[012]: https://reviews.llvm.org/D152723