搞定剑桥面试数学题番外篇2:使用多线程并发“加强版”

搞定剑桥面试数学题番外篇2:使用多线程并发“加强版”_第1张图片

0. 概览

我们在之前三篇博文中已经介绍了如何用多种语言(ruby、swift、c、x64 汇编和 ARM64 汇编)实现一道“超超超难”的剑桥数学面试题:

  • · 有趣的小实验:四种语言搞定“超超超难”剑桥面试数学题

  • · 搞定“超超超难”剑桥面试数学题番外篇:ARM64汇编

  • · 超详细:实现 Swift 与 汇编(Asm)代码混编并在真机或模拟器上运行

在以上这一系列博文中,我们用多种语言生成可执行文件,并分别在多个平台做了性能测试:

  • MacBook Pro(Intel i5 2.9GHz)
  • MacBook Air (M2)
  • iPhone XR
  • iPhone 14 Pro Max

现在,我们还想利用 cpu 强大的多核并发执行来进一步提高我们的算法速度。在本篇博文中,我们将使用 swift、c、x64汇编以及 ARM64 汇编语言来完成此“挑战”!

本文用半娱乐的心境写成,不求测试多么精确,但求保持一颗童心,Let‘s go!!!


1. 题目回顾

在这里插入图片描述

题目很简单:

  • 如果 a + b + c + d = 63;
  • 求 ab + bc + cd 的最大值;
  • 其中 a、b、c、d 都为自然数;

我们假设 0 不属于自然数,即有: a、b、c、d 最小值皆为 1。

2. swift 语言

我们先易后难,先让 swift 打头阵。

swift 语言是并发编程的“绝顶高手”!我们有多种“姿势”可以实现代码并发执行,这里我们使用 async/await 结构化并发来完成它。

使用 Xcode 新建控制台类型项目,并填入如下代码:

import Foundation

typealias GroupNumbers = (a: Int, b: Int, c: Int, d: Int, rlt: Int)

let full_range = 1...63
func calc_max_range(_ r: ClosedRange<Int>) async -> Int {
    var max = 0
    for a in r {
        for b in full_range {
            for c in full_range {
                for d in full_range {
                    if a + b + c + d == 63 {
                        let rlt = a*b + b*c + c*d
                        if rlt >= max {
                            max = rlt
                        }
                    }
                }
            }
        }
    }

    return max
}

let group = DispatchGroup()
group.enter()
Task {
    async let r0 = calc_max_range(1...15)
    async let r1 = calc_max_range(16...30)
    async let r2 = calc_max_range(31...45)
    async let r3 = calc_max_range(46...63)
    
    let max = await [r0, r1, r2, r3].max()!
    print("max is \(max)")
    group.leave()
}

group.wait()

使用 Release 配置编译运行,在 M2 上大约耗时 0.017 秒左右。

3. c 语言

c 语言并发编程远比想象的要简单的多,我们可以利用 pthread 库非常容易的把指令流调度到多核上:

#include 
#include 

const int START = 1;
const int END = 63;

typedef struct {
    pthread_t id;
    int start, end;
    int max;
} PInfo;

void *calc_range(void *args) {
    int max = 0;
    PInfo* info = (PInfo *) args;

    for (int a = info->start; a <= info->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;
                        }
                    }
                }
            }
        }
    }

    info->max = max;
    pthread_exit(NULL);
}

int main() {

    PInfo infos[4] = {
        {NULL, 1, 15, 0},
        {NULL, 16, 30, 0},
        {NULL, 31, 45, 0},
        {NULL, 46, END, 0}
    };

    pthread_create(&infos[0].id, NULL, calc_range, &infos[0]);
    pthread_create(&infos[1].id, NULL, calc_range, &infos[1]);
    pthread_create(&infos[2].id, NULL, calc_range, &infos[2]);
    pthread_create(&infos[3].id, NULL, calc_range, &infos[3]);

    int max = 0;
    for (int i = 0; i < 4; i++) {
        pthread_join(infos[i].id, NULL);
        if(infos[i].max > max) {
            max = infos[i].max;
        }
    }

    printf("max is %d\n", max);
    return 0;
}

