Fork me on GitHub

代码优化、内存屏障和原子操作-介绍

1.内存屏障

内存屏障的作用:内存屏障主要解决的问题是编译器的优化和CPU的乱序执行

基本的内存屏障有4种:

  • 1.写屏障
  • 2.数据依赖屏障(常与写屏障成对出现)
  • 3.读屏障
  • 4.通用内存屏障

1.1 代码优化

编译器会对c语言进行代码优化,生成的汇编语言可能和c语言执行顺序不一样。在需要严格按照c语言顺序执行时,需要显示的告诉编译器不需要优化。在linux下通过barrier()宏完成,它依靠volatile关键字和memory关键字:

/* Optimization barrier ,The volatile is due to gcc bugs*/
#define barrier() __asm__ __volatile__("":::"memory")
  • volatile: 告诉编译barrier()周围的指令不要被优化;
  • memory:是告诉编译器汇编代码会使内存里面的值更改,编译器应使用内存里的新值而非寄存器里保存的老值。

1.2.cpu乱序访问

CPU执行会通过乱序以提高性能。汇编里的指令不一定是按照我们看到的顺序执行的。linux中通过mb()系列宏来保证执行的顺序。具体做法是通过mfence/lfence指令(它们是奔4后引进的,早期x86没有)以及x86指令中带有串行特性的指令(这样的指令很多,例如linux中实现时用到的lock指令,I/O指令,操作控制寄存器、系统寄存器、调试寄存器的指令、iret指令等等)。简单的说,如果在程序某处插入了mb()/rmb()/wmb()宏,则宏之前的程序保证比宏之后的程序先执行,从而实现串行化。wmb的实现和barrier()类似,是因为在x86平台上,写内存的操作不会被乱序执行。

2.原子操作

原子操作在单核,单线程/无中断,且编译器不优化的情况下是确定的,是按照C/C++代码顺序执行的,所以不存在异步问题

2.1. 原子整数操作

针对整数的原子操作只能对atomic_t类型的数据处理。这里没有使用C语言的int类型,主要是因为:

  1. 让原子函数只接受atomic_t类型操作数,可以确保原子操作只与这种特殊类型数据一起使用.
  2. 使用atomic_t类型确保编译器不对相应的值进行访问优化. (原理为: 变量被volatile修饰了)
  3. 使用atomic_t类型可以屏蔽不同体系结构上的数据类型的差异。尽管Linux支持的所有机器上的整型数据都是32位,但是使用atomic_t的代码只能将该类型的数据当作24位来使用。这个限制完全是因为在SPARC体系结构上,原子操作的实现不同于其它体系结构:32位int类型的低8位嵌入了一个锁,因为SPARC体系结构对原子操作缺乏指令级的支持,所以只能利用该锁来避免对原子类型数据的并发访问。

原子整数操作最常见的用途就是实现计数器。原子整数操作列表在中定义。原子操作通常是内敛函数,往往通过内嵌汇编指令来实现。如果某个函数本来就是原子的,那么它往往会被定义成一个宏。

atomic_t cnt;
atomic_set(&cnt, 2);
atomic_add(4, &cnt);
atomic_inc(cnt);

2.2 原子位操作

原子位操作定义在文件中。令人感到奇怪的是位操作函数是对普通的内存地址进行操作的。原子位操作在多数情况下是对一个字节长的内存(注1)访问,因而位号该位于0-31之间(在64位机器上是0-63之间),但是对位号的范围没有限制。

注1:操作系统可以确保,在同一时刻,只有一个CPU的一个进程访问特定的某个字节,再加上单核中的原子性(基本数据类型的简单操作),所以单字节内存的简单操作是具有天生的多核原子性的。

编写内核代码,把要操作的数据的指针给操作函数,就可以进行位操作了:

unsigned long var = 0;
set_bit(0, &var); /* set the 0th bit */
set_bit(1, &var);           /*set the 1th bit*/
clear_bit(1, &var);         /*clear the 1th bit*/
change_bit(0, &var);        /*change the 1th bit*/
posted @ 2022-01-23 16:35  BabyMelvin  阅读(269)  评论(0编辑  收藏  举报