0. 概览
在 有趣的小实验:四种语言搞定“超超超难”剑桥面试数学题 那篇博文中,我们使用 4 种语言(x64汇编、C、Swift 以及 Ruby)实现了一道算法题。
不过,其中的汇编语言对应的是 intel CPU 上的 x64 指令集,那么能不能在 Apple Silicon 芯片(M1,M2)上使用汇编来原生实现相同的功能呢?
答案是肯定的!
在本篇博文中,我们将会在 M2 芯片的 Mac 上使用 ARM64 汇编语言完成算法题的挑战。
本文测试环境为:M2 芯片 + 16GB 内存的 MBA,macOS Ventura 13.4。
闲言少叙,让我们躁起来 😉
Let’s go!!!
1. 情景重现
让我们回顾一下原题,这是一道剑桥面试题(如下图所示),图中用了 4 个 ‘Super’ 来形容它的“超超超超级难”:
题目的意思很简单:
- 如果 a + b + c + d = 63;
- 求 ab + bc + cd 的最大值;
- 其中 a、b、c、d 都为自然数;
如果说用用数学证明它“难得离谱”的话,那么编程来解决则恰恰相反:没有比写代码搞定它更容易的事了。
2. M2 芯片上的重测结果
首先,我们用之前博文中相同的 Ruby、Swift 和 C 代码生成可执行文件,在 M2 上重新运行一下,看看速度如何。
为了方便小伙伴们阅览,下面把每种语言原来的代码一并奉上。
2.1 Ruby 语言
#!/usr/bin/ruby
max = 0
r = 1..63
for a in r do
for b in r do
for c in r do
for d in r do
if a + b + c + d == 63
rlt = a*b + b*c + c*d
if rlt >= max
max = rlt
end
end
end
end
end
end
print("max is #{max}\n")
连续执行 3 次,平均耗时大约 0.7 秒左右:
% time ./test.rb
max is 991
./test.rb 0.67s user 0.03s system 99% cpu 0.701 total
% time ./test.rb
max is 991
./test.rb 0.67s user 0.03s system 99% cpu 0.706 total
% time ./test.rb
max is 991
./test.rb 0.67s user 0.03s system 99% cpu 0.702 total
2.2 Swift 语言
import Foundation
typealias GroupNumbers = (a: Int, b: Int, c: Int, d: Int, rlt: Int)
@inline(__always) func value(_ g: GroupNumbers) -> Int {
g.a * g.b + g.b * g.c + g.c * g.d
}
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
}
}
}
}
}
}
print("max is \(max)")
照旧使用 Xcode 创建 Command Line Tool 类型项目,将源代码贴入 main.swift 文件中,不过这次为我们干苦力的是最新的 Xcode 15 beta。
我们直接切换至 Release 模式,运行结果如下:
% time ./test
max is 991
./test 0.02s user 0.00s system 86% cpu 0.023 total
% time ./test
max is 991
./test 0.02s user 0.00s system 86% cpu 0.024 total
% time ./test
max is 991
./test 0.02s user 0.00s system 89% cpu 0.021 total
可以看到,Swift 优化还是一如既往的棒,只需 0.02 秒多一点。
2.3 C 语言
#include <stdio.h>
int main() {
int start = 1;
int end = 63;
int max = 0;
for (int a = start; a <= end; a++) {
for (int b = start; b <= end; b++) {
for (int c = start; c <= end; c++) {
for (int d = start; d < end; d++) {
if(a + b + c + d == 63) {
int rlt = a*b + b*c + c*d;
if(rlt >= max) {
max = rlt;
}
}
}
}
}
}
printf("max is %d\n", max);
return 0;
}
同样,我们使用clang 的 O2 优化选项来编译 C 代码,耗时稍微比 Swift 慢一丢丢:
% time ./test
max is 991
./test 0.02s user 0.00s system 93% cpu 0.027 total
% time ./test
max is 991
./test 0.02s user 0.00s system 93% cpu 0.027 total
% time ./test
max is 991
./test 0.02s user 0.00s system 93% cpu 0.027 total
ARM64 汇编
因为无论是 C、Swift 还是 Ruby 的源代码都可以“原封不动”的在 intel 或 M2 芯片上编译执行,所以它们的兼容性非常高!
不过,汇编语言可就“拖后腿”了。
x64 汇编是无法直接在 M2 上执行的。于是乎,我们呼唤 M2 芯片原生指令集汇编火速抵达战场。
x64 汇编生成的可执行文件能不能通过 Rosetta2 在 M2 上执行呢?这是一个有趣的问题,有条件的小伙伴们可以亲自测试一下。😉
所以现在,轮到我们主角 ARM64 闪亮登场了。
Apple Silicon(M1,M2等)是苹果公司开发的基于 ARM 架构的系列芯片,用于替代之前使用的英特尔芯片,旨在提高 Mac 电脑的性能和能效。
Apple Silicon 芯片兼容 ARMv8 指令集,并以 64 位模式(AArch64)运行。所以,我们可以使用 ARM64 汇编语言来构建 M2 上的可执行文件。
ARM 是 risc 架构的 cpu,它的指令是等长(32位)的,不像 intel 是变长指令集。
关于更多 64 位汇编语言的相关知识,感谢兴趣的小伙伴们可以移步到我的汇编(Asm)专栏中观赏相关文章:
- 大熊猫侯佩的 Asm 系列专栏
下面的 ARM64 汇编代码在关键处做了详细的注释,大家可以轻松读懂:
# as test_arm64.s -o ta64.o
# ld ta64.o -lSystem -L `xcrun --show-sdk-path -sdk macosx`/usr/lib -o ta64
# test_a64.s
.equ total, 63
.text
.globl _main
.p2align 2
_main:
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:
adr x0,string
str x11,[sp]
bl _printf
ldp x29,x30,[sp,#16]
add sp,sp,#32
# eor x0,x0,x0
mov x0,#0
ret
string: .asciz "max is %ld\n"
使用 as 汇编器和 ld 链接器生成可执行文件,那么它的执行速度如何呢?
% time ./ta64
max is 992
./ta64 0.03s user 0.00s system 91% cpu 0.034 total
% time ./ta64
max is 992
./ta64 0.03s user 0.00s system 94% cpu 0.031 total
% time ./ta64
max is 992
./ta64 0.03s user 0.00s system 94% cpu 0.031 total
可能大家会对上面的结果失望了,它比 Swift 和 C 还要慢。
至于是什么原因,我们在之前的博文中已经谈过了。
虽然上面汇编代码运行速度只能排在第 3 名,但它也是可以再被优化的。不过,限于篇幅这里不再展开说明了,有兴趣的童鞋可以关注我的其它博文。
另一种乘法的实现
上面我们在实现 ab + bc + cd 的计算时,使用的是标准的乘法指令: mul。M2 芯片还支持 SIMD (单指令流多数据流)指令集,它们可以在一条指令中同时完成多个数据的计算。
下面,我们就用 SIMD 指令来重写之前 mul 乘法计算的逻辑:
// 其余代码不变,省略之...
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
// 将 a,b,c 依次放入 v1 SIMD 寄存器的 3 个 32 位通道中
mov v1.s[0],w0
mov v1.s[1],w1
mov v1.s[2],w2
// 同样将 b,c,d 依次放入 v2 寄存器的对应通道中
mov v2.s[0],w1
mov v2.s[1],w2
mov v2.s[2],w3
// 用 mul.4s 指令同时计算 a*b,b*c,c*d 的值
mul.4s v0,v1,v2
// 计算 ab + bc + cd 的值,并将结果放入 w4 寄存器
mov w4,v0.s[0]
mov w5,v0.s[1]
mov w6,v0.s[2]
add w5,w5,w6
add w4,w4,w5
/* 原来的 mul 指令...
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:
运行发现,结果并没有太大变化,可能该场景不是 SIMD 指令发挥最大威力的领域:
% time ./ta64
max is 992
./ta64 0.03s user 0.00s system 93% cpu 0.031 total
% time ./ta64
max is 992
./ta64 0.03s user 0.00s system 93% cpu 0.031 total
至此,我们分别用 Ruby、Swift、C、x64 和 ARM64 汇编语言实现了博文开头的算法题,相信大家对于这些语言的语法特点、底层兼容性和速度有了更多的了解。
总结
在本篇博文中,我们用 ARM64 汇编实现了之前题目的算法,并用 SIMD 指令重写了其中对应的乘法(mul)运算操作。
感谢观赏,再会!😎