优化屏障(Optimization barrier)第二讲

1. gcc编译的大致过程

可以看到,gcc优化主要分两大部分:Tree优化RTL(Register Transfer Language)优化

前文所说的指令调度(Instruction scheduling)即为RTL优化的一部分。

2. 从RTL指令调度出发,追寻Optimization barrier的踪迹

还是从实验出发,实验代码如下:

1
2
3
4
5
6
7
8
volatile int ready;
int message[100];
 
void cmb ( int i) {
     message[i/10] = 42;
     __asm__ __volatile__ ( "" ::: "memory" );
     ready = 1;
}

老的gcc可以-dr输出过程中的rtl文件,现在这个版本-dr好像不认识,所以只好先用-da输出全部信息,即gcc -da basic_cmb.c
执行后发现生成一大堆basic_cmb.c.XXXr.YYY文件,我们找到basic_cmb.c.128r.expand即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
;;
;; Full RTL generated for this function: // ;; 表注释
;;
(note 1 0 3 NOTE_INSN_DELETED)   // 调试和说明信息,不用关心
 
;; Start of basic block ( 0) -> 2
;; Pred edge  ENTRY (fallthru)
(note 3 1 2 2 [bb 2] NOTE_INSN_BASIC_BLOCK) // 调试和说明信息,不用关心
 
(note 2 3 4 2 NOTE_INSN_FUNCTION_BEG) // 调试和说明信息,不用关心
;; End of basic block 2 -> ( 3)
 
;; Succ edge  3 [100.0%]  (fallthru)
 
;; Start of basic block ( 2) -> 3
;; Pred edge  2 [100.0%]  (fallthru)
(note 4 2 5 3 [bb 3] NOTE_INSN_BASIC_BLOCK) // 调试和说明信息,不用关心
 
(insn 5 4 6 3 basic_cmb.c:5 (set (reg:SI 59) // virtual-incoming-args表示入参,即reg59=入参
         (mem/c/i:SI (reg/f:SI 53 virtual -incoming-args) [0 i+0 S4 A32])) -1 (nil))
 
(insn 6 5 7 3 basic_cmb.c:5 (set (reg:SI 61) // reg61=1717986919
         (const_int 1717986919 [0x66666667])) -1 (nil))
 
(insn 7 6 8 3 basic_cmb.c:5 (parallel [ // ((reg59 * reg61) << 32)取高32位,
             (set (reg:SI 60) // 再赋值给reg60
                (truncate:SI (lshiftrt:DI (mult:DI (sign_extend:DI (reg:SI 59))
                                                   (sign_extend:DI (reg:SI 61)))
                                                   (const_int 32 [0x20])))) // 即取reg59和reg61乘积的高32位,等效于imull指令             
                             (clobber (scratch:SI))            
                             (clobber (reg:CC 17 flags))]) -1 (nil))
 
(insn 8 7 9 3 basic_cmb.c:5 (parallel [    // reg62 = reg60 >> 2
             (set (reg:SI 62)
                 (ashiftrt:SI (reg:SI 60)
                     (const_int 2 [0x2])))
             (clobber (reg:CC 17 flags))
         ]) -1 (nil))
 
(insn 9 8 10 3 basic_cmb.c:5 (parallel [  // reg63 = reg59 >> 31
             (set (reg:SI 63)
                 (ashiftrt:SI (reg:SI 59)
                     (const_int 31 [0x1f])))
             (clobber (reg:CC 17 flags))
         ]) -1 (nil))
 
(insn 10 9 11 3 basic_cmb.c:5 (parallel [
             (set (reg:SI 58 [ D.1250 ])  // reg58 = reg62 - reg63
                 (minus:SI (reg:SI 62)
                     (reg:SI 63)))
             (clobber (reg:CC 17 flags))
         ]) -1 (expr_list:REG_EQUAL ( div :SI (reg:SI 59) // 表示reg59/10由上面的
             (const_int 10 [0xa]))        //reg58 = reg62 -reg63替代
         (nil)))  // 即它们是等效的,从这个点也可以看出编译器对除法做出的优化
 
