匹配表达式 match

match 表达式与 C/C++ 中的 switch/case 语句类似, 用于匹配多个分支条件.

它的语法如下所示:

match value {
  pattern1 => expr1,
  pattern2 => expr2,
  ...
}

注意上面每个分支匹配表达式是以逗号 , 结尾的, 如果该表达式是块表达式 (block expression), 这个逗号就可以省略不写. match 语句中的匹配优先级是根据分支顺序来确定的, 即优先检查第一条分支条件 pattern1 是否匹配, 如果匹配则执行 expr1 表达式, 并跳过剩下的所有分支. Rust 要求这些分支中必须有一条被匹配成功, 比如, 如果枚举中的条目没有被完全匹配到的话, 编译器就会报错:

#![allow(dead_code)]

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday,
}

fn main() {
    let date = Weekday::Friday;

    let to_weekend: i32 = match date {
        Weekday::Monday => 5,
        Weekday::Tuesday => 4,
        Weekday::Wednesday => 3,
        Weekday::Thursday => 2,
        Weekday::Friday => 1,
        Weekday::Saturday => 0,

        // 把下面这一行代码注释掉并重新编译, 查看编译器报错信息.
        // 它编译失败, 并提示:
        // ^^^^ pattern `Weekday::Sunday` not covered
        Weekday::Sunday => 0,
    };

    assert_eq!(to_weekend, 1);
}

上面的代码中, 最后两条可以合并在一起, 作为一个分支; 或者使用通配符匹配. match 只能在最后一个分支使用通配符匹配 (wildcard pattern), 表示无条件匹配这个分支, 类似于 C 语言 switch/case 中的 default. 先看一个简单的用例:

use std::env;