因为上面 4 个线程写入结果的地址都不相同,所以不会有数据竞争发生。

使用 O2 选项优化编译代码,同样在 M2 mac 上运行平均耗时 0.015 秒,比 swift 有所进步。

4. x64 汇编

x64 汇编相比较 swift 和 c 两种语言,更显“繁琐”一些。

不过核心思路仍然很简单:我们只需调用系统 fork 功能号创建新线程,并发计算即可。

但是,这样要处理的底层事情太多,不如借助 c 库(pthread)更简单!

# as mt_x64.s -o mt_x64.o
# ld mt_x64.o -lSystem -L `xcrun --show-sdk-path -sdk macosx`/usr/lib -o mt_x64  

.equ    loop_upper_bound, 63
.equ    NULL,0
// PINFO 结构的长度
.equ    PINFO_SIZE, pinfo_1 - pinfo_0

// 函数构造器
.macro  func_constructor
    push    %rbp
    mov     %rsp,%rbp
    pushq   %rbx
    pushq   %rdx
.endm

// 函数析构器
.macro  func_destructor
    popq    %rdx
    popq    %rbx
    popq    %rbp
.endm
    
    .data
pinfo_0:
    // pthread_t, start, end, max
    .quad NULL,1,15,0
pinfo_1:
    .quad NULL,16,30,0
pinfo_2:
    .quad NULL,31,45,0
pinfo_3:
    .quad NULL,46,loop_upper_bound,0
max:
    .quad 0
string: .asciz  "max is %ld\n"
    .text
    .globl      _main
    .p2align    4, 0x90
_main:
    func_constructor
    
    leaq    pinfo_0(%rip),%rdi
    movq    $0,%rsi
    leaq    calc_max_in_range_func(%rip),%rdx
    movq    %rdi,%rcx
   	// pthread_create(rdi,rsi,rdx,rcx)
    call    _pthread_create

    leaq    pinfo_1(%rip),%rdi
    movq    $0,%rsi
    leaq    calc_max_in_range_func(%rip),%rdx
    movq    %rdi,%rcx
    call    _pthread_create

    leaq    pinfo_2(%rip),%rdi
    movq    $0,%rsi
    leaq    calc_max_in_range_func(%rip),%rdx
    movq    %rdi,%rcx
    call    _pthread_create

    leaq    pinfo_3(%rip),%rdi
    movq    $0,%rsi
    leaq    calc_max_in_range_func(%rip),%rdx
    movq    %rdi,%rcx
    call    _pthread_create

    movq    pinfo_0(%rip),%rdi
    xorq    %rsi,%rsi
    // pthread_join(rdi,rsi)
    call    _pthread_join

    movq    pinfo_1(%rip),%rdi
    xorq    %rsi,%rsi
    call    _pthread_join

    movq    pinfo_2(%rip),%rdi
    xorq    %rsi,%rsi
    call    _pthread_join

    movq    pinfo_3(%rip),%rdi
    xorq    %rsi,%rsi
    call    _pthread_join

    // i in rax, pinfo addr in rbx, max in r11
    movq    $3,%rax
    leaq    pinfo_0(%rip),%rbx
    mov     24(%rbx),%r11
1:
    addq    $PINFO_SIZE,%rbx
    mov     24(%rbx),%r12
    cmpq    %r12,%r11
    jg      2f
    mov     %r12,%r11
2:
    dec     %rax
    jz      3f
    jmp     1b
3:
    movq    %r11,%rsi
    lea     string(%rip),%rdi
    callq   _printf
    func_destructor
    xor     %rax,%rax
    ret
.pushsection "__TEXT", "__text"
// void *calc_max_in_range(void *arg);
calc_max_in_range_func:
    func_constructor

    // void *arg in %rdi
    // rax: start, r12: a loop end
    movq    8(%rdi),%rax
    movq    16(%rdi),%r12
    mov     $1,%rbx
    mov     %rbx,%rcx
    mov     %rbx,%rdx
    // max in r11
    xor     %r11,%r11
start_a_loop:
    cmpq    %r12,%rax
    jg      end_a_loop
start_b_loop: 
    cmpq    $loop_upper_bound,%rbx
    jg      end_b_loop
