GCC __builtin_expect与kernel指令序列优化

本文介绍了一种使用C语言判断整数是否为回文数的方法,并深入探讨了__builtin_expect宏在编译优化中的作用。通过对代码的逐行分析及对应的汇编指令解释,展示了如何高效地实现该功能。
例题描述

例题描述:通过C语言识别一个int型数据在十进制下是否为回文数字。不能有额外的字符串空间开销。
如:2156512是回文数,而21565不是回文数。

问题分析:
1. 当这个数字是负数的时候,肯定不是回文数
2. 可以将这个数翻转,判断翻转后是否相同

C语言代码演示:
/*************************************************************************
	> File Name: isPalindrome.cpp
	> Author: ChenXiansen 
	> Mail: 1494089474@qq.com 
	> Created Time: Wed 11 Nov 2020 09:49:43 AM CST
 ************************************************************************/

#include <stdio.h>

bool isPalindrome(int x, int n) {
    if (__builtin_expect(!!(x < 0), 0)) return false;
    int y = 0, z = x;
    while (x) {
        y = y * n + x % n;
        x /= n;
    }
    return z == y;
}

int main() {
    int n, Cov = 10;
    scanf("%d", &n);
    if (isPalindrome(n, Cov)) {
        printf("Num: %d in Cov: %d is a reverse num!\n", n, Cov);
    } else {
        printf("NO! It's not a reverse num!\n");
    }
    return 0;
}

代码分析

isPalindrome函数中,使用了if (__builtin_expect(!!(x < 0), 0)) return false; 这条语句。
下面是对__builtin_expect宏的表述:

GCC提供了__builtin_expect宏,作为编译分支时候的暗示。用法是__builtin_expect(var, expected_value),也就是说,告诉编译器var这个变量的值比较可能是什么。在kernel中这个宏被用在likely和unlikely这两个宏定义中:

#define likely(x) __builtin_expect(!!(x), 1)		//x很可能成立
#define unlikely(x) __builtin_expect(!!(x), 0) 		//x很可能不成立

从现代的处理器架构说起。相信大家都知道流水线技术,就是CPU可以在统一个时钟周期内同时执行多条指令,当前指令尚未执行完毕,实际上就已经开始处理后面的指令了。然而当处理器遇到分支的时候,就无法判断即将执行的是哪个分支,流水线优化就受到了限制。

后来,随着处理器技术的发展,处理器开始直接预取分支后面的指令,如果发现分支预判错误,则抛弃之前的执行结果,重新转入正确的分支继续执行。 更加现代的处理器甚至能够预取更多后面的指令,对于不依赖之前执行结果的指令都可以按照一定的规则预先执行得到结果。


汇编代码
1. 初始代码汇编

接下来在终端运行gcc -S isPalindrome.cpp,查看汇编代码中的isPalindrome()函数:

.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -20(%rbp)
	movl	%esi, -24(%rbp)
	movl	-20(%rbp), %eax
	shrl	$31, %eax
	movzbl	%al, %eax
	testq	%rax, %rax
	je	.L2
	movl	$0, %eax
	jmp	.L3
.L2:
	movl	$0, -8(%rbp)
	movl	-20(%rbp), %eax
	movl	%eax, -4(%rbp)
.L5:
	cmpl	$0, -20(%rbp)
	je	.L4
	movl	-8(%rbp), %eax
	imull	-24(%rbp), %eax
	movl	%eax, %ecx
	movl	-20(%rbp), %eax
	cltd
	idivl	-24(%rbp)
	movl	%edx, %eax
	addl	%ecx, %eax
	movl	%eax, -8(%rbp)
	movl	-20(%rbp), %eax
	cltd
	idivl	-24(%rbp)
	movl	%eax, -20(%rbp)
	jmp	.L5
.L4:
	movl	-4(%rbp), %eax
	cmpl	-8(%rbp), %eax
	sete	%al
.L3:
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	_Z12isPalindromeii, .-_Z12isPalindromeii
	.section	.rodata

