ARM汇编中的浮点运算与Neon协处理器详解
1. 浮点基本算术运算
浮点运算单元(FPU)包含四种基本算术运算,还有一些扩展功能,如乘加运算。此外,还有像平方根这样的特殊函数,以及不少影响符号的变体函数。这些函数可以在H、S或D寄存器上操作,以下是部分指令示例:
| 指令 | 说明 |
| — | — |
|
FADD Hd, Hn, Hm
|
Hd = Hn + Hm
|
|
FADD Sd, Sn, Sm
|
Sd = Sn + Sm
|
|
FADD Dd, Dn, Dm
|
Dd = Dn + Dm
|
|
FSUB Dd, Dn, Dm
|
Dd = Dn - Dm
|
|
FMUL Dd, Dn, Dm
|
Dd = Dn * Dm
|
|
FDIV Dd, Dn, Dm
|
Dd = Dn / Dm
|
|
FMADD Dd, Dn, Dm, Da
|
Dd = Da + Dm * Dn
|
|
FMSUB Dd, Dn, Dm, Da
|
Dd = Da - Dm * Dn
|
|
FNEG Dd, Dn
|
Dd = - Dn
|
|
FABS Dd, Dn
|
Dd = Absolute Value( Dn )
|
|
FMAX Dd, Dn, Dm
|
Dd = Max( Dn, Dm )
|
|
FMIN Dd, Dn, Dm
|
Dd = Min( Dn, Dm )
|
|
FSQRT Dd, Dn
|
Dd = Square Root( Dn )
|
2. 计算两点间的距离
若有两点
(x1, y1)
和
(x2, y2)
,它们之间的距离公式为:
[d = \sqrt{(y2 - y1)^2 + (x2 - x1)^2}]
下面是一个用汇编语言实现计算两点间距离的函数示例:
// 计算两点间距离的函数
// 输入:
// X0 - 指向4个浮点数的指针,分别为x1, y1, x2, y2
// 输出:
// X0 - 距离(单精度浮点数)
.global distance
distance:
// 保存LR寄存器
STR LR, [SP, #-16]!
// 一次性加载4个数字
LDP S0, S1, [X0], #8
LDP S2, S3, [X0]
// 计算 s4 = x2 - x1
FSUB S4, S2, S0
// 计算 s5 = y2 - y1
FSUB S5, S3, S1
// 计算 s4 = S4 * S4 (x2 - X1)^2
FMUL S4, S4, S4
// 计算 s5 = s5 * s5 (Y2 - Y1)^2
FMUL S5, S5, S5
// 计算 S4 = S4 + S5
FADD S4, S4, S5
// 计算 sqrt(S4)
FSQRT S4, S4
// 将结果移动到X0以便返回
FMOV W0, S4
// 恢复保存的寄存器
LDR LR, [SP], #16
RET
主程序调用该函数三次,计算三组不同点之间的距离并打印结果:
// 主程序,测试距离函数
// W19 - 循环计数器
// X20 - 当前点集的地址
.global main
.equ N, 3 // 点的组数
main:
STP X19, X20, [SP, #-16]!
STR LR, [SP, #-16]!
LDR X20, =points // 指向当前点集的指针
MOV W19, #N // 循环迭代次数
loop:
MOV X0, X20 // 将指针移动到参数1 (X0)
BL distance // 调用距离函数
// 需要将单精度返回值转换为双精度,因为C的printf函数只能打印双精度数
FMOV S2, W0 // 移回FPU进行转换
FCVT D0, S2 // 单精度转换为双精度
FMOV X1, D0 // 将双精度结果返回X1
LDR X0, =prtstr // 加载打印字符串
BL printf // 打印距离
ADD X20, X20, #(4*4) // 每组4个点,每个点4字节
SUBS W19, W19, #1 // 递减循环计数器
B.NE loop // 如果还有点,继续循环
MOV X0, #0 // 返回代码
LDR LR, [SP], #16
LDP X19, X20, [SP], #16
RET
.data
points: .single 0.0, 0.0, 3.0, 4.0
.single 1.3, 5.4, 3.1, -1.5
.single 1.323e10, -1.2e-4, 34.55, 5454.234
prtstr: .asciz "Distance = %f\n"
Makefile用于编译程序:
distance: distance.s main.s
gcc -o distance distance.s main.s
运行程序后,会输出三组点之间的距离。需要注意的是,由于C的
printf
函数只能打印双精度数,所以在汇编中需要将单精度结果转换为双精度。转换步骤如下:
1. 使用
FMOV
将单精度结果移回FPU。
2. 使用
FCVT
指令将单精度转换为双精度。
3. 使用
FMOV
将双精度结果移回寄存器
X1
。
3. 浮点转换操作
FPU支持多种浮点转换操作,不仅包括单精度和双精度浮点数之间的转换,还支持浮点数与整数之间的转换,以及转换为定点小数。常用的转换指令如下:
| 指令 | 说明 |
| — | — |
|
FCVT Dd, Sm
| 单精度转双精度 |
|
FCVT Sd, Dm
| 双精度转单精度 |
|
FCVT Sd, Hm
| 半精度转单精度 |
|
FCVT Hd, Sm
| 单精度转半精度 |
|
SCVTF Dd, Xm
| 有符号整数转双精度浮点数 |
|
UCVTF Sd, Wm
| 无符号整数转单精度浮点数 |
|
FCVTAS Wd, Hn
| 有符号,四舍五入到最近的整数 |
|
FCVTAU Wd, Sn
| 无符号,四舍五入到最近的整数 |
|
FCVTMS Xd, Dn
| 有符号,向负无穷方向舍入 |
|
FCVTMU Xd, Dn
| 无符号,向负无穷方向舍入 |
|
FCVTPS Xd, Dn
| 有符号,向正无穷方向舍入 |
|
FCVTPU Xd, Dn
| 无符号,向正无穷方向舍入 |
|
FCVTZS Xd, Dn
| 有符号,向零方向舍入 |
|
FCVTZU Xd, Dn
| 无符号,向零方向舍入 |
4. 比较浮点数值
大多数浮点指令没有“S”版本,因此不会更新条件标志。主要用于更新条件标志的指令是
FCMP
,其形式如下:
| 指令 | 说明 |
| — | — |
|
FCMP Hd, Hm
| 比较两个半精度寄存器 |
|
FCMP Hd, #0.0
| 半精度寄存器与零比较 |
|
FCMP Sd, Sm
| 比较两个单精度寄存器 |
|
FCMP Sd, #0.0
| 单精度寄存器与零比较 |
|
FCMP Dd, Dm
| 比较两个双精度寄存器 |
|
FCMP Dd, #0.0
| 双精度寄存器与零比较 |
由于浮点运算存在舍入误差,直接比较两个浮点数是否相等是不可靠的。通常的做法是设定一个容差(epsilon),如果两个数的差值的绝对值小于容差,则认为它们相等。例如,定义
e = 0.000001
,若
abs(S1 - S2) < e
,则认为
S1
和
S2
相等。
以下是一个比较两个浮点数是否在容差范围内相等的函数示例:
// 比较两个浮点数是否在容差范围内相等的函数
// 输入:
// X0 - 指向3个浮点数的指针,分别为x1, x2, e
// 输出:
// X0 - 若相等返回1,否则返回0
.global fpcomp
fpcomp:
// 加载3个数字
LDP S0, S1, [X0], #8
LDR S2, [X0]
// 计算 s3 = x2 - x1
FSUB S3, S1, S0
FABS S3, S3
FCMP S3, S2
B.LE notequal
MOV X0, #1
B done
notequal:
MOV X0, #0
done:
RET
主程序调用该函数,先将100个0.01相加,然后分别直接比较和在容差范围内比较结果与1是否相等:
// 主程序,测试浮点比较函数
// W19 - 循环计数器
// X20 - 当前点集的地址
.global main
.equ N, 100 // 加法次数
main:
STP X19, X20, [SP, #-16]!
STR LR, [SP, #-16]!
// 加载0.01、累加和和真实和到FPU
MOV W19, #N // 循环迭代次数
LDR X0, =cent
LDP S0, S1, [X0], #8
LDR S2, [X0]
loop:
// 累加0.01到累加和
FADD S1, S1, S0
SUBS W19, W19, #1 // 递减循环计数器
B.NE loop // 如果还有加法,继续循环
// 比较累加和与真实和
FCMP S1, S2
// 打印是否相等
B.EQ equal
LDR X0, =notequalstr
BL printf
B next
equal:
LDR X0, =equalstr
BL printf
next:
// 加载指针到累加和、真实和和容差
LDR X0, =runsum
// 调用比较函数
BL fpcomp
// 比较返回码与1并打印是否相等
CMP X0, #1
B.EQ equal2
LDR X0, =notequalstr
BL printf
B done
equal2:
LDR X0, =equalstr
BL printf
done:
MOV X0, #0 // 返回代码
LDR LR, [SP], #16
LDP X19, X20, [SP], #16
RET
.data
cent: .single 0.01
runsum: .single 0.0
sum: .single 1.00
epsilon: .single 0.00001
equalstr: .asciz "equal\n"
notequalstr: .asciz "not equal\n"
Makefile用于编译程序:
fpcomp: fpcomp.s maincomp.s
gcc -o fpcomp fpcomp.s maincomp.s
运行程序后,会先输出直接比较的结果(通常不相等),然后输出在容差范围内比较的结果(可能相等)。这表明在进行浮点运算时,由于舍入误差的存在,需要谨慎处理比较操作。
5. Neon协处理器概述
Neon协处理器可以实现真正的并行计算,它与FPU有很多相似的功能,但能够同时执行多个操作。例如,一条指令可以同时完成四个32位浮点运算。Neon协处理器采用单指令多数据(SIMD)的并行处理方式,即一条指令可以并行处理多个数据项。
6. Neon寄存器介绍
Neon协处理器可以操作64位寄存器(如D寄存器)和128位寄存器(如V寄存器)。128位寄存器并不是用于执行128位算术运算,而是将其分割为多个较小的值。例如,一个128位寄存器可以容纳四个32位单精度浮点数。当两个这样的寄存器相乘时,四个32位数字会同时相乘,结果存储在另一个128位寄存器中。
Neon协处理器可以处理整数和浮点数,使用8位整数时可以获得最大的并行度,一次可以执行16个操作。不同寄存器类型可容纳的元素数量如下表所示:
| 寄存器类型 | 8位元素数量 | 16位元素数量 | 32位元素数量 |
| — | — | — | — |
| 64位 | 8 | 4 | 2 |
| 128位 | 16 | 8 | 4 |
7. Neon的“车道”概念
Neon协处理器使用“车道”(lanes)的概念进行计算。当选择数据类型时,处理器会将寄存器划分为相应数量的车道,每个车道对应一个数据元素。例如,使用32位整数和128位V寄存器时,寄存器会被划分为四个车道,每个车道对应一个整数。我们通过指定车道数量和数据大小来表示车道配置。常用的车道指定符及其大小如下表所示:
| 指定符 | 大小 |
| — | — |
| D | 64位 |
| S | 32位 |
| H | 16位 |
| B | 8位 |
8. Neon算术运算
Neon协处理器的加法指令有两种形式,分别用于整数加法和浮点加法:
| 指令 | 说明 |
| — | — |
|
ADD Vd.T, Vn.T, Vm.T
| 整数加法 |
|
FADD Vd.T, Vn.T, Vm.T
| 浮点加法 |
其中,
T
的取值如下:
- 对于
ADD
指令:可以是
8B
、
16B
、
4H
、
8H
、
2S
、
4S
或
2D
。
- 对于
FADD
指令:可以是
4H
、
8H
、
2S
、
4S
或
2D
。
需要注意的是,这里使用的指令与标量整数和浮点算术运算的指令相同,汇编器会根据V寄存器和
T
指定符生成Neon协处理器的代码。使用Neon的关键在于合理安排代码,使所有车道都能持续进行有效工作。
下面是一个简单的mermaid流程图,展示了使用Neon协处理器进行32位整数加法的过程:
graph TD;
A[加载Vn和Vm寄存器] --> B[执行ADD指令];
B --> C[结果存储在Vd寄存器];
综上所述,Neon协处理器通过并行计算可以显著提高浮点运算和整数运算的效率。在实际应用中,需要根据具体需求合理安排数据和指令,充分发挥Neon的并行优势。同时,在进行浮点运算时,要注意舍入误差对结果的影响。
9. 实际应用示例及注意事项
在实际编程中,使用Neon协处理器和浮点运算时,有一些关键的注意事项和应用示例值得探讨。
9.1 数据安排与并行性
使用Neon协处理器的核心在于合理安排数据,以充分利用其并行计算能力。例如,在进行矩阵乘法时,如果矩阵元素是32位单精度浮点数,可以将数据按每四个一组的方式加载到128位V寄存器中,这样就能在一次指令中同时处理四个元素的乘法。以下是一个简单的伪代码示例,展示如何安排数据进行矩阵乘法:
// 假设矩阵A和B都是32位单精度浮点数矩阵
// 矩阵A的维度为m x n,矩阵B的维度为n x p
for (int i = 0; i < m; i++) {
for (int j = 0; j < p; j += 4) {
VRegister result = 0; // 初始化结果V寄存器
for (int k = 0; k < n; k++) {
// 加载矩阵A和B的对应元素到V寄存器
VRegister a = load_A(i, k);
VRegister b = load_B(k, j);
// 执行乘法并累加结果
result = FADD(result, FMUL(a, b));
}
// 存储结果
store_result(i, j, result);
}
}
这个示例展示了如何通过循环和寄存器加载,将数据合理安排到V寄存器中,以实现并行的矩阵乘法。
9.2 浮点运算的精度问题
在使用浮点运算时,舍入误差是一个需要特别关注的问题。如前面提到的,简单的加法运算也可能引入舍入误差。例如,在累加多个小数时,误差会逐渐累积,导致最终结果与预期不符。为了减少误差的影响,可以采用以下策略:
-
使用更高精度的数据类型
:如果可能,尽量使用双精度浮点数进行计算,因为双精度浮点数的表示范围更广,精度更高。
-
合理安排计算顺序
:在进行多个数的累加时,可以先对较小的数进行累加,再将结果与较大的数相加,这样可以减少大数对小数的舍入影响。
9.3 调试与监控
在调试使用浮点运算和Neon协处理器的程序时,需要使用特定的工具和指令来监控寄存器的内容。例如,在使用GDB调试时,可以使用“info all - registers”命令来查看FPU寄存器的内容。这对于定位和解决浮点运算中的问题非常有帮助。
10. 总结
通过对浮点运算和Neon协处理器的学习,我们掌握了以下重要知识:
-
浮点运算基础
:了解了浮点基本算术运算,包括加、减、乘、除等操作,以及如何在不同精度的寄存器(H、S、D)上进行运算。
-
浮点转换与比较
:掌握了浮点转换指令,如单精度与双精度之间的转换、浮点数与整数之间的转换,以及如何处理浮点比较中的舍入误差问题。
-
Neon协处理器
:熟悉了Neon协处理器的并行计算能力,包括其寄存器结构、“车道”概念以及如何使用并行加法指令进行整数和浮点运算。
下面是一个总结性的表格,对比了浮点运算和Neon协处理器的特点:
| 特性 | 浮点运算(FPU) | Neon协处理器 |
| — | — | — |
| 并行性 | 一次处理一个数据 | 一次可处理多个数据(SIMD) |
| 寄存器类型 | H、S、D(64位为主) | 64位D和128位V |
| 运算类型 | 基本算术、转换、比较 | 整数和浮点并行运算 |
| 应用场景 | 普通浮点计算 | 大规模数据并行处理,如矩阵运算 |
11. 未来展望
随着计算机技术的不断发展,浮点运算和并行计算的需求也在不断增加。Neon协处理器作为一种高效的并行计算工具,有望在更多领域得到应用,如人工智能、图像处理、科学计算等。未来,可能会出现更强大的协处理器架构,支持更高的并行度和更复杂的运算。同时,对于浮点运算的精度和性能优化也将是研究的重点方向,以满足日益增长的计算需求。
为了更好地理解未来的发展趋势,下面是一个mermaid流程图,展示了从当前技术到未来发展的可能路径:
graph LR;
A[当前浮点运算和Neon技术] --> B[更高并行度架构];
A --> C[更精确的浮点表示];
B --> D[人工智能应用];
B --> E[高性能计算];
C --> F[科学计算优化];
C --> G[金融计算安全];
在未来的编程实践中,我们需要不断学习和掌握新的技术,以充分发挥这些计算资源的潜力,为各个领域的发展提供更强大的支持。
超级会员免费看
626

被折叠的 条评论
为什么被折叠?