start_c_loop:
    cmpq    $loop_upper_bound,%rcx
    jg      end_c_loop
start_d_loop:
    cmpq    $loop_upper_bound,%rdx
    jg      end_d_loop

    # if a + b + c + d == 63
    xorq    %r8,%r8
    add     %rax,%r8
    add     %rbx,%r8
    add     %rcx,%r8
    add     %rdx,%r8
    cmpq    $loop_upper_bound,%r8
    jne     not_equ_63
    # == 63, 计算 a*b + b*c + c*d 放到 r8 中
    mov     %rax,%r8
    imul    %rbx,%r8
    
    mov     %r8,%r9
    mov     %rbx,%r8
    imul    %rcx,%r8
    
    mov     %r8,%r10
    mov     %rcx,%r8
    imul    %rdx,%r8
    
    addq    %r9,%r8
    addq    %r10,%r8
    cmpq    %r11,%r8
    jl      not_equ_63

    # 更新 max 值
    mov     %r8,%r11
not_equ_63:
    incq    %rdx
    jmp     start_d_loop
end_d_loop:
    mov     $1,%rdx
    incq    %rcx
    jmp     start_c_loop
end_c_loop:
    mov     $1,%rcx
    incq    %rbx
    jmp     start_b_loop
end_b_loop:
    mov     $1,%rbx
    incq    %rax
    jmp     start_a_loop
end_a_loop:
    mov     %r11,24(%rdi)
    func_destructor
    xorq    %rax,%rax
    ret
.popsection

咋一看上面代码很长,但其本质很简单,我们在关键处做了注释,方便大家阅览。

使用调试器加载运行 x64 汇编代码生成的可执行文件,在第一个 pthread_join 函数下断点,中断后可以发现我们的计算确是在多个线程上并发进行的:

汇编代码在 MBP(intel i5)上大约耗时 0.025 秒。


我们可以在  Silicon 芯片的 mac 上使用交叉编译来处理上述 x64 汇编代码,然后利用 Rosetta 2 来运行它:

// 使用 x86_64 架构编译代码
as x64.s -arch x86_64 -o x64.o
ld x64.o -lSystem -L `xcrun --show-sdk-path -sdk macosx`/usr/lib -o x64    

运行发现耗时仅 0.023 秒左右,在 M2 上翻译执行的速度反而要比在 intel cpu 上原生执行还要快,只能说那台 intel MBP “廉颇老矣”…

为了证明上面 x64 汇编产生的可执行文件的确是 intel x64 指令集,我们可以用 otool 工具来验证一下:

hopy@Love2 asm % otool -tvV x64
x64:
(__TEXT,__text) section
_main:
0000000100003de0	pushq	%rbp
0000000100003de1	movq	%rsp, %rbp
0000000100003de4	pushq	%rbx
0000000100003de5	pushq	%rdx
0000000100003de6	leaq	pinfo_0(%rip), %rdi
0000000100003ded	movq	$NULL, %rsi
0000000100003df4	leaq	calc_max_in_range_func(%rip), %rdx
0000000100003dfb	movq	%rdi, %rcx
0000000100003dfe	callq	0x100003edc                     ## symbol stub for: _pthread_create
0000000100003e03	leaq	pinfo_1(%rip), %rdi
0000000100003e0a	movq	$NULL, %rsi
0000000100003e11	leaq	calc_max_in_range_func(%rip), %rdx
0000000100003e18	movq	%rdi, %rcx
0000000100003e1b	callq	0x100003edc                     ## symbol stub for: _pthread_create
0000000100003e20	leaq	pinfo_2(%rip), %rdi
0000000100003e27	movq	$NULL, %rsi
0000000100003e2e	leaq	calc_max_in_range_func(%rip), %rdx
0000000100003e35	movq	%rdi, %rcx
0000000100003e38	callq	0x100003edc                     ## symbol stub for: _pthread_create
0000000100003e3d	leaq	pinfo_3(%rip), %rdi
0000000100003e44	movq	$NULL, %rsi
0000000100003e4b	leaq	calc_max_in_range_func(%rip), %rdx
0000000100003e52	movq	%rdi, %rcx
0000000100003e55	callq	0x100003edc                     ## symbol stub for: _pthread_create
0000000100003e5a	movq	pinfo_0(%rip), %rdi
0000000100003e61	xorq	%rsi, %rsi
0000000100003e64	callq	0x100003ee2                     ## symbol stub for: _pthread_join
0000000100003e69	movq	pinfo_1(%rip), %rdi
0000000100003e70	xorq	%rsi, %rsi
0000000100003e73	callq	0x100003ee2                     ## symbol stub for: _pthread_join
0000000100003e78	movq	pinfo_2(%rip), %rdi
0000000100003e7f	xorq	%rsi, %rsi
0000000100003e82	callq	0x100003ee2                     ## symbol stub for: _pthread_join
0000000100003e87	movq	pinfo_3(%rip), %rdi
0000000100003e8e	xorq	%rsi, %rsi
0000000100003e91	callq	0x100003ee2                     ## symbol stub for: _pthread_join
0000000100003e96	movq	$0x3, %rax
0000000100003e9d	leaq	pinfo_0(%rip), %rbx
0000000100003ea4	movq	0x18(%rbx), %r11
0000000100003ea8	addq	$0x20, %rbx
0000000100003eac	movq	0x18(%rbx), %r12
0000000100003eb0	cmpq	%r12, %r11
0000000100003eb3	jg	0x100003eb8
0000000100003eb5	movq	%r12, %r11
0000000100003eb8	decq	%rax
0000000100003ebb	je	0x100003ebf
0000000100003ebd	jmp	0x100003ea8
0000000100003ebf	movq	%r11, %rsi
0000000100003ec2	leaq	string(%rip), %rdi
0000000100003ec9	callq	0x100003ed6                     ## symbol stub for: _printf
0000000100003ece	popq	%rdx
0000000100003ecf	popq	%rbx
0000000100003ed0	popq	%rbp
0000000100003ed1	xorq	%rax, %rax
0000000100003ed4	retq

看到了吗?百分之百纯正 intel x64 指令!


5. ARM64 汇编

现在,让最后一位选手 ARM64 登场吧。

ARM64 和 x64 汇编都可以用 as 汇编器(Mac OS X Mach-O GNU-based assemblers)编译成目标文件。我们还可以利用 as 的 arch 选项实现跨平台交叉编译。

比如,我们在 intel Mac 上可以利用如下命令编译 ARM64 格式的汇编代码:

as test_arm64.s -arch arm64 -o arm64.o

类似的,前面我们也讨论过如何在 M2 Mac 上编译执行 x64 汇编代码。

下面,我们就用 ARM64 汇编代码实现多线程并发计算:

# as mt_arm64.s -o mt_arm64.o
# ld mt_arm64.o -lSystem -L `xcrun --show-sdk-path -sdk macosx`/usr/lib -o mt_arm64
# mt_arm64.s

.equ    loop_upper_bound, 63
.equ    NULL,0