(insn 11 10 12 3 basic_cmb.c:5 (set (mem/s/j:SI (plus:SI (mult:SI (reg:SI 58 [ D.1250 ]) // reg58 * 4 + message = 42
                     (const_int 4 [0x4]))
                 (symbol_ref:SI ( "message" ) )) [0 message S4 A32]) //其实这个已经和movl $42, message(,%eax,4)相对应了
         (const_int 42 [0x2a])) -1 (nil))
 
(insn 12 11 13 3 basic_cmb.c:6 (parallel [  // __asm__ __volatile__ ("" ::: "memory");出现在这里
             (asm_operands/v ( "" ) ( "" ) 0 []
                  [] 736)
             (clobber (reg:QI 18 fpsr))
             (clobber (reg:QI 17 flags))
             (clobber (mem:BLK (scratch) [0 A8]))
         ]) -1 (nil))
 
(insn 13 12 18 3 basic_cmb.c:7 (set (mem/v/c/i:SI (symbol_ref:SI ( "ready" ) ) [0 ready+0 S4 A32]) // movl  $1, ready
         (const_int 1 [0x1])) -1 (nil))
;; End of basic block 3 -> ( 4)
 
;; Succ edge  4 (fallthru)
 
;; Start of basic block ( 3) -> 4
;; Pred edge  3 (fallthru)
(note 18 13 15 4 [bb 4] NOTE_INSN_BASIC_BLOCK) // 调试和说明信息,不用关心
 
(jump_insn 15 18 16 4 basic_cmb.c:8 (set (pc) // goto label 17
         (label_ref 17)) -1 (nil))
;; End of basic block 4 -> ( 6)
 
;; Succ edge  6
 
(barrier 16 15 14)    // 栅栏
 
;; Start of basic block () -> 5
(code_label 14 16 19 5 1 "" [0 uses])  // label 14
 
(note 19 14 17 5 [bb 5] NOTE_INSN_BASIC_BLOCK) // 调试和说明信息,不用关心
;; End of basic block 5 -> ( 6)
 
;; Succ edge  6 (fallthru)
 
;; Start of basic block ( 4 5) -> 6
;; Pred edge  4
;; Pred edge  5 (fallthru)
(code_label 17 19 20 6 2 "" [1 uses])  // label 17
 
(note 20 17 0 6 [bb 6] NOTE_INSN_BASIC_BLOCK) // 调试和说明信息,不用关心
;; End of basic block 6 -> ( 1)
 
;; Succ edge  EXIT [100.0%]  (fallthru)

有关insns的介绍在http://www.lingcc.com/gccint/Insns.html#Insns
rtl文件有多个insn组成,insns表示的灵感即来自于LISP,当然,肯定不是规范的LISP语法,但至少”;;”作为注释这一点还是相同的,这里大致介绍一下各个insns的作用:

  • note用于表示额外的调试和说明信息
    如:(note 1 0 3 NOTE_INSN_DELETED)
    1 0 3分别表示当前insns的id为1,上一条insns的id为0,下一条为3
    NOTE_INSN_DELETED这样的注解被完全忽略掉。编译器的一些过程会通过将insn修改成这种类型的注解,来删除insn。
    本文并不是为了讲解RTL的,所以这里只是简单提一下,既然note是调试相关的信息,我们就暂时不关心它了。
  • insn
    表达式代码insn用于不进行跳转和函数调用的指令。sequence表达式总是包含在表达式代码为insn的insn中,即使它们中的一个insn是跳转或者函数调用。
    如:(insn 13 12 18 3 basic_cmb.c:7 (set (mem/v/c/i:SI (symbol_ref:SI (“ready”) ) [0 ready+0 S4 A32]) (const_int 1 [0x1])) -1 (nil))
    13 12 18和note一样,分别表示当前id,前一条id和后一条id;后面的3则表示所属的基本块id;
    basic_cmb.c:7表示为当前insns对应basic_cmb.c文件中的第7行;后面一大串内容要解释起来也花上很大篇幅,可以参考http://www.lingcc.com/gccint/;不过应该还是比较容易看懂这句在干嘛:set ready const_int 1,自然对应于ready=1的语句。
  • barrier
    栅栏被放在指令流中,控制无法经过的地方。它们被放在无条件跳转指令的后面,表示跳转是无条件的,以及对volatile函数的调用之后,表示不会返回(例如,exit)。除了三个标准的域以外,不包含其它信息。
  • code_label即表示标签label
  • jump_insn
    表达式代码jump_insn用于可能执行跳转(或者,更一般的讲,指令中可能包含了label_ref表达式,并用其来设置pc)的指令。如果有一条从当前函数返回的指令,则其被记录为jump_insn。
    如:(jump_insn 15 18 16 4 basic_cmb.c:8 (set (pc) (label_ref 17)) -1 (nil));设置pc寄存器为17号label引用,即相当于goto label 17

