Hexagon_V65_Programmers_Reference_Manual(8)
- 7. 程序流程
-
- 7.1 条件指令
- 7.2 硬件循环
-
- 7.2.1 循环设置
- 7.2.2 循环结束
- 7.2.3 循环执行
- 7.2.4 流水线硬件循环
- 7.2.5 循环限制
7. 程序流程
Hexagon 支持以下程序流程工具的处理器:
- 条件指令
- 硬件循环
- 软件分支
- 暂停
- 异常 软件分支包括跳转、调用和返回。 支持几种类型的跳转:
- 条件跳转(Speculative jumps)
- 比较跳转(Compare jumps)
- 寄存器赋值跳转(Register transfer jumps)
- 双跳(Dual jumps)
7.1 条件指令
许多 Hexagon 可有条件执行处理器指令。 例如:
if (P0) R0 = memw(R2) // conditionally load word if P0 if (!P1) jump label // conditionally jump if not P1
下列指令可指定为有条件:
- 跳转和调用
- 许多加载和存储指令
- 逻辑指令(包括AND/OR/XOR)
- 移位半字
- 通过寄存器或短立即数 32 位加/减
- 符号和零扩展
- 32 位寄存器传输及 64 位组合字
- 立即赋值寄存器(Register transfer immediate)
- 释放帧并返回
7.2 硬件循环
Hexagon 处理器包含硬件循环指令,可执行零费用循环分支。 例如:
loop0(start,#3) // loop 3 times start: { R0 = mpyi(R0,R0) } :endloop0
提供两组硬件循环指令 loop0 和loop1–使硬件循环能够嵌套一层。 例如:
// Sum the rows of a 100x200 matrix. loop1(outer_start,#100) outer_start: R0 = #0 loop0(inner_start,#200) inner_start: R3 = memw(R1 #4) { R0 = add(R0,R3) }:endloop0 { memw(R2 #4) = R0 }:endloop1
硬件循环指令使用如下:
- 对于非嵌套循环,使用loop0。
- 对于嵌套循环,loop0 用于内循环,loop1 用于外循环。
注意 如果程序需要创建嵌套超过一层的循环,则最内层的两个循环可以实现为硬件循环,其余的外部循环可以实现为软件分支。
每个硬件循环都与一对特殊循环寄存器有关:
- 循环起始地址寄存器 SAn 将地址设置为循环中的第一个指令(通常用汇编语言表示为标签)。
- 循环计数寄存器 LCn 设置为 32 位无符号值,指定要执行的循环迭代次数。 当 PC 到达循环结束时,检查 LCn 确定循环是否应重复或退出。
硬件循环设置指令一次设置两个寄存器——通常不需要单独设置。 然而,由于循环寄存器完全指定了硬件循环状态,它们可以保存和恢复(处理器中断或程序员手动),一旦循环寄存器重新加载,暂停的硬件循环就可以正常恢复。保存值。 Hexagon 处理器为两个硬件循环提供了两组循环寄存器:
- SA0 和 LC0 被 loop0 使用
- SA1 和 LC1 被 loop1 使用
表 7-1 列出硬件循环指令。
语法 | 描述 |
---|---|
loopN(start, Rs) | 硬件循环具有寄存器循环计数。为硬件循环 N 设置寄存器 SAn 和 LCn: * SAn 指定的循环起始地址已分配。* LCn 被赋值为通用寄存器Rs 的值。注意 - 循环开始操作数被编码为 PC 相关立即数。 |
loopN(start, #count) | 硬件循环具有即时循环计数。为硬件循环 N 设置寄存器 SAn 和 LCn:SAn 指定的循环起始地址已分配。LCn 被赋予指定的立即数 (0-1023)。注意 - 循环开始操作数被编码为 PC 相关立即数。 |
:endloopN | 硬件循环结束指令。执行以下操作:if (LCn > 1) {PC = SAn; LCn = LCn-1} 注:该指令显示为附加到循环中的最后一个数据包的后缀。 它在最后一个数据包中编码。 |
SAn = Rs | 将循环起始地址设置为通用寄存器 Rs |
LCn = Rs | 将循环计数设置为通用寄存器 Rs |
注意 循环指令分配给指令类 CR。
7.2.1 循环设置
循环寄存器必须设置硬件循环 SAn 和 LCn 设置为正确的值。 以两种方式完成:
- loopN 指令
- 寄存器传输到 SAn 和 LCn
loopN 指令执行设置 SAn 和 LCn 所有的工作。 例如:
loop0(start,#3) // SA0=&start, LC0=3 start: { R0 = mpyi(R0,R0) } :endloop0
在这种情况下,硬件循环(由一个乘法指令组成)执行 3 次。 loop0 指示寄存器 SA0 将地址值设置为标签的开始,并将其设置为标签 LC0 设置为 3。
循环计数在 loopN 当循环计数被限制为立即数时 0-1023 的范围内。 若所需的循环计数超出此范围,则必须将其指定为寄存器值。 例如:
使用循环N:
R1 = #20000; loop0(start,R1) // LC0=20000, SA0=&start start: { R0 = mpyi(R0,R0) } :endloop0
使用寄存器赋值:
R1 = #20000 LC0 = R1 // LC0=20000 R1 = #start SA0 = R1 // SA0=&start start: { R0 = mpyi(R0,R0) } :endloop0
如果 loopN 指令远离其循环起始地址,用于指定起始地址 PC 相对偏移值可能超过指令起始地址操作数的最大范围。 如果发生这种情况,要么是 loopN 指令移近循环开始,或指定循环开始地址 32 位常量(第 10.9 节)。 例如: 使用 32 位常量:
R1 = #20000; loop0(##start,R1) // LC0=20000, SA0=&start ...
7.2.2 循环结束
循环结束指令指示硬件循环中的最后一个数据包。 它用汇编语言表示,在数据包后面添加符号":endloopN",其中 N 指定硬件循环(0 或 1)。 例如:
loop0(start,#3) start: { R0 = mpyi(R0,R0) } :endloop0 // last packet in loop
循环中的最后一个指令必须始终用汇编语言表示为数据包(使用花括号),即使它是数据包中的唯一指令。p
嵌套的硬件循环可以指定相同的指令作为内部和外部循环的结束。 例如:
// Sum the rows of a 100x200 matrix.
// Software pipeline the outer loop.
p0 = cmp.gt(R0,R0) // p0 = false
loop1(outer_start,#100)
outer_start:
{ if (p0) memw(R2++#4) = R0
p0 = cmp.eq(R0,R0) // p0 = true
R0 = #0
loop0(inner_start,#200) }
inner_start:
R3 = memw(R1++#4)
{ R0 = add(R0,R3) }:endloop0:endloop1
memw(R2++#4) = R0
虽然 endloopN 的行为类似于常规指令(通过实现循环测试和分支),但请注意它不会在任何指令槽中执行,并且不计为数据包中的指令。 因此,标记为循环结束的单个指令包最多可以执行六个操作:
- 四个常规指令(一个指令包的正常限制)
- endloop0 测试和分支
- endloop1 测试和分支
注意 endloopN 指令被编码在指令包中(第 10.6 节)。
7.2.3 循环执行
建立硬件循环后,无论指定的循环计数如何,循环体总是至少执行一次(因为直到循环中的最后一条指令才检查循环计数)。 因此,如果一个循环需要可选地执行零次,则必须在它之前有一个显式的条件分支。 例如:
loop0(start,R1)
P0 = cmp.eq(R1,#0)
if (P0) jump skip
start:
{ R0 = mpyi(R0,R0) } :endloop0
skip:
在此示例中,使用 R1 中的循环计数设置了一个硬件循环,但如果 R1 中的值为零,则软件分支将跳过循环体。
执行完硬件循环的循环结束指令后,Hexagon 处理器会检查相应循环计数寄存器中的值:
- 如果该值大于 1,则处理器递减循环计数寄存器并执行零循环分支到循环起始地址。
- 如果该值小于或等于 1,则处理器在紧跟循环结束指令之后的指令处恢复程序执行。
注意 因为嵌套的硬件循环可以共享相同的循环结束指令,处理器可以在一次操作中检查两个循环计数寄存器。
7.2.4 流水线硬件循环
软件流水线循环对于 Hexagon 处理器等 VLIW 架构很常见。 它们通过重叠多个循环迭代来提高循环中的代码性能。
软件流水线包含三个部分:
- 循环开始的序幕
- 内核(或稳态)部分
- 流水线耗尽的尾声 最好用一个简单的例子来说明这一点,如下所示。
int foo(int *A, int *result)
{
int i;
for (i=0;i<100;i++) {
result[i]= A[i]*A[i];
}
}
foo:
{ R3 = R1
loop0(.kernel,#98) // Decrease loop count by 2
}
R1 = memw(R0++#4) // 1st prologue stage
{ R1 = memw(R0++#4) // 2nd prologue stage
R2 = mpyi(R1,R1)
}
.falign
.kernel:
{ R1 = memw(R0++#4) // kernel
R2 = mpyi(R1,R1)
memw(R3++#4) = R2
}:endloop0
{ R2 = mpyi(R1,R1) // 1st epilogue stage
memw(R3++#4) = R2
}
memw(R3++#4) = R2 // 2nd epilogue stage
jumpr lr
上述代码中,流水线循环的内核部分并行执行循环的三个迭代:
- 迭代 N+2 的负载
- 迭代 N+1 的乘法
- 迭代 N 的存储
软件流水线的一个缺点是流水线循环的序言和结尾部分所需的额外代码。 为了解决这个问题,Hexagon 处理器提供了 spNloop0 指令,其中指令名称中的“N”表示 1-3 范围内的数字。 例如:
P3 = sp2loop0(start,#10) // Set up pipelined loop
spNloop0 是 loop0 指令的变体:它使用 SA0 和 LC0 建立一个正常的硬件循环,但还执行以下附加操作:
- 执行spNloop0指令时,将真值false赋给条件寄存器P3。
- 关联循环执行 N 次后,P3 自动设置为 true。
此功能(称为自动条件控制)使流水线循环的内核部分中的存储指令能够由 P3 有条件地执行,因此由于 spNloop0 控制 P3 的方式 - 在流水线预热期间不会执行。 这可以通过消除对序言代码的需要来减少许多软件流水线循环的代码大小。
spNloop0 不能用于从流水线循环中消除结尾代码; 但是,在某些情况下,可以通过使用编程技术来做到这一点。
通常,影响结尾代码删除的问题是加载安全性。 如果流水线循环的内核部分可以安全地访问其数组的末尾——要么是因为已知它是安全的,要么是因为数组已在末尾填充——那么结尾代码是不必要的。 但是,如果无法确保加载安全,则需要显式的结尾代码来排空软件管道。
软件流水线循环(使用 spNloop0)
int foo(int *A, int *result)
{
int i;
for (i=0;i<100;i++) {
result[i]= A[i]*A[i];
}
}
foo:
{ // load safety assumed
P3 = sp2loop0(.kernel,#102) // set up pipelined loop
R3 = R1
}
.falign
.kernel:
{ R1 = memw(R0++#4) // kernel
R2 = mpyi(R1,R1)
if (P3) memw(R3++#4) = R2
}:endloop0
jumpr lr
注意 spNloop0 用来控制 P3 设置的计数值存储在用户状态寄存器 USR.LPCFG 中。
7.2.5 循环限制
硬件循环有以下限制:
- loopN 或 spNloop0(第 7.2.4 节)中的循环设置数据包不能包含推测性间接跳转、新值比较跳转或 dealloc_return。
- 硬件循环中的最后一个数据包不能包含任何程序流指令(包括跳转或调用)。
- loop0 中的循环结束包不能包含任何改变SA0 或LC0 的指令。 同样,loop1 中的循环结束包不能包含任何改变 SA1 或 LC1 的指令。
- spNloop0中的循环结束包不能包含任何改变P3的指令。
注意 SA1 和 LC1 可以在 loop0 结束时更改,而 SA0 和 LC0 可以在 loop1 结束时更改。