// 函数构造器
.macro  func_constructor
    sub     sp,sp,#32
    stp     x29,x30,[sp,#16]
    add     x29,sp,#16
.endm

// 函数析构器
.macro  func_destructor
    ldp     x29,x30,[sp,#16]
    add     sp,sp,#32
.endm
    
    .data
pinfo_0:
    // pthread_t, start, end, max
    .quad NULL,1,15,0
pinfo_1:
    .quad NULL,16,30,0
pinfo_2:
    .quad NULL,31,45,0
pinfo_3:
    .quad NULL,46,loop_upper_bound,0

    .text
    .globl  _main
    .p2align    2
// *******************************************************************
_main:
    func_constructor

    adrp    x0,pinfo_0@PAGE
    add     x0,x0,pinfo_0@PAGEOFF
    mov     x19,x0

    mov     x1,#0
    adr     x2,_calc_max_thread_func
    mov     x3,x19
    bl      _pthread_create

    add     x5,x19,#32
    mov     x0,x5
    mov     x1,#0
    adr     x2,_calc_max_thread_func
    mov     x3,x5
    bl      _pthread_create

    add     x5,x19,#64
    mov     x0,x5
    mov     x1,#0
    adr     x2,_calc_max_thread_func
    mov     x3,x5
    bl      _pthread_create

    add     x5,x19,#96
    mov     x0,x5
    mov     x1,#0
    adr     x2,_calc_max_thread_func
    mov     x3,x5
    bl      _pthread_create

    ldr     x0,[x19]
    mov     x1,xzr
    bl      _pthread_join

    add     x5,x19,#32
    ldr     x0,[x5]
    mov     x1,xzr
    bl      _pthread_join

    add     x5,x19,#64
    ldr     x0,[x5]
    mov     x1,xzr
    bl      _pthread_join

    add     x5,x19,#96
    ldr     x0,[x5]
    mov     x1,xzr
    bl      _pthread_join

    mov     x11,xzr
    mov     x0,4
0:
    ldr     x1,[x19,24]
    cmp     x11,x1
    b.ge    1f
    mov     x11,x1
1:
    subs    x0,x0,1
    b.eq    2f
    add     x19,x19,#32
    b       0b
2:
    adr     x0,string
    str     x11,[sp]
    bl      _printf

    func_destructor
    mov     x0,xzr
    ret
// *******************************************************************
// void *calc_max_thread_func(void *arg)
_calc_max_thread_func:
    func_constructor
    str     x0,[x29,#-8]

    mov     x12,x0
    ldr     x0,[x12,#8]
    ldr     x1,[x12,#16]
    bl      _calc_max_in_range

    ldr     x12,[x29,#-8]
    str     x0,[x12,#24]

    func_destructor
    mov     x0,NULL
    ret
// *******************************************************************
.pushsection "__TEXT", "__text"
// long calc_max_in_range(long start, long end);
_calc_max_in_range:
    func_constructor
    // start in x0, end in x1
    mov     x12,x1  // a loop end in x12
    mov     x1,#1   // b in x1
    mov     x2,x1   // c in x2
    mov     x3,x1   // d in x3
    mov     x11,xzr // max in x11
1:
    cmp     x0,x12
    b.hi    9f
2:
    cmp     x1,loop_upper_bound
    b.hi    8f
3:
    cmp     x2,loop_upper_bound
    b.hi    7f
4:
    cmp     x3,loop_upper_bound
    b.hi    6f
    // 计算 a + b + c + d 的值
    add     x4,x0,x1
    add     x4,x4,x2
    add     x4,x4,x3
    cmp     x4,loop_upper_bound
    b.ne    5f
    // 若等于 a + b + c + d = 63,则计算 ab + bc + cd 的值 x
    mul     x4,x0,x1
    mul     x5,x1,x2
    mul     x6,x2,x3
    add     x5,x5,x6
    add     x4,x4,x5
    // 若 x > max ,则需要更新 max 为 x 值
    cmp     x4,x11
    b.ls    5f
    mov     x11,x4
5:
    add     x3,x3,#1
    b       4b
6:
    mov     x3,#1
    add     x2,x2,#1
    b       3b
7:
    mov     x2,#1
    add     x1,x1,#1
    b       2b
8:
    mov     x1,#1
    add     x0,x0,#1
    b       1b
9:
    func_destructor
    mov     x0,x11
    ret
.popsection
string: .asciz  "max is %ld\n"

如上所示:ARM64 汇编语言的数据格式和 x64 汇编没有什么差别,不过指令语法、函数构造器和析构器等还是有很大不同的。

将上述代码编译链接为可执行文件,在 M2 MBA 上运行耗时大约在 0.015 秒左右,和 c 旗鼓相当。

之前在 M2 上单线程执行算法耗时将近 0.03 秒之多,现在快了 1 倍!可见  Silicon 芯片上多核并发执行就是“香”啊!

值得一提的是,因为  的A系列处理器(比如 iPhone14 Pro Max 的 A16)同样兼容 ARM64 指令集,所以上述代码同样可以运行在 iPhone 真机上。

6. 总结

在本篇博文中,我们使用并行算法(swift、c、x64汇编和 ARM64 汇编)充分“榨干” 了 cpu ,进一步提高了原算法的速度,棒棒哒!

感谢观赏,再会!

你可能感兴趣的:(极客,swift,ARM64,汇编,x64,汇编,并发执行,c,语言)