功能需求
虽然现在 开发的绝对主角是 Swift 语言,不过我们也希望有时 Swift 能够调用小段汇编代码以完成特殊功能。
在本篇博文中,您将学到如下内容:
- Swift 与 汇编语言混编的基本原理;
- 如何在模拟器中使用 Swift + x64 汇编指令?
- 如何在真机中使用 Swift + ARM64 汇编指令?
- Xcode 如何根据不同编译目标切换不同类型的汇编指令?
- 实例测试真机上汇编语言执行速度;
关于更多 Swift、C、 intel x64 和 ARM64 汇编语言执行效率比较,请移步如下链接观赏:
- 有趣的小实验:四种语言搞定“超超超难”剑桥面试数学题
- 搞定“超超超难”剑桥面试数学题番外篇:ARM64汇编
请系好安全带,本次航行我们将穿越量子微宇宙。
Let‘s go!!!😉
功能分析
1. Swift 与 汇编如何才能成为“好基友”?
大家都知道,在 C 或 C++ 等其它高级语言中我们都可以通过内嵌汇编语言(Inline assembly)的方式来与它方便的“打成一片”。
比如,在以下 C 代码中我们就以内嵌形式调用了汇编代码:
#include <stdio.h>
extern int func(void);
// the definition of func is written in assembly language
__asm__(".globl func\n\t"
".type func, @function\n\t"
"func:\n\t"
".cfi_startproc\n\t"
"movl $7, %eax\n\t"
"ret\n\t"
".cfi_endproc");
int main(void)
{
int n = func();
// gcc's extended inline assembly
__asm__ ("leal (%0,%0,4),%0"
: "=r" (n)
: "0" (n));
printf("7*5 = %d\n", n);
fflush(stdout); // flush is intentional
// standard inline assembly in C++
__asm__ ("movq $60, %rax\n\t" // the exit syscall number on Linux
"movq $2, %rdi\n\t" // this program returns 2
"syscall");
}
不过,可能是 考虑到安全和多平台移植等诸多烦心的事, Swift 语言本身是没有内嵌汇编语言机制的。
所以,为了 Swift 能与汇编 成为“好基友”,我们需要找到 Swift 代码识别并调用汇编的方法:这可以通过桥接 Objective-C(后面简称为:Objc) 代码来实现。
2. Objc:缘分一道桥
首先,用 Xcode 创建名为 ‘Asm_X64’ (iOS 类型)的项目,选择 Swift 作为项目主语言。
在 Swift 项目中桥接 Objc 代码很容易,我们只需在 Xcode 中新建一个 Objc 源代码文件,随后 Xcode 会自动为我们创建桥接头文件。
为了简单起见,我们可以直接选择创建 Cocoa Touch Class 类型,这会同时生成 .h 头文件和 .m 文件:
理论上,我们可以不需要 .m 文件,只需要一个 .h 文件即可,因为本例中 Swift 需要的所有代码都是由汇编而不是 Objc 来实现的。
不过,在项目中只创建一个 .h 文件无法让 Xcode 自动生成桥接头文件(Bridging Header)。
所以,如果大家不嫌手动创建桥接头文件麻烦的话,可以只创建 .h 文件。
我们打算用 Objc 类作为跳板来让 Swift 间接调用汇编代码。So,为我们的 Objc 类起名为 AsmHelper:
提示:我们后面会介绍 Swift 如何不通过 Objc 类作为跳板来调用汇编代码的方法。
紧接着,Xcode 会自动提示是否创建 Objc 的桥接头文件(Objective-C bridging header),这里当然是选择 创建 了:
在新创建的 AsmHelper 类中添加一个 calc_max 方法,现在该方法啥也不做只是默默的返回 0:
// AsmHelper.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AsmHelper : NSObject
-(int)calc_max;
@end
NS_ASSUME_NONNULL_END
// ****************************************************
// AsmHelper.m
#import "AsmHelper.h"
@implementation AsmHelper
- (int)calc_max {
return 0;
}
@end
还差一步:打开 AsmTest-Bridging-Header.h 桥接头文件,导入 AsmHelper.h。
// AsmTest-Bridging-Header.h
#import "AsmHelper.h"
准备工作已就绪,下面终于可以进入“正题”了!
3. 如何在模拟器中使用 Swift + x64 汇编指令?
根据我们 Mac 机器中不同的处理器,我们需要分别为 intel 和 Silicon (M1,M2)两种芯片来考虑开发设置。
3.1 intel 芯片的 Mac
如果你使用 intel 芯片的 Mac 来进行开发,那么模拟器实际是在 intel 处理器上运行的。所以我们需要使用 intel x64 类型的汇编指令。
在 Xcode 项目中新建一个 max_x64.s 文件用来存放汇编代码。为了方便 Objc 调用,我们在 max_x64.s 中写了一个函数:
.text
.globl _calc_max_x64_asm
.p2align 4, 0x90
// 方法进入点
_calc_max_x64_asm:
// 方法构造前缀
push %rbp
mov %rsp,%rbp
pushq %rbx
pushq %rdx
/*
实际代码将在此...
*/
// 方法析构后缀
popq %rdx
popq %rbx
popq %rbp
// 返回值放在 rax 寄存器中
mov $11,%rax
ret
需要说明一下:上面 .globl 伪指令的作用是将 _calc_max_x64_asm 作为全局标签来对待,这样我们就可以从外部(Objc中)找到它。
其实从汇编指令层面上来说,是没有所谓“函数或方法”概念的。只要满足以下 5 点要求,任何指令块都可以作为函数或方法来调用:
- 确定指令执行的起始地址(方法进入点);
- 设置栈;
- 处理寄存器的保存和恢复;
- 处理传入参数并设置返回值;
- 添加返回指令(ret);
以上这些都属于应用程序二进制接口编程(ABI)的范畴,不同语言和系统的实现都有所不同,我们需要按照对应的规范来编写代码。
对于上面的“函数”来说:它没有传入参数,并且有一个 int 类型的返回值。
关于更多汇编语言的知识,感谢兴趣的小伙伴们可以移步到我汇编(Asm)专栏中观赏相关文章:
- 大熊猫侯佩的 Asm 系列专栏
回到 AsmHelp.h 文件中,我们需要在 Objc 中声明汇编函数的签名:
// AsmHelp.h
extern int calc_max_x64_asm(void);
接着,在 AsmHelp 类的 calc_max 实例方法中调用汇编实现的 calc_max_x64_asm 函数:
@implementation AsmHelper
- (int)calc_max {
return calc_max_x64_asm();
@end
现在,我们可以回到 Swift 语言中通过 Objc 调用 calc_max_x64_asm 函数了:
import SwiftUI
struct ContentView: View {
@State var max = 0
var body: some View {
Text("max is \(max)")
.font(.title)
.onAppear {
max = AsmHelper().calc_max()
}
}
}
在模拟器或 Xcode 预览(Preview)中运行上述代码,结果不出所料:
3.2 Apple Silicon 芯片的 Mac
Silicon 处理器(M1,M2等)本身就兼容 arm 指令集,所以对于此 Mac 中模拟器汇编语言的适配,可以按照下面 “在 iPhone 真机中” 运行的方案来对待。
小知识:其实在 Silicon 上也可以用 intel x64 “仿真”模式运行模拟器,只需在 Xcode 中设置 Destination Architectures 为 ‘Show Rosetta Destinations’ (或 Show Both)即可:
4. 如何在真机中使用 Swift + ARM64 汇编指令?
如果在 Xcode 中将构建目标由模拟器变为真机,那么编译上面包含 intel x64 汇编代码的项目 ‘Asm_X64’ 会妥妥的报错:
这是因为 真机设备的处理器是 arm 架构,要想在真机上运行我们需要使用 ARM64 种类的汇编代码。
- iPhone 14PM(Pro Max)使用 A16 处理器(4纳米)
- iPhone XR 使用 A12处理器(7纳米)
在文章最后,我们会分别在以上两种真机上运行 ARM64 汇编测试代码,并比较它们的耗时情况。
现在,为了不与之前 intel x64 汇编代码混淆,我们新建另一个名为 ‘Asm_ARM64’ 的 Xcode 项目,其中 AsmHelper 类的实现和桥接头文件的内容和之前完全相同。
接着,我们创建 max_arm64.s 文件,并填入如下代码:
.text
.globl _calc_max_arm64_asm
.p2align 2
// 方法进入点
_calc_max_arm64_asm:
// 方法构造前缀
sub sp,sp,#32
stp x29,x30,[sp,#16]
add x29,sp,#16
/*
实际代码将在此...
*/
// 方法析构后缀
ldp x29,x30,[sp,#16]
add sp,sp,#32
// 返回值放在 x0 寄存器中
mov x0,$11
ret
可以看到:ARM64 实现的函数和 intel x64 汇编类似,都符合各自 ABI 接口的规范。
该项目的 Swift 代码也和 ‘Asm_X86’ 项目中的如出一辙。我们选择真机作为构建目标,编译运行后的结果也应该和之前模拟器上的完全相同。
5. Xcode 如何根据不同编译目标切换不同类型的汇编代码?
细心的小伙伴们可能察觉到了:把 intel x64 和 ARM64 汇编代码分散在两个不同项目中既 不利于测试 又 违反了 DRY 原则。
能不能把它们放在同一个项目中,而根据不同构建目标(真机或模拟器)来切换对应的汇编代码呢?
必须可以!!!
5.1 ”新目标,新征程“
我们回到上面 ‘Asm_X64’ 项目里,在原目标(TARGET)基础之上“克隆”一个新目标:
修改新目标的名称为 AsmTestARM:
接着,打开 Scheme 管理界面,同样将新 Scheme 名称也改为 AsmTestARM:
现在我们有了两个 Scheme:AsmTest 和 AsmTestARM,分别对应于 x86 和 ARM64 汇编代码文件。
关键的部分来了,我们导入之前 ‘Asm_ARM64’ 项目中的 ARM64 汇编文件 max_arm64.s,并将它的 Target 成员关系设为 AsmTestARM:
同样的,将原来的 x64 汇编文件 max_x64.s 的 Target 成员关系设为 AsmTest:
5.2 条件编译走起!
上面,我们已经把不同汇编代码文件划分到了对应的 Scheme 中,接下来还需要根据不同构建目标,来选择实际使用的汇编函数。
按如下代码修改 AsmHelper 类中的 calc_max 实例方法:
@implementation AsmHelper
- (int)calc_max {
#if TARGET_IPHONE_SIMULATOR
return calc_max_x64_asm();
#elif TARGET_OS_IPHONE
return calc_max_arm64_asm();
#endif
}
@end
如上代码所示,我们使用 #if 宏指令来选择调用合适的汇编函数。同样的,在 AsmHelper.h 中也按相同条件来声明对应的外部函数:
#if TARGET_IPHONE_SIMULATOR
extern int calc_max_x64_asm(void);
#elif TARGET_OS_IPHONE
extern int calc_max_arm64_asm(void);
#endif
原 Swift 代码无需做任何修改。
现在,我们可以根据不同构建目标(模拟器或真机)来切换 Scheme 了:
至此,项目中不同类型的汇编代码在模拟器或真机上编译运行都不会有任何问题了,棒棒哒!💯
6. 跳过 Objc 直接与汇编代码混编
经过不懈努力,我们完成了 Swift 与汇编语言的混合编译。
不过,实现中使用了 Objc 作为“媒介”,仔细考虑会发现这纯属多余:因为 AsmHelper 类基本自己啥也没干!
Swift 能不能完全甩开 Objc 直接与汇编“在一起”呢?
答案是肯定的!
删除项目中的 AsmHelper.m 文件,并只保留 AsmHelper.h 文件中相关的条件编译代码:
// AsmHelper.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#if TARGET_IPHONE_SIMULATOR
extern int calc_max_x64_asm(void);
#elif TARGET_OS_IPHONE
extern int calc_max_arm64_asm(void);
#endif
/*
@interface AsmHelper : NSObject
-(int)calc_max;
@end
*/
NS_ASSUME_NONNULL_END
回到 Swift 的 ContentView 结构中,创建 calc_max() 方法,其中我们根据目标的架构来直接选择实际的汇编函数。
完整代码如下:
import SwiftUI
struct ContentView: View {
@State var max = 0
private func calc_max() -> Int {
#if arch(x86_64)
return Int(calc_max_x64_asm())
#elseif arch(arm64)
return Int(calc_max_arm64_asm())
#else
return -1
#endif
}
var body: some View {
Text("max is \(max)")
.font(.title)
.onAppear {
max = calc_max()
}
}
}
大功告成,Objc 再见!
7. 实例测试真机上汇编语言执行效率;
在 搞定“超超超难”剑桥面试数学题番外篇:ARM64汇编 这篇博文中,我们在 M2 处理器上测试了 ARM64 汇编的表现。
现在,我们用相同的汇编代码在 iPhone 真机上测试一下。
将上面 max_arm64.s 文件的代码改为如下内容:
.equ total, 63
.text
.globl _calc_max_arm64_asm
.p2align 2
_calc_max_arm64_asm:
sub sp,sp,#32
stp x29,x30,[sp,#16]
add x29,sp,#16
mov w0,1 // a in w0
mov w1,w0 // b in w1
mov w2,w0 // c in w2
mov w3,w0 // d in w3
mov w11,wzr // max in w11
start_a_loop:
cmp w0,total
b.hi end_a_loop
start_b_loop:
cmp w1,total
b.hi end_b_loop
start_c_loop:
cmp w2,total
b.hi end_c_loop
start_d_loop:
cmp w3,total
b.hi end_d_loop
// 计算 a + b + c + d 的值
add w4,w0,w1
add w4,w4,w2
add w4,w4,w3
cmp w4,total
b.ne not_equ_63
// 若等于 a + b + c + d = 63,则计算 ab + bc + cd 的值 x
mul w4,w0,w1
mul w5,w1,w2
mul w6,w2,w3
add w5,w5,w6
add w4,w4,w5
// 若 x > max ,则需要更新 max 为 x 值
cmp w4,w11
b.ls not_equ_63
mov w11,w4
not_equ_63:
add w3,w3,#1
b start_d_loop
end_d_loop:
mov w3,wzr
add w2,w2,#1
b start_c_loop
end_c_loop:
mov w2,wzr
add w1,w1,#1
b start_b_loop
end_b_loop:
mov w1,wzr
add w0,w0,#1
b start_a_loop
end_a_loop:
ldp x29,x30,[sp,#16]
add sp,sp,#32
// 将计算出来的最大值通过 x0 寄存器返回
mov x0,x11
ret
string: .asciz "max is %ld\n"
将 ContentView 中的 body 也略作更改:
var body: some View {
Text("max is \(max)")
.font(.title)
.onAppear {
let start = Date.now
let result = calc_max()
print("\(String(format: "耗时 %0.5f 秒", start.distance(to: Date.now)))")
max = result
}
}
在 iPhone XR 和 iPhone 14 Pro Max 真机运行上面的汇编代码,差距并没有想象那么大:
// 汇编代码执行耗时
// iPhone XR
耗时 0.01920 秒
// iPhone 14PM
耗时 0.01469 秒
为了便于大家横向比较,同样贴出纯 Swift 优化后(Release)代码的执行结果:
// Swift 代码执行耗时
// iPhone XR
耗时 0.02132 秒
// iPhone 14PM
耗时 0.01704 秒
可以发现,在 iPhone 上我们的汇编执行速度要比优化后的 Swift 代码还要快。
此役,在 A系列芯片上汇编终于扳回一局!!! ❤️
同样将 Swift 语言的测试代码贴在下方,以满足小伙伴们的好奇心:
typealias GroupNumbers = (a: Int, b: Int, c: Int, d: Int, rlt: Int)
@inline(__always) fileprivate func value(_ g: GroupNumbers) -> Int {
g.a * g.b + g.b * g.c + g.c * g.d
}
func calc_max_swift() -> Int {
var max = 0
let r = 1...63
for a in r {
for b in r {
for c in r {
for d in r {
if a + b + c + d == 63 {
let v = (a: a, b: b, c: c, d: d, rlt: 0)
let rlt = value(v)
if rlt >= max {
max = rlt
}
}
}
}
}
}
return max
}
总结
在本篇博文中,我们图文并茂的详细介绍了如何在真机或模拟器上运行 Swift 和汇编混编后的代码,并通过一个实际例子来测试不同 iPhone 上汇编的执行效率。
最后,还得问一下小伙伴:你们学会了吗?😎
结束语
Hi,我是大熊猫侯佩,一名非自由App开发者,希望我的文章可以解决你的痛点、难点问题。
如果还有问题欢迎在下面一起讨论吧 😉
感谢观赏,再会。