fn main() {
use std::env;

fn main() {
    let num = env::args().len();
    let accum = match num {
        1 => 100,
        2 => 200,
        3 => 300,
        _ => 0,
    };
    assert_eq!(accum, 100);
}

用 C 语言来描述相同的功能, 大概如下:

#include <assert.h>

int main(int argc, char** argv) {
  int accum;
  switch (argc) {
    case 1: {
      accum = 100;
      break;
    }
    case 2: {
      accum = 200;
      break;
    }
    case 3: {
      accum = 300;
      break;
    }
    default: {
      accum = 0;
    }
  }

  assert(accum == 100);
  
  return 0;
}

后文有单独的章节介绍 模式匹配 更多功能和写法.

跳转表 Jump Table

跳转表 Jump Table, 又称作 分支表 Branch Table, 是对分支语句的一种优化手段.

下面的代码用于本次验证:

use std::env;

fn with_if_else() {
    let num = env::args().len();
    let accum = if num == 1 {
        100
    } else if num == 2 {
        200
    } else if num == 3 {
        300
    } else if num == 4 {
        400
    } else if num == 5 {
        500
    } else if num == 6 {
        600
    } else {
        0
    };
    assert_eq!(accum, 100);
}

fn with_match_short() {
    let num = env::args().len();
    let accum = match num {
        1 => 100,
        2 => 200,
        3 => 300,
        _ => 0,
    };
    assert_eq!(accum, 100);
}

// 在 x86_64 (AMD Ryzen 5) 上超过 4 个分支后, 才会构造 jump table.
// 在 aarch64 (Rock 3A) 上面也可以发现, 当超过 4 个分支后, 会构造 jump table.
fn with_match_long() {
    let num = env::args().len();
    let accum = match num {
        1 => 100,
        2 => 200,
        3 => 300,
        4 => 400,
        _ => 0,
    };
    assert_eq!(accum, 100);
}

fn main() {
    with_if_else();
    with_match_short();
    with_match_long();
}

是上面的是 with_if_else() 函数, 它里面的分支语句比较多, 使用 rustc --emit asm jump-table.rs 命令生成汇编代码, 生成的部分 x86_64 汇编代码如下:


// with_if_else()
	.section	.text._ZN10jump_table12with_if_else17hc2cb507cd4512507E,"ax",@progbits
	.p2align	4, 0x90
	.type	_ZN10jump_table12with_if_else17hc2cb507cd4512507E,@function
_ZN10jump_table12with_if_else17hc2cb507cd4512507E:
.Lfunc_begin4:
	.cfi_startproc
	.cfi_personality 155, DW.ref.rust_eh_personality
	.cfi_lsda 27, .Lexception4
	subq	$168, %rsp
	.cfi_def_cfa_offset 176
	movq	_ZN3std3env4args17h377a659c89f76567E@GOTPCREL(%rip), %rax
	leaq	40(%rsp), %rdi
	movq	%rdi, 24(%rsp)
	callq	*%rax
	movq	24(%rsp), %rdi
.Ltmp23:
	movq	_ZN84_$LT$std..env..Args$u20$as$u20$core..iter..traits..exact_size..ExactSizeIterator$GT$3len17hfc397728d7a27a41E@GOTPCREL(%rip), %rax
	callq	*%rax
.Ltmp24:
	movq	%rax, 32(%rsp)
	jmp	.LBB47_3
.LBB47_1:
.Ltmp26:
	leaq	40(%rsp), %rdi
	callq	_ZN4core3ptr35drop_in_place$LT$std..env..Args$GT$17h27567071cfad13afE
.Ltmp27:
	jmp	.LBB47_20
.LBB47_2:
.Ltmp25:
	movq	%rax, %rcx
	movl	%edx, %eax
	movq	%rcx, 152(%rsp)
	movl	%eax, 160(%rsp)
	jmp	.LBB47_1
.LBB47_3:
	leaq	40(%rsp), %rdi
	callq	_ZN4core3ptr35drop_in_place$LT$std..env..Args$GT$17h27567071cfad13afE
	movq	32(%rsp), %rax
	cmpq	$1, %rax
	jne	.LBB47_5
	movl	$100, 76(%rsp)
	jmp	.LBB47_6
.LBB47_5:
	movq	32(%rsp), %rax
	cmpq	$2, %rax
	je	.LBB47_7
	jmp	.LBB47_8
.LBB47_6:
	leaq	76(%rsp), %rax
	movq	%rax, 80(%rsp)
	leaq	.L__unnamed_10(%rip), %rax
	movq	%rax, 88(%rsp)
	movq	80(%rsp), %rax
	movq	%rax, 8(%rsp)
	movq	88(%rsp), %rcx
	movq	%rcx, 16(%rsp)
	movl	(%rax), %eax
	cmpl	(%rcx), %eax
	je	.LBB47_18
	jmp	.LBB47_17
.LBB47_7:
	movl	$200, 76(%rsp)
	jmp	.LBB47_6
.LBB47_8:
	movq	32(%rsp), %rax
	cmpq	$3, %rax
	jne	.LBB47_10
	movl	$300, 76(%rsp)
	jmp	.LBB47_6
.LBB47_10:
	movq	32(%rsp), %rax
	cmpq	$4, %rax
	jne	.LBB47_12
	movl	$400, 76(%rsp)
	jmp	.LBB47_6
.LBB47_12:
	movq	32(%rsp), %rax
	cmpq	$5, %rax
	jne	.LBB47_14
	movl	$500, 76(%rsp)
	jmp	.LBB47_6
.LBB47_14:
	movq	32(%rsp), %rax
	cmpq	$6, %rax
	jne	.LBB47_16
	movl	$600, 76(%rsp)
	jmp	.LBB47_6
.LBB47_16:
	movl	$0, 76(%rsp)
	jmp	.LBB47_6
.LBB47_17:
	movq	16(%rsp), %rdx
	movq	8(%rsp), %rsi
	movb	$0, 103(%rsp)
	movq	$0, 104(%rsp)
	movzbl	103(%rsp), %edi
	leaq	.L__unnamed_11(%rip), %r8
	leaq	104(%rsp), %rcx
	callq	_ZN4core9panicking13assert_failed17h884f0f31899bb549E
.LBB47_18:
	addq	$168, %rsp
	.cfi_def_cfa_offset 8
	retq
.LBB47_19:
	.cfi_def_cfa_offset 176
.Ltmp28:
	movq	_ZN4core9panicking16panic_in_cleanup17hc8e2b17e1b6d1381E@GOTPCREL(%rip), %rax
	callq	*%rax
.LBB47_20:
	movq	152(%rsp), %rdi
	callq	_Unwind_Resume@PLT
.Lfunc_end47:
	.size	_ZN10jump_table12with_if_else17hc2cb507cd4512507E, .Lfunc_end47-_ZN10jump_table12with_if_else17hc2cb507cd4512507E
	.cfi_endproc
	.section	.gcc_except_table._ZN10jump_table12with_if_else17hc2cb507cd4512507E,"a",@progbits
	.p2align	2, 0x0

可以看到, with_if_else() 函数, 使用 if/else 语句判断 num 变量时, 使用多次跳转才能匹配到 else 分支, 跳转次数越多, CPU 执行指令的效率越低.

接下来看 with_match_short() 函数, 它内部使用了 match 表达式来匹配 num 的值, 生成的汇编代码片段如下:

// with_match_short()
	.section	.text._ZN10jump_table16with_match_short17h5f66f4b7024a913fE,"ax",@progbits
	.p2align	4, 0x90
	.type	_ZN10jump_table16with_match_short17h5f66f4b7024a913fE,@function
_ZN10jump_table16with_match_short17h5f66f4b7024a913fE:
.Lfunc_begin5:
	.cfi_startproc
	.cfi_personality 155, DW.ref.rust_eh_personality
	.cfi_lsda 27, .Lexception5
	subq	$168, %rsp
	.cfi_def_cfa_offset 176
	movq	_ZN3std3env4args17h377a659c89f76567E@GOTPCREL(%rip), %rax
	leaq	40(%rsp), %rdi
	movq	%rdi, 24(%rsp)
	callq	*%rax
	movq	24(%rsp), %rdi
.Ltmp29:
	movq	_ZN84_$LT$std..env..Args$u20$as$u20$core..iter..traits..exact_size..ExactSizeIterator$GT$3len17hfc397728d7a27a41E@GOTPCREL(%rip), %rax
	callq	*%rax
.Ltmp30:
	movq	%rax, 32(%rsp)
	jmp	.LBB48_3
.LBB48_1:
.Ltmp32:
	leaq	40(%rsp), %rdi
	callq	_ZN4core3ptr35drop_in_place$LT$std..env..Args$GT$17h27567071cfad13afE
.Ltmp33:
	jmp	.LBB48_12
.LBB48_2:
.Ltmp31:
	movq	%rax, %rcx
	movl	%edx, %eax
	movq	%rcx, 152(%rsp)
	movl	%eax, 160(%rsp)
	jmp	.LBB48_1
.LBB48_3:
	leaq	40(%rsp), %rdi
	callq	_ZN4core3ptr35drop_in_place$LT$std..env..Args$GT$17h27567071cfad13afE
	movq	32(%rsp), %rax
	subq	$1, %rax
	je	.LBB48_5
	jmp	.LBB48_13
.LBB48_13:
	movq	32(%rsp), %rax
	subq	$2, %rax
	je	.LBB48_6
	jmp	.LBB48_14
.LBB48_14:
	movq	32(%rsp), %rax
	subq	$3, %rax
	je	.LBB48_7
	jmp	.LBB48_4
.LBB48_4:
	movl	$0, 76(%rsp)
	jmp	.LBB48_8
.LBB48_5:
	movl	$100, 76(%rsp)
	jmp	.LBB48_8
.LBB48_6:
	movl	$200, 76(%rsp)
	jmp	.LBB48_8
.LBB48_7:
	movl	$300, 76(%rsp)
.LBB48_8:
	leaq	76(%rsp), %rax
	movq	%rax, 80(%rsp)
	leaq	.L__unnamed_10(%rip), %rax
	movq	%rax, 88(%rsp)
	movq	80(%rsp), %rax
	movq	%rax, 8(%rsp)
	movq	88(%rsp), %rcx
	movq	%rcx, 16(%rsp)
	movl	(%rax), %eax
	cmpl	(%rcx), %eax
	je	.LBB48_10
	movq	16(%rsp), %rdx
	movq	8(%rsp), %rsi
	movb	$0, 103(%rsp)
	movq	$0, 104(%rsp)
	movzbl	103(%rsp), %edi
	leaq	.L__unnamed_12(%rip), %r8
	leaq	104(%rsp), %rcx
	callq	_ZN4core9panicking13assert_failed17h884f0f31899bb549E
.LBB48_10:
	addq	$168, %rsp
	.cfi_def_cfa_offset 8
	retq
.LBB48_11:
	.cfi_def_cfa_offset 176
.Ltmp34:
	movq	_ZN4core9panicking16panic_in_cleanup17hc8e2b17e1b6d1381E@GOTPCREL(%rip), %rax
	callq	*%rax
.LBB48_12:
	movq	152(%rsp), %rdi
	callq	_Unwind_Resume@PLT
.Lfunc_end48:
	.size	_ZN10jump_table16with_match_short17h5f66f4b7024a913fE, .Lfunc_end48-_ZN10jump_table16with_match_short17h5f66f4b7024a913fE
	.cfi_endproc
	.section	.gcc_except_table._ZN10jump_table16with_match_short17h5f66f4b7024a913fE,"a",@progbits
	.p2align	2, 0x0

从上面的汇编代码可以看到, 汇编器并没有生成跳转表, 也都只是一些条件判断语句, 需要多次判断用跳转才能到达最后一个分支. 但这部分代码要比 with_if_else() 的汇编代码更简洁, 执行效率也会更高.

只有分支语句达到某个限制时, 汇编器才会生成跳转表; 在 x86_64 上, 这个分支个数是4. 接下来看 with_match_long() 函数的汇编代码, 它就被构造出了跳转表:

// with_match_long()
	.section	.text._ZN10jump_table15with_match_long17hf93574d4242b2d97E,"ax",@progbits
	.p2align	4, 0x90
	.type	_ZN10jump_table15with_match_long17hf93574d4242b2d97E,@function
_ZN10jump_table15with_match_long17hf93574d4242b2d97E:
.Lfunc_begin6:
	.cfi_startproc
	.cfi_personality 155, DW.ref.rust_eh_personality
	.cfi_lsda 27, .Lexception6
	subq	$168, %rsp
	.cfi_def_cfa_offset 176
	movq	_ZN3std3env4args17h377a659c89f76567E@GOTPCREL(%rip), %rax
	leaq	40(%rsp), %rdi
	movq	%rdi, 24(%rsp)
	callq	*%rax
	movq	24(%rsp), %rdi
.Ltmp35:
	movq	_ZN84_$LT$std..env..Args$u20$as$u20$core..iter..traits..exact_size..ExactSizeIterator$GT$3len17hfc397728d7a27a41E@GOTPCREL(%rip), %rax
	callq	*%rax
.Ltmp36:
	movq	%rax, 32(%rsp)
	jmp	.LBB49_3
.LBB49_1:
.Ltmp38:
	leaq	40(%rsp), %rdi
	callq	_ZN4core3ptr35drop_in_place$LT$std..env..Args$GT$17h27567071cfad13afE
.Ltmp39:
	jmp	.LBB49_13
.LBB49_2:
.Ltmp37:
	movq	%rax, %rcx
	movl	%edx, %eax
	movq	%rcx, 152(%rsp)
	movl	%eax, 160(%rsp)
	jmp	.LBB49_1
.LBB49_3:
	leaq	40(%rsp), %rdi
	callq	_ZN4core3ptr35drop_in_place$LT$std..env..Args$GT$17h27567071cfad13afE
	movq	32(%rsp), %rax
	decq	%rax
	movq	%rax, 16(%rsp)
	subq	$3, %rax
	ja	.LBB49_4
	movq	16(%rsp), %rax
	leaq	.LJTI49_0(%rip), %rcx
	movslq	(%rcx,%rax,4), %rax
	addq	%rcx, %rax
	jmpq	*%rax
.LBB49_4:
	movl	$0, 76(%rsp)
	jmp	.LBB49_9
.LBB49_5:
	movl	$100, 76(%rsp)
	jmp	.LBB49_9
.LBB49_6:
	movl	$200, 76(%rsp)
	jmp	.LBB49_9
.LBB49_7:
	movl	$300, 76(%rsp)
	jmp	.LBB49_9
.LBB49_8:
	movl	$400, 76(%rsp)
.LBB49_9:
	leaq	76(%rsp), %rax
	movq	%rax, 80(%rsp)
	leaq	.L__unnamed_10(%rip), %rax
	movq	%rax, 88(%rsp)
	movq	80(%rsp), %rax
	movq	%rax, (%rsp)
	movq	88(%rsp), %rcx
	movq	%rcx, 8(%rsp)
	movl	(%rax), %eax
	cmpl	(%rcx), %eax
	je	.LBB49_11
	movq	8(%rsp), %rdx
	movq	(%rsp), %rsi
	movb	$0, 103(%rsp)
	movq	$0, 104(%rsp)
	movzbl	103(%rsp), %edi
	leaq	.L__unnamed_13(%rip), %r8
	leaq	104(%rsp), %rcx
	callq	_ZN4core9panicking13assert_failed17h884f0f31899bb549E
.LBB49_11:
	addq	$168, %rsp
	.cfi_def_cfa_offset 8
	retq
.LBB49_12:
	.cfi_def_cfa_offset 176
.Ltmp40:
	movq	_ZN4core9panicking16panic_in_cleanup17hc8e2b17e1b6d1381E@GOTPCREL(%rip), %rax
	callq	*%rax
.LBB49_13:
	movq	152(%rsp), %rdi
	callq	_Unwind_Resume@PLT
.Lfunc_end49:
	.size	_ZN10jump_table15with_match_long17hf93574d4242b2d97E, .Lfunc_end49-_ZN10jump_table15with_match_long17hf93574d4242b2d97E
	.cfi_endproc

// 定义的跳转表
	.section	.rodata._ZN10jump_table15with_match_long17hf93574d4242b2d97E,"a",@progbits
	.p2align	2, 0x0
.LJTI49_0:
	.long	.LBB49_5-.LJTI49_0
	.long	.LBB49_6-.LJTI49_0
	.long	.LBB49_7-.LJTI49_0
	.long	.LBB49_8-.LJTI49_0
	.section	.gcc_except_table._ZN10jump_table15with_match_long17hf93574d4242b2d97E,"a",@progbits
	.p2align	2, 0x0

下图展示了跳转表的基本结构, 与 if/else 语句相比, 分支越多, match 表达式的执行效率相对越高.

jump table

另外, 在 aarch64 平台编译器也有类似的行为, 下面的汇编代码片段展示了 with_match_long() 函数:


	.section	.text._ZN10jump_table15with_match_long17h4ee0bf410c273a3cE,"ax",@progbits
	.p2align	2
	.type	_ZN10jump_table15with_match_long17h4ee0bf410c273a3cE,@function
_ZN10jump_table15with_match_long17h4ee0bf410c273a3cE:
.Lfunc_begin7:
	.cfi_startproc
	.cfi_personality 156, DW.ref.rust_eh_personality
	.cfi_lsda 28, .Lexception7
	sub	sp, sp, #192
	.cfi_def_cfa_offset 192
	str	x30, [sp, #176]
	.cfi_offset w30, -16
	.cfi_remember_state
	add	x8, sp, #48
	str	x8, [sp, #32]
	bl	_ZN3std3env4args17hf417fc576c685b45E
	ldr	x0, [sp, #32]
.Ltmp41:
	bl	_ZN84_$LT$std..env..Args$u20$as$u20$core..iter..traits..exact_size..ExactSizeIterator$GT$3len17ha56aa4d3a3bd219eE
	str	x0, [sp, #40]
.Ltmp42:
	b	.LBB43_3
.LBB43_1:
.Ltmp44:
	add	x0, sp, #48
	bl	_ZN4core3ptr35drop_in_place$LT$std..env..Args$GT$17hdc3a512020133b55E
.Ltmp45:
	b	.LBB43_14
.LBB43_2:
.Ltmp43:
	str	x0, [sp, #160]
	mov	w8, w1
	str	w8, [sp, #168]
	b	.LBB43_1
.LBB43_3:
	add	x0, sp, #48
	bl	_ZN4core3ptr35drop_in_place$LT$std..env..Args$GT$17hdc3a512020133b55E
	ldr	x8, [sp, #40]
	subs	x8, x8, #1
	str	x8, [sp, #24]
	subs	x8, x8, #3
	cset	w8, hi
	tbnz	w8, #0, .LBB43_5
	ldr	x11, [sp, #24]
	adrp	x10, .LJTI43_0
	add	x10, x10, :lo12:.LJTI43_0
.Ltmp47:
	adr	x8, .Ltmp47
	ldrsw	x9, [x10, x11, lsl #2]
	add	x8, x8, x9
	br	x8
.LBB43_5:
	str	wzr, [sp, #84]
	b	.LBB43_10
.LBB43_6:
	mov	w8, #100
	str	w8, [sp, #84]
	b	.LBB43_10
.LBB43_7:
	mov	w8, #200
	str	w8, [sp, #84]
	b	.LBB43_10
.LBB43_8:
	mov	w8, #300
	str	w8, [sp, #84]
	b	.LBB43_10
.LBB43_9:
	mov	w8, #400
	str	w8, [sp, #84]
	b	.LBB43_10
.LBB43_10:
	add	x8, sp, #84
	str	x8, [sp, #88]
	adrp	x8, .L__unnamed_10
	add	x8, x8, :lo12:.L__unnamed_10
	str	x8, [sp, #96]
	ldr	x8, [sp, #88]
	str	x8, [sp, #8]
	ldr	x9, [sp, #96]
	str	x9, [sp, #16]
	ldr	w8, [x8]
	ldr	w9, [x9]
	subs	w8, w8, w9
	cset	w8, ne
	tbnz	w8, #0, .LBB43_12
	b	.LBB43_11
.LBB43_11:
	ldr	x30, [sp, #176]
	add	sp, sp, #192
	.cfi_def_cfa_offset 0
	.cfi_restore w30
	ret
.LBB43_12:
	.cfi_restore_state
	ldr	x2, [sp, #16]
	ldr	x1, [sp, #8]
	strb	wzr, [sp, #111]
	add	x3, sp, #112
	str	xzr, [sp, #112]
	ldrb	w0, [sp, #111]
	adrp	x4, .L__unnamed_13
	add	x4, x4, :lo12:.L__unnamed_13
	bl	_ZN4core9panicking13assert_failed17h10918a4a4d6d5d6fE
	brk	#0x1
.LBB43_13:
.Ltmp46:
	bl	_ZN4core9panicking19panic_cannot_unwind17hf82fd8d1e9cc4d07E
	brk	#0x1
.LBB43_14:
	ldr	x0, [sp, #160]
	bl	_Unwind_Resume
	brk	#0x1
.Lfunc_end43:
	.size	_ZN10jump_table15with_match_long17h4ee0bf410c273a3cE, .Lfunc_end43-_ZN10jump_table15with_match_long17h4ee0bf410c273a3cE
	.cfi_endproc

// 定义的跳转表
	.section	.rodata._ZN10jump_table15with_match_long17h4ee0bf410c273a3cE,"a",@progbits
	.p2align	2, 0x0
.LJTI43_0:
	.word	.LBB43_6-.Ltmp47
	.word	.LBB43_7-.Ltmp47
	.word	.LBB43_8-.Ltmp47
	.word	.LBB43_9-.Ltmp47
	.section	.gcc_except_table._ZN10jump_table15with_match_long17h4ee0bf410c273a3cE,"a",@progbits
	.p2align	2, 0x0

参考