对.LFB0段的释义:

  1. main()函数中的变量n的值存入了edi寄存器,作为bool isPalindrome(int x, int n)的形参x。
  2. main()函数中的变量Cov的值存入esi寄存器,作为bool isPalindrome(int x, int n)的形参n。
  3. movl %edi, -20(%rbp)
    movl %esi, -24(%rbp)
    将这两个参数压入函数栈。
  4. movl -20(%rbp), %eax :将存在于-20(%rbp)中x的值,存入eax寄存器。
  5. 执行以下汇编指令,这段汇编指令由if (__builtin_expect(!!(x < 0), 0)) return false;转换得到:
    	shrl	$31, %eax
    	movzbl	%al, %eax
    	testq	%rax, %rax
    	je	.L2
    	movl	$0, %eax
    	jmp	.L3
    
    可以看到:在执行前三行语句后(这三行代码现在没有弄清楚具体含义,待后续查阅相关资料),下一步是判断分支:汇编代码是je .L2
    1. 如果满足testq %rax, %rax条件,下一步是执行C程序中的:
      int y = 0, z = x;
      
      对应在汇编.L2函数中的:
      	.L2:
      		movl	$0, -8(%rbp)
      		movl	-20(%rbp), %eax
      		movl	%eax, -4(%rbp)
      
    2. 如果不满足条件,将返回值0放入寄存器eax中,执行.L3 汇编函数部分的return操作。
修改程序后汇编:

现在将

if (__builtin_expect(!!(x < 0), 0)) return false;

这部分C代码转换成

if (x < 0) return false;

观察执行预处理-编译-汇编过程后,查看汇编代码中的isPalindrome()函数。(由于.L2之后的部分完全相同,因此没有在博客中显示)

.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -20(%rbp)
	movl	%esi, -24(%rbp)
	cmpl	$0, -20(%rbp)
	jns	.L2
	movl	$0, %eax
	jmp	.L3

可以看到:将函数参数全部压栈后,进行的分支判断:

	movl	%edi, -20(%rbp)
	movl	%esi, -24(%rbp)
	cmpl	$0, -20(%rbp)
	jns	.L2
	movl	$0, %eax
	jmp	.L3

cmpl $0, -20(%rbp):如果 bool isPalindrome(int x, int n)函数中 参数x的值不满足 x < 0,则执行.L2之后的汇编函数,反之直接return 0.


参考资料:
https://zhuanlan.zhihu.com/p/27339191
http://deltamaster.is-programmer.com/posts/37285.html

附录:完整汇编程序代码:
	.file	"isPalindrome.cpp"
	.text
	.globl	_Z12isPalindromeii
	.type	_Z12isPalindromeii, @function
_Z12isPalindromeii:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -20(%rbp)
	movl	%esi, -24(%rbp)
	movl	-20(%rbp), %eax
	shrl	$31, %eax
	movzbl	%al, %eax
	testq	%rax, %rax
	je	.L2
	movl	$0, %eax
	jmp	.L3
.L2:
	movl	$0, -8(%rbp)
	movl	-20(%rbp), %eax
	movl	%eax, -4(%rbp)
.L5:
	cmpl	$0, -20(%rbp)
	je	.L4
	movl	-8(%rbp), %eax
	imull	-24(%rbp), %eax
	movl	%eax, %ecx
	movl	-20(%rbp), %eax
	cltd
	idivl	-24(%rbp)
	movl	%edx, %eax
	addl	%ecx, %eax
	movl	%eax, -8(%rbp)
	movl	-20(%rbp), %eax
	cltd
	idivl	-24(%rbp)
	movl	%eax, -20(%rbp)
	jmp	.L5
.L4:
	movl	-4(%rbp), %eax
	cmpl	-8(%rbp), %eax
	sete	%al
.L3:
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	_Z12isPalindromeii, .-_Z12isPalindromeii
	.section	.rodata
.LC0:
	.string	"%d"
	.align 8
.LC1:
	.string	"Num: %d in Cov: %d is a reverse num!\n"
.LC2:
	.string	"NO! It's not a reverse num!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movq	%fs:40, %rax
	movq	%rax, -8(%rbp)
	xorl	%eax, %eax
	movl	$10, -12(%rbp)
	leaq	-16(%rbp), %rax
	movq	%rax, %rsi
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax
	call	__isoc99_scanf@PLT
	movl	-16(%rbp), %eax
	movl	-12(%rbp), %edx
	movl	%edx, %esi
	movl	%eax, %edi
	call	_Z12isPalindromeii
	testb	%al, %al
	je	.L7
	movl	-16(%rbp), %eax
	movl	-12(%rbp), %edx
	movl	%eax, %esi
	leaq	.LC1(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	jmp	.L8
.L7:
	leaq	.LC2(%rip), %rdi
	call	puts@PLT
.L8:
	movl	$0, %eax
	movq	-8(%rbp), %rcx
	xorq	%fs:40, %rcx
	je	.L10
	call	__stack_chk_fail@PLT
.L10:
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	 1f - 0f
	.long	 4f - 1f
	.long	 5
0:
	.string	 "GNU"
1:
	.align 8
	.long	 0xc0000002
	.long	 3f - 2f
2:
	.long	 0x3
3:
	.align 8
4:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值