从rtl中可以看到,每个insns通过当前id,前一条id和后一条id可以形成一条链,把所有insns链起来未免过长,由于我们的主要研究对象是Optimization barrier,所以就从insns11开始:

从图中可以看到,在RTL这一层,我们的Optimization barrier还是存在的,所以可以大胆的猜测是在RTL优化的过程中它贡献了自己的力量。

RTL(Register Transfer Language)优化指令调度相关说明可以看到gcc实现部分相关代码:haifa-sched.c, sched-deps.c, sched-ebb.c, sched-rgn.c和sched-vis.c。

先不用追根溯源,gcc大庞大,反而会摸不着头脑的,Optimization barrier对应的insns为:

1
2
3
4
5
6
7
(insn 12 11 13 3 basic_cmb.c:6 (parallel [
             (asm_operands/v ( "" ) ( "" ) 0 []
                  [] 736)
             (clobber (reg:QI 18 fpsr))
             (clobber (reg:QI 17 flags))
             (clobber (mem:BLK (scratch) [0 A8]))
         ]) -1 (nil))

可以先参照http://www.lingcc.com/gccint/Side-Effects.html#Side-Effects
http://www.lingcc.com/gccint/Flags.html#Flags
不过,还是针对这条insn做一个较详细的解释吧:

  • asm_operands:表这是一条asm操作指令
  • /v:表含有volatile修饰符,所以asm_operands/v就和__asm__ __volatile__相对应
  • reg:表寄存器
  • QI:Quarter-Integer模式,表示一个一字节的整数
  • fpsr:float point state register,表浮点状态寄存器,所以reg:QI 18 fpsr表这是一个字节大小的浮点状态寄存器,引用是18
  • flags:通用状态寄存器,所以reg:QI 17 flags表这是一个字节大小的通用状态寄存器,引用是17
  • mem:表内存
  • BLK:“Block”模式,表示其它模式都不适用的聚合值
  • (clobber x)
    表示一个不可预期的存储或者可能的存储,将不可描述的值存储到x,其必须为一个reg,scratch, parallel 或者 mem表达式。
    可以用在字符串指令中,将标准的值存储到特定的硬件寄存器中。不需要去描述被存储的值,只用来告诉编译器寄存器被修改了,以免其尝试在字符串指令中保持数据。如果x为(mem:BLK (const_int 0))或者(mem:BLK (scratch)),则意味着所有的内存位置必须假设被破坏。

所以综合来讲,这条指令的意思是:volatile修饰的asm操作指令,并告知编译器这条指令会改变状态寄存器和内存,正好应对

1
__asm__ __volatile__ ( "" ::: "memory" )

针对这一点,我们还可以做一个实验:

1
2
3
4
5
6
7
8
9
10
11
#include
int main( void )
{
         int foo = 10, bar = 15;
         __asm__ __volatile__( "addl  %%ebx,%%eax"
                              : "=a" (foo)
                              : "a" (foo), "b" (bar)
                              );
         printf ( "foo+bar=%d\n" , foo);
         return 0;
}

asm语句对应的insn为(如对asm不熟悉,可以参照http://ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html):

1
2
3
4
5
6
7
8
9
10
11
12
13
(insn 9 8 10 asm.c:5 (parallel [
             (set (reg:SI 61)
                 (asm_operands/v:SI ( "addl  %%ebx,%%eax" ) ( "=a" ) 0 [
                         (reg:SI 62)
                         (reg:SI 63)
                     ]
                      [
                         (asm_input:SI ( "a" ) 0)
                         (asm_input:SI ( "b" ) 0)
                     ] 596358))
             (clobber (reg:QI 18 fpsr))
             (clobber (reg:QI 17 flags))
         ]) -1 (nil))

这条insn更为详细,因为未加”memory”所以,只默认告知编译器状态寄存器的改变。
我们再做一个实验来应证这一点:

1
2
3
4
5
6
7
8
volatile int ready;
int message[100];
 
void cmb ( int i) {
     message[i/10] = 42;
     __asm__ __volatile__ ( "" ::: "cc" );
     ready = 1;
}

将”memory”修改为”cc”后,发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.globl cmb
     .type   cmb, @function
cmb:
     pushl   %ebp
     movl    %esp, %ebp
     movl    8(%ebp), %ecx
     movl    $1717986919, %edx
     movl    $1, ready
     movl    %ecx, %eax
     imull   %edx
     movl    %ecx, %eax
     sarl    $31, %eax
     sarl    $2, %edx
     movl    %edx, %ecx
     subl    %eax, %ecx
     movl    %ecx, %eax
     movl    $42, message(,%eax,4)
     popl    %ebp
     ret

这条语句是不起作用的,所以,是这个”memory”起到了关键作用,下面我们就重点关注”memory”产生的影响。

<think>嗯,用户让我详细说明“描述符队列”的数据结构,并提供参考文献。首先,我需要确认“描述符队列”具体指的是什么,因为这个词在不同的上下文中可能有不同的含义。比如,在计算机网络、操作系统或者硬件相关的领域里,描述符队列可能涉及不同的实现和结构。 首先,我应该回忆一下常见的应用场景。例如,在网络数据传输中,特别是高性能网络框架如DPDK中,描述符队列用于管理数据包缓冲区。描述符通常包含指向内存地址、长度等信息,队列则用来高效地传递这些描述符。此外,操作系统中的I/O处理,如DMA(直接内存访问)也会用到描述符队列来管理数据传输。 接下来,我需要明确描述符队列的数据结构组成。通常,队列可能是一个环形缓冲区(Ring Buffer),由数组实现,有头尾指针来管理入队和出队操作。每个描述符可能包含内存地址、数据长度、状态标志等信息。数据结构的设计需要考虑并发访问,比如多生产者或多消费者的情况,可能需要锁或者无锁设计。 然后,考虑参考文献。DPDK的官方文档应该是一个可靠的来源,里面详细描述了描述符队列的实现。另外,操作系统相关的书籍,比如《现代操作系统》可能涉及I/O管理和DMA部分。学术论文方面,可能需要查找关于高性能网络数据处理的论文,比如那些讨论零拷贝技术或RDMA(远程直接内存访问)的文章。 用户可能需要这些信息来理解描述符队列在具体系统中的应用,比如开发高性能网络应用或者优化系统I/O。也有可能他们在学习相关课程,需要理论结合实际的例子。因此,在解释时,应该兼顾数据结构的理论设计和实际应用案例,帮助用户更好地理解其工作原理和实现细节。 需要确认的是,用户是否指特定领域的描述符队列,比如网络、存储或图形处理。假设是网络领域,那么DPDK的例子比较合适。同时,要确保解释清晰,避免使用过多专业术语,或在必要时进行解释,以确保不同背景的读者都能理解。 可能还需要比较不同的实现方式,比如单生产者单消费者队列与多生产者多消费者队列的区别,以及它们如何影响性能。此外,描述符队列的管理策略,如批量处理、缓存对齐等优化手段,也是值得提及的方面。 总结来说,回答的结构应该包括:定义和应用场景、数据结构的具体实现(如环形缓冲区)、描述符的组成元素、并发控制机制,以及参考文献。这样用户不仅能了解基本概念,还能知道如何进一步查阅资料。</think>### 描述符队列的数据结构详解 **描述符队列**(Descriptor Queue)是一种用于高效管理数据传输或资源分配的底层数据结构,常见于网络通信、存储系统、硬件加速(如GPU/DMA)等领域。其核心功能是通过预分配的“描述符”记录资源信息(如内存地址、数据长度、状态标志等),并以队列形式实现生产者-消费者模型,降低数据搬移开销。 --- #### **数据结构组成** 1. **环形缓冲区(Ring Buffer)** - 描述符队列通常基于环形数组实现,通过头(Head)和尾(Tail)指针管理入队和出队操作。 - **优势**:内存连续、缓存友好,避免动态内存分配的开销。 2. **描述符(Descriptor)** 每个描述符是一个结构体或对象,包含以下字段(以网络数据包为例): ```c struct descriptor { void *buffer_addr; // 数据缓冲区地址 uint32_t length; // 数据长度 uint8_t status; // 状态(如“已就绪”“已消费”) uint16_t flags; // 控制标志(如校验和卸载) }; ``` 3. **并发控制机制** - **单生产者单消费者(SPSC)**:无需锁,通过原子操作更新头尾指针。 - **多生产者多消费者(MPMC)**:可能需要锁或CAS(Compare-and-Swap)操作。 --- #### **工作流程** 1. **初始化**:预分配描述符和缓冲区,构建环形队列。 2. **生产者填充**: - 应用程序或硬件(如网卡)将数据写入缓冲区,更新描述符状态为“就绪”。 - 移动尾指针,通知消费者有新数据。 3. **消费者处理**: - 读取描述符,处理数据后标记为“已消费”。 - 移动头指针,释放缓冲区供重用。 --- #### **应用场景** 1. **高性能网络(如DPDK/SPDK)**:网卡直接读写描述符队列,实现零拷贝(Zero-Copy)传输。 2. **存储系统**:NVMe SSD通过队列管理I/O请求,提升吞吐量。 3. **GPU渲染**:命令队列描述渲染指令和资源地址,驱动GPU异步执行。 --- #### **优化设计** - **批量处理**:一次操作多个描述符,减少指针更新次数。 - **缓存对齐**:避免描述符跨缓存行,提高访问速度。 - **无锁设计**:通过内存屏障(Memory Barrier)确保线程安全。 --- #### **参考文献** 1. **书籍与文档** - DPDK官方文档:[Data Plane Development Kit: Ring Library](https://doc.dpdk.org/guides/prog_guide/ring_lib.html) - 《深入理解计算机系统》(Computer Systems: A Programmer's Perspective):第6章解存储器层次结构与DMA。 - 《Linux设备驱动程序开发》:详解DMA和环形缓冲区实现。 2. **学术论文** - Mogul, J. C., & Ramakrishnan, K. K. (1997). Eliminating receive livelock in an interrupt-driven kernel. *ACM Transactions on Computer Systems (TOCS)*. (探讨中断与队列管理优化) - Intel技术白皮书:[Intel® 64 and IA-32 Architectures Optimization Reference Manual](https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf) (缓存与无锁队列设计) 3. **开源实现参考** - DPDK的`rte_ring`库(C语言实现无锁环形队列)。 - Linux内核`kfifo`(用于内核态环形缓冲区)。 --- 通过上述设计,描述符队列能够在高并发场景下实现低延迟、高吞吐的资源管理,是系统性能优化的关键组件之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值