开篇:一场悄无声息的换代
2013 年,iPhone 5s 发布。
发布会上苹果说了一句当时没什么人在意的话:
"A7 是世界第一款 64 位手机芯片。"当时的普遍反应是:
"手机内存才 1GB,要 64 位干什么?" "32 位能寻址 4GB,还远没用满啊。"十年后回头看:
★ 苹果全线产品从 x86 换成了自研 ARM ★ 安卓强制要求 64 位应用 ★ 亚马逊、阿里、华为都在做 ARM 服务器芯片 ★ Windows 出了 ARM 版而那个"64 位",从来就不只是"能用更多内存"。
这篇文章讲清楚:ARM64 到底改了什么,为什么这些改动如此重要。
第一部分:先理清名字
一、一堆容易混的术语
这是入门第一个坑:同一件事有好几个名字。
| 名字 | 指什么 |
|---|---|
| ARMv8-A | ⭐架构版本(2011年发布的规范) |
| AArch64 | ⭐64 位执行状态(ARM 官方叫法) |
| AArch32 | 32 位执行状态(兼容老代码) |
| arm64 | ⭐ 苹果/LLVM 的叫法 |
| aarch64 | ⭐ Linux/GCC 的叫法 |
| ARM64 | 微软的叫法 |
💡
arm64=aarch64=ARM64,★ 都是同一个东西。只是不同厂商起的名字不同。
二、⭐ 关键概念:执行状态
ARMv8 处理器可以在两种"状态"下跑:
┌────────────────────────────────────────┐ │ ARMv8 处理器 │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ AArch64 │ │ AArch32 │ │ │ │ ★ 64位状态 │ │ 兼容32位 │ │ │ │ │ │ │ │ │ │ A64 指令集 │ │ A32 / T32 │ │ │ └──────────────┘ └──────────────┘ │ │ ↑ ↑ │ │ └── ★ 可以切换 ────┘ │ └────────────────────────────────────────┘⭐ 最重要的一点:A64 是全新指令集
❌ 常见误解: "ARM64 就是 ARM32 加了个 64 位模式" ✅ 实际情况: ★ A64 是【重新设计】的指令集 ★ 和 A32 的编码格式完全不同 ★ 不是扩展,是另起一套🔥对比一下 x86 的做法:
x86-64(AMD64):★ 在 x86 上打补丁扩展 → 保留了所有历史包袱 ARM64(A64): ★ 借 64 位的机会推倒重来 → 扔掉了 ARM32 的历史包袱这是 ARM64 能做得这么干净的根本原因。
第二部分:最直观的变化
三、寄存器:从 16 个到 31 个
ARM32 的寄存器
R0 ~ R15 ★ 只有 16 个,而且: R13 = SP(栈指针) R14 = LR(返回地址) R15 = PC(程序计数器) ↓ ★ 真正能自由用的只有 13 个ARM64 的寄存器
X0 ~ X30 ★ 31 个 64 位通用寄存器 (W0 ~ W30 是它们的低 32 位) SP ★ 独立的栈指针(不再占用通用寄存器) PC ★ 独立的程序计数器(★ 不能直接读写了) XZR / WZR ★ 零寄存器(读出来永远是 0)⭐ 为什么寄存器多这么重要
寄存器少 → 变量放不下 → ★ 频繁存取内存 ↓ 访问内存比访问寄存器慢几十倍 ↓ ★ 性能损失举个例子:
// 一个函数用了 20 个局部变量【ARM32】只有13个可用寄存器 ↓ ★ 有7个变量必须放栈上(内存) ★ 每次用都要 load/store 【ARM64】有31个 ↓ ★20个变量全部放寄存器 ★ 整个函数几乎不碰内存⭐这是 ARM64 性能提升的一个主要来源。
★ 经验数据:仅寄存器数量翻倍这一项,就能带来 5~15% 的性能提升。
寄存器命名规则
X0 ★ 64 位访问 W0 ★ 32 位访问(就是 X0 的低 32 位) ★ 写 W0 时,X0 的高 32 位会被自动清零mov x0, #0xFFFFFFFFFFFFFFFF // x0 = 全 1 mov w0, #1 // ★ x0 变成 0x0000000000000001 // 高 32 位被清零,不是保留💡这个设计避免了"部分寄存器写入"带来的依赖问题。
★ x86 上mov al, 1会保留 eax 的高位,造成隐藏依赖,是性能陷阱。
四、⭐ 指令集的简化
ARM32 的两个"特色"(其实是包袱)
包袱① 条件执行
ARM32 里几乎每条指令都能带条件:
; ARM32 CMP r0, #0 ADDEQ r1, r1, #1 ; ★ 相等才执行 SUBNE r1, r1, #1 ; ★ 不相等才执行 MOVGT r2, #5 ; ★ 大于才执行看起来很酷——不用跳转就能做分支。
但代价是:
🔴 每条指令都要花 4 位编码条件 → ★ 浪费了大量指令编码空间 🔴 乱序执行的处理器很难优化 → ★ 每条指令都可能"不执行",依赖关系复杂 🔴 现代分支预测已经很准 → ★ 条件执行的优势不再明显ARM64 的处理:
★ 砍掉了绝大部分条件执行 ★ 只保留少数几条:CSEL、CSINC、CSET 等 csel x0, x1, x2, eq ; ★ eq 成立取 x1,否则取 x2包袱② PC 可以直接操作
ARM32 里 PC 就是 R15,可以当普通寄存器用:
; ARM32 MOV pc, r0 ; ★ 直接跳转到 r0 的地址 ADD pc, pc, r1 ; ★ 甚至能算出跳转目标 LDR r0, [pc, #8] ; ★ 相对 PC 取数据问题:
🔴 任何写 R15 的指令都可能是跳转 → ★ 处理器无法提前判断控制流 → ★ 流水线和分支预测极难做ARM64 的处理:
★ PC 不再是通用寄存器 ★ 只能通过专门的跳转指令改变 ★ 读 PC 也要用专门指令(adr / adrp)⭐这两项简化的共同目标:
★ 让处理器更容易分析指令、更容易做乱序执行和流水线优化。
代价是指令数变多了一点,但换来了大得多的性能空间。
其他简化
| ARM32 | ARM64 |
|---|---|
| ⭐LDM/STM(一条指令存取多个寄存器) | ⭐ 只保留LDP/STP(存取一对) |
| Thumb / Thumb-2 混合(16位+32位指令) | ⭐固定 32 位指令长度 |
| 移位可以内嵌在很多指令里 | 保留但简化 |
💡为什么砍掉 LDM/STM?
一条
LDM r0, {r1-r12}要访问 12 次内存。
★ 这种"微码级"的复杂指令,在乱序处理器上很难高效实现。
第三部分:地址空间
五、64 位地址空间有多大
32 位: 2³² = 4 GB 64 位: 2⁶⁴ = ★ 16 EB(约 170 亿 GB)但实际上没人用满 64 位:
★ 目前主流实现只用 48 位 → 256 TB ★ 部分支持 52 位 / 57 位为什么不用满?
① 用不完——256TB 对当前完全够用 ② ★ 页表层级越多,地址翻译越慢 ③ ★ 高位空出来,可以拿来放别的信息(见下文)六、⭐ 高位空着的妙用
妙用① 用户/内核空间分离
0x0000_0000_0000_0000 ┐ ├─ ★ 用户空间(低位全 0) 0x0000_FFFF_FFFF_FFFF ┘ ... 巨大的空洞 ... 0xFFFF_0000_0000_0000 ┐ ├─ ★ 内核空间(高位全 1) 0xFFFF_FFFF_FFFF_FFFF ┘★ 看地址最高位就知道是用户还是内核 ★ 硬件用两个独立的页表基址寄存器(TTBR0/TTBR1) ★ 用户态和内核态切换不用换页表妙用② ⭐ 指针标记(Top Byte Ignore)
ARM64 有个特性叫 TBI:★ 地址的最高 8 位在访存时被硬件忽略。
0x AB 00_7F_12_34_56_78 ~~ ★ 这 8 位随便填,硬件访存时当它不存在用途:
✅ 在指针里塞额外信息(类型标签、引用计数) ✅ ★ iOS 的 Objective-C "tagged pointer" ✅ ★ 内存调试工具(HWASan)用它做标记 ✅ JIT 编译器的对象头优化💡这是 64 位带来的一个隐藏好处:
★ 地址位太多了,多出来的位可以当"免费的元数据空间"用。
32 位时代 4GB 地址全都要用上,根本没有余量做这种事。
妙用③ ⭐ PAC(指针认证)
ARMv8.3 引入的安全特性:
函数返回地址存进栈之前: ↓ ★ 用密钥算一个签名,塞进地址的高位 ↓ 0x AB CD _ 00007F1234 ~~~~~ ★ 签名 ↓ 返回时校验签名 ↓ ★ 如果攻击者改了返回地址,签名对不上 → 崩溃🎯这直接打击了 ROP(返回导向编程)攻击。
★ 苹果 A12 之后的芯片全部启用了 PAC。
第四部分:其他重要改进
七、异常级别(Exception Level)
ARM32 的特权模式
ARM32 有 7 种处理器模式,混乱且不成体系 (User、FIQ、IRQ、Supervisor、Abort、Undefined、System)⭐ ARM64 重新设计成四层
┌──────────────────────────────────────┐ │ EL0 ★ 用户程序 │ 最低权限 ├──────────────────────────────────────┤ │ EL1 ★ 操作系统内核 │ ├──────────────────────────────────────┤ │ EL2 ★ 虚拟机监控器(Hypervisor) │ ├──────────────────────────────────────┤ │ EL3 ★ 安全监控器(TrustZone) │ 最高权限 └──────────────────────────────────────┘每一层都有独立的:
★ 栈指针(SP_EL0 ~ SP_EL3) ★ 异常返回地址(ELR_ELx) ★ 系统控制寄存器(SCTLR_ELx)为什么这个设计重要
① ★ EL2 专门给虚拟化 → 手机上跑虚拟机、云服务器都需要 → ARM32 时代虚拟化要靠软件技巧,性能差 ② ★ EL3 专门给安全世界 → 指纹、支付、DRM 都跑在这里 → 即使内核被攻破也访问不到 ③ ★ 层级清晰,权限边界明确💡这是 ARM 能进服务器市场的关键之一。
★ 没有硬件虚拟化支持,云计算根本没法做。
八、⭐ 内存模型:弱序
什么是内存序
多核 CPU 上,一个核写的内容,另一个核什么时候能看到?
【x86:强序(TSO)】 ★ 基本保证"先写的先被看到" ★ 硬件帮你维护顺序 ★ 代价:核间通信开销大,难扩展到很多核 【ARM64:弱序】 ★ 不保证顺序! ★ 需要程序员显式加内存屏障 ★ 好处:硬件简单、功耗低、易扩展一个会出问题的例子
// 线程 A // 线程 Bdata=42;if(flag==1)flag=1;print(data);★ 在 x86 上:基本不会有问题 ★ 在 ARM64 上:★ 线程 B 可能看到 flag=1,但 data 还是旧值!原因:
ARM64 允许处理器/编译器重排这两条写操作 ↓ ★ flag = 1 可能先写出去 ↓ 另一个核看到 flag=1 时,data 还没写完正确写法
// ✅ 用内存屏障data=42;__atomic_store_n(&flag,1,__ATOMIC_RELEASE);// ★ release 语义// 线程 Bif(__atomic_load_n(&flag,__ATOMIC_ACQUIRE))// ★ acquire 语义print(data);// ★ 保证能看到 data=42ARM64 的屏障指令
| 指令 | 作用 |
|---|---|
| DMB | 数据内存屏障(保证访存顺序) |
| DSB | 数据同步屏障(更强,等待完成) |
| ISB | 指令同步屏障(刷新流水线) |
| ⭐LDAR / STLR | 带 acquire/release 语义的访存(更高效) |
🔥实战影响:
★ 从 x86 移植到 ARM64,最容易出的 bug 就是内存序问题。
★ 在 x86 上"碰巧能跑"的无锁代码,到 ARM64 上会随机出错。而且这种 bug 极难复现——可能一百万次才出一次。
九、SIMD:NEON
什么是 SIMD
SIMD = Single Instruction, Multiple Data ★ 一条指令处理多个数据【普通做法】 for (i = 0; i < 4; i++) c[i] = a[i] + b[i]; ★ 4 次加法 【SIMD】 ★ 一条指令同时算 4 个加法ARM64 的 NEON
★ 32 个 128 位向量寄存器(V0 ~ V31) 比 ARM32 的 NEON 多一倍 一个 128 位寄存器可以看作: 16 × 8位 整数 8 × 16位 整数 4 × 32位 整数或浮点 2 × 64位 整数或浮点例子:
; ★ 一条指令加 4 个 32 位浮点 fadd v0.4s, v1.4s, v2.4sARM64 对浮点的重要改进
【ARM32】 ★ 浮点是可选的(VFP 扩展) ★ 有些芯片没有,要用软件模拟 【ARM64】 ★ 浮点和 NEON 是【必选】的 ★ 所有 ARM64 处理器都有💡这意味着编译器可以放心生成浮点和 SIMD 代码,不用考虑兼容性。
更新的 SVE
ARMv8.2 引入 SVE(Scalable Vector Extension):
★ 向量长度可变(128 ~ 2048 位) ★ 同一份代码,在不同硬件上自动用不同向量宽度对比 x86 的做法: SSE(128) → AVX(256) → AVX-512(512) ★ 每次扩宽都要重写代码 SVE: ★ 写一次,硬件宽度变了自动适配主要用在服务器和 HPC,手机上还不常见。
十、原子操作
ARM32/早期 ARM64 的做法
★ LL/SC(Load-Linked / Store-Conditional) ldxr w0, [x1] ; 独占加载 add w0, w0, #1 stxr w2, w0, [x1] ; 条件存储 cbnz w2, retry ; ★ 失败就重试问题:
🔴 竞争激烈时不断重试 🔴 ★ 核数越多,冲突越多,性能越差⭐ ARMv8.1 的 LSE
加入了真正的原子指令:
ldadd w0, w1, [x2] ; ★ 原子加,一条指令搞定 swp w0, w1, [x2] ; ★ 原子交换 cas w0, w1, [x2] ; ★ 比较并交换🎯这对多核服务器至关重要。
★ 在 64 核、128 核的 ARM 服务器上,LSE 能带来数倍的锁性能提升。
第五部分:兼容性
十一、⭐ ARM64 怎么跑 32 位程序
AArch64 状态 ←──── ★ 只能在异常返回时切换 ────→ AArch32 状态规则:
✅ 64 位内核可以运行 32 位应用 ❌ ★ 32 位内核不能运行 64 位应用 ❌ ★ 同一个进程内不能混用 32/64 位代码 ✅ 不同进程可以一个 32 位一个 64 位十二、⭐ 32 位兼容正在消失
这是近年一个重要趋势:
| 事件 | 时间 |
|---|---|
| 苹果 iOS 11 彻底停止支持 32 位 App | 2017 |
| Google Play 要求必须提供 64 位版本 | 2019 |
| ⭐ARM Cortex-A710 之后的大核不再支持 AArch32 | 2021 |
| ⭐部分新芯片完全移除 32 位支持 | 2023+ |
为什么要移除?
① ★ 省晶体管、省功耗 → 一整套 32 位译码逻辑可以删掉 ② ★ 简化验证 → 少一半的状态组合要测试 ③ 生态已经完成迁移🔥实际影响:
★ 如果你的应用还有 32 位的 so 库,在新手机上可能根本跑不起来。
必须提供 arm64-v8a 版本。
第六部分:为什么 ARM64 赢了
十三、⭐ 三个层面的原因
① 架构层:干净
x86-64:★ 在 40 年的历史包袱上打补丁 - 变长指令(1~15 字节) - 复杂的寻址模式 - 段寄存器、实模式等远古遗留 ↓ ★ 译码器极其复杂,占大量晶体管和功耗 ARM64: ★ 借 64 位机会推倒重来 - 固定 32 位指令长度 - 简单的寻址模式 - 没有历史包袱 ↓ ★ 译码简单 → 可以做很宽的解码(8 发射以上)💡一个关键数据:
★ Apple M 系列能做到 8 宽解码,x86 目前普遍在 4~6 宽。
★ 原因就是变长指令让 x86 很难并行解码。
② 商业层:授权模式
【Intel/AMD】 ★ 卖芯片 → 你只能买现成的 【ARM】 ★ 卖授权 → 苹果、高通、华为、亚马逊都能自己设计核心 → ★ 针对自己的场景深度优化结果:
★ 苹果为 macOS/iOS 定制 → 能效比极高 ★ 亚马逊为云服务定制 Graviton → 性价比高 ★ 高通为手机定制 → 兼顾性能和功耗③ 场景层:从功耗敏感场景起家
ARM 起点:★ 嵌入式、手机(功耗是生死线) ↓ ★ 从一开始就把"每瓦性能"当第一目标 ↓ 十几年打磨 ↓ ★ 等数据中心开始关心电费时,ARM 已经准备好了对比:
x86 起点:★ 桌面、服务器(性能优先,功耗其次) ↓ ★ 往低功耗方向走(Atom)一直不成功🎯"从低功耗往高性能走"比"从高性能往低功耗走"容易。
这是 ARM 能上桌面和服务器,而 x86 进不了手机的根本原因。
第七部分:对开发者的实际影响
十四、⭐ 六个要注意的地方
① 内存序(最重要)
// ❌ 在 x86 能跑,在 ARM64 会随机出错volatileintflag=0;intdata;// 线程 Adata=42;flag=1;// 🔴 可能被重排到 data 之前// ✅ 用标准原子操作std::atomic<int>flag{0};data=42;flag.store(1,std::memory_order_release);🔴规则:★ 多线程共享数据,必须用
std::atomic或加锁。
不要依赖volatile——它不提供内存序保证。
② 数据对齐
ARM64 大部分访存支持非对齐 ↓ ★ 但性能可能变差 ★ 某些指令(如 LDP/STP、原子操作)★ 仍然要求对齐// ✅ 显式对齐structalignas(16)Data{...};③ 指针大小
// ❌ 假设指针是 4 字节inthandle=(int)ptr;// 🔴 64 位下截断// ✅intptr_thandle=(intptr_t)ptr;// 或用 uintptr_t / size_t④ 类型大小变化
| 类型 | 32位 ARM | ⭐ 64位 ARM(LP64) |
|---|---|---|
int | 4 | 4(没变) |
long | 4 | ⭐8 |
pointer | 4 | ⭐8 |
size_t | 4 | ⭐8 |
🔥
long从 4 变 8 是移植时最常见的坑。★ Windows 上
long仍然是 4(LLP64 模型),Linux/macOS 是 8(LP64)。
★ 想要固定宽度就用int32_t/int64_t。
⑤ 内联汇编 / SIMD 代码
★ x86 的 SSE/AVX 代码不能直接用 ★ 需要改写成 NEON // x86 __m128 a = _mm_add_ps(b, c); // ARM NEON float32x4_t a = vaddq_f32(b, c);跨平台方案:
✅ 用 SIMD 抽象库(如 xsimd、Highway) ✅ 或者写标准 C,让编译器自动向量化⑥ 移动端打包
必须提供: ★ arm64-v8a (主流,必须) ⚠️ armeabi-v7a (老设备,逐渐可省) ❌ x86 / x86_64 (只有模拟器需要)// Android ndk { abiFilters 'arm64-v8a' // ★ 新项目只出 64 位 }速查表
ARM64 vs ARM32 核心差异
| 项目 | ARM32 | ⭐ ARM64 |
|---|---|---|
| 指令集 | A32 / T32 | ⭐A64(全新设计) |
| 通用寄存器 | 16 个 | ⭐31 个 |
| 指令长度 | 32位 / 16位混合 | ⭐固定 32 位 |
| 条件执行 | 几乎所有指令 | ⭐基本取消 |
| PC 可直接写 | ✅ | ⭐❌ |
| 地址空间 | 4 GB | ⭐48 位起(256TB) |
| 特权模式 | 7 种混乱模式 | ⭐EL0~EL3 四层 |
| 浮点/SIMD | 可选 | ⭐必选 |
| 向量寄存器 | 16×128位 | ⭐32×128位 |
| 硬件虚拟化 | 弱 | ⭐EL2 专门支持 |
版本特性速查
| 版本 | 关键特性 |
|---|---|
| ARMv8.0 | ⭐ AArch64 / A64 指令集 |
| ARMv8.1 | ⭐LSE 原子指令 |
| ARMv8.2 | ⭐ SVE、半精度浮点 |
| ARMv8.3 | ⭐PAC 指针认证 |
| ARMv8.4 | 内存分区、增强虚拟化 |
| ARMv8.5 | ⭐ MTE 内存标记(抓内存 bug) |
| ARMv9 | ⭐ SVE2、CCA 机密计算 |
移植 Checklist
- ⭐多线程代码用
std::atomic,不要靠volatile - ⭐ 不要假设指针是 4 字节
- ⭐ 注意
long在不同平台的大小 - 用
int32_t/int64_t代替int/long - 检查结构体对齐和 padding
- ⭐ SIMD 代码要改写(SSE → NEON)
- 检查内联汇编
- ⭐ 打包时必须包含 arm64-v8a
- ⭐ 在真机上跑多线程压力测试(内存序问题只在真机暴露)
收尾
回到 2013 年那个问题:“手机内存才 1GB,要 64 位干什么?”
现在答案清楚了——64 位从来不只是为了内存:
★ 寄存器从 16 个变 31 个 → 性能 ★ 扔掉条件执行和可写 PC → 流水线和乱序更好做 ★ EL0~EL3 四层特权 → 虚拟化和安全 ★ 浮点/SIMD 变成必选 → 编译器敢优化 ★ 地址高位空出来 → 指针认证、内存标记 ★ 固定 32 位指令 → 可以做 8 宽解码苹果当年真正做的,不是"支持更多内存",
而是★ 借 64 位这个契机,把整个指令集重新设计了一遍**。**
⭐三句话总结:
一、ARM64 不是 ARM32 的扩展,是重新设计的新指令集。
★ 这是它能保持干净、进而在能效比上超越 x86 的根本原因。二、"64 位"最大的收益不是地址空间,是寄存器数量和架构简化。
★ 地址空间只是顺带的,而且还没用满一半。三、对开发者而言,最危险的差异是弱内存序。
★ x86 上"碰巧能跑"的多线程代码,到 ARM64 上会随机崩溃。
最后一句:
x86 用了 40 年往上加补丁,ARM 用一次机会重新出发。
★ 有时候最大的优势不是你做了什么,而是你没有背什么。