18、ARM汇编中的浮点运算与Neon协处理器详解

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[金融计算安全];

在未来的编程实践中,我们需要不断学习和掌握新的技术,以充分发挥这些计算资源的潜力,为各个领域的发展提供更强大的支持。

内容概要:本文详细介绍了“秒杀商城”微服务架构的设计实战全过程,涵盖系统从需求分析、服务拆分、技术选型到核心功能开发、分布式事务处理、容器化部署及监控链路追踪的完整流程。重点解决了高并发场景下的超卖问题,采用Redis预减库存、消息队列削峰、数据库乐观锁等手段保障数据一致性,并通过Nacos实现服务注册发现配置管理,利用Seata处理跨服务分布式事务,结合RabbitMQ实现异步下单,提升系统吞吐能力。同时,项目支持Docker Compose快速部署和Kubernetes生产级编排,集成Sleuth+Zipkin链路追踪Prometheus+Grafana监控体系,构建可观测性强的微服务系统。; 适合人群:具备Java基础和Spring Boot开发经验,熟悉微服务基本概念的中高级研发人员,尤其是希望深入理解高并发系统设计、分布式事务、服务治理等核心技术的开发者;适合工作2-5年、有志于转型微服务或提升架构能力的工程师; 使用场景及目标:①学习如何基于Spring Cloud Alibaba构建完整的微服务项目;②掌握秒杀场景下高并发、超卖控制、异步化、削峰填谷等关键技术方案;③实践分布式事务(Seata)、服务熔断降级、链路追踪、统一配置中心等企业级中间件的应用;④完成从本地开发到容器化部署的全流程落地; 阅读建议:建议按照文档提供的七个阶段循序渐进地动手实践,重点关注秒杀流程设计、服务间通信机制、分布式事务实现和系统性能优化部分,结合代码调试监控工具深入理解各组件协作原理,真正掌握高并发微服务系统的构建能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值