性能优化,要懂点编译原理

前言一

现在正处在世界杯小组赛的阶段,我通常都是10点钟早早睡觉,然后爬起来看完12点场的比赛再睡觉,或者,早起看6点场的比赛然后冲个澡去上班。今晚为了写这篇文章,就不打算早睡了,直接看完12点场的比赛再睡觉。

前言二

我不是想说gcc的-O选项,诚然它很强大,不过gcc manual已经说的很清楚了 Gcc-Optimize-Options

前言三

在我上大学的时候,我买了本《编译原理》,盗版的,这应该是我这一生买的最后一本盗版书。那本书我翻了几页就没继续往下看,因为纸张看着实在是太不舒服了。后来工作后,我从amazon上买了本正版的,纸张确实好很多,不过还是看不下去,原因嘛,你懂的~

正文

可执行文件的内存布局对程序性能的影响是非常巨大的,因为我最近一直在做性能优化,对这方面感触颇深。要搞明白可执行文件的内存布局,就必须得了解编译原理,当然编译原理实在是太过于高深了,我所知也是皮毛,所以我就从最实用的地方开始入手一点点的分析。
就从我前文里提到的__attribute__((section(".sec_name")))来说起吧,因为我使用这个东西确实给我们的性能带来了一定的提升。
关于attribute section这个东西,你要google的话,能够搜索出来不少前人的分析,不过实在都是大同小异,你抄我来我抄你,毫无营养。在他们的博客里,无非是说,“将作用的函数或者数据放入指定名为‘.sec_name’ 的输入段”,然后再巴拉巴拉一通什么是输入段,说的你云里雾里一头雾水分不清东西南北顿觉高大上。
那我们就来看下attribute section到底是什么。
要知道attribute section, 就要先理解链接脚本。链接脚本即链接器在把.o文件链接成最后的elf文件所遵循的规则,也就是,最终的可执行文件是什么样子的是由这个链接脚本决定的。链接脚本的语法和C语言很类似,我们能够很容易读明白,所以从链接脚本来入手分析这个东西会更清晰一些。对应于 __attribute__((section(".sec_name")))这句话,它在链接的时候采取的默认规则是:

1
2
3
4
.sec_name
{
     *(.sec_name)
}

即把.sec_name指向的内容放在.sec_name这个段里面。我们再来稍微清晰化一些,下面举个例子。

1
2
void foo(void)  __attribute__((section(".in_name")));
void bar(void)  __attribute__((section(".in_name")));

我们使用attribute section来声明了两个函数,然后我们在链接脚本里面做如下约束:

1
2
3
4
.out_name
{
     *(.in_name)
}

这样就把foor(), bar()这两个函数给放在了最终elf文件里的.out_name这个section。而如果我们不再链接脚本里做这个约束,那么它在链接过程中就会采用默认规则,即输入段和输出段的名字是一样的:

1
2
3
4
.in_name
{
     *(.in_name)
}

总结起来就是,__attribute__((section(".in_name")))的作用是把.in_name指向的符号给放在一起。
唔~ 仍然有点模糊是不? 那就好好读读《linkers and loaders》或者《程序员的自我修养》这两本书吧。

然后我们来看下linux内核对于attribute section的应用, 以linux kernel的链接脚本vmlinux.lds为例。先来看下linux kernel最终镜像的代码段是如何规划的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SECTIONS
{
     . = VMLINUX_LOAD_ADDRESS;
     /* read-only */
     _text = .;     /* Text and read-only data */
     .text : {
          TEXT_TEXT
          SCHED_TEXT
          LOCK_TEXT
          KPROBES_TEXT
          IRQENTRY_TEXT
          *(.text.*)
          *(.fixup)
          *(.gnu.warning)
     } :text = 0
}

如上就是一个典型的linux kenrel链接脚本的代码段部分。稍微解释下。

1
.  = VMLINUX_LOAD_ADDRESS;

的意思是说,此处的地址是VMLINUX_LOAD_ADDRESS,接着又把该值赋给了_text,也就是内核代码段的其实地址是VMLINUX_LOAD_ADDRESS,就这就开始了代码段。在代码段里面我们可以很明显的看到它划分了TEXT_TEXT、SCHED_TEXT、LOCK_TEXT、KPROBES_TEXT、IRQENTRY_TEXT,这样划分的目的,就是为了合理规划地址空间以提升性能。我们可以看下这几个宏到底表示什么:

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
#define TEXT_TEXT                                   \
          ALIGN_FUNCTION();                         \
          *(.text.hot)                              \
          *(.text)                              \
          *(.ref.text)                              \
     MEM_KEEP(init.text)                              \
     MEM_KEEP(exit.text)                              \
          *(.text.unlikely)

#define SCHED_TEXT                                   \
          ALIGN_FUNCTION();                         \
          VMLINUX_SYMBOL(__sched_text_start) = .;               \
          *(.sched.text)                              \
          VMLINUX_SYMBOL(__sched_text_end) = .;

#define LOCK_TEXT                                   \
          ALIGN_FUNCTION();                         \
          VMLINUX_SYMBOL(__lock_text_start) = .;               \
          *(.spinlock.text)                         \
          VMLINUX_SYMBOL(__lock_text_end) = .;

#define KPROBES_TEXT                                   \
          ALIGN_FUNCTION();                         \
          VMLINUX_SYMBOL(__kprobes_text_start) = .;          \
          *(.kprobes.text)                         \
          VMLINUX_SYMBOL(__kprobes_text_end) = .;

#define IRQENTRY_TEXT                                   \
          ALIGN_FUNCTION();                         \
          VMLINUX_SYMBOL(__irqentry_text_start) = .;          \
          *(.irqentry.text)                         \
          VMLINUX_SYMBOL(__irqentry_text_end) = .;

这些宏其实就是定义了一些input section, 比如.text.hot等。
接着以.sched.text为例来看看到底是怎么用的。

1
2
3
4
5
6
7
8
9
10
#define __sched          __attribute__((__section__(".sched.text")))

void __sched notrace preempt_schedule_context(void);
static void __sched __schedule(void);
asmlinkage void __sched schedule(void);
asmlinkage void __sched schedule_user(void);
void __sched schedule_preempt_disabled(void);
asmlinkage void __sched notrace preempt_schedule(void);
asmlinkage void __sched preempt_schedule_irq(void);
....

一目了然了吧? 就是前面我们说的attribute section这个东西,内核就是使用了这个东西来规划地址空间,将相互关联的代码给放在一起,以达到提升性能并保持稳定的作用。

因为松柏公司的性能受代码check-in影响波动较大,所以我就想到了使用linux kernel的这种做法来规划可执行文件的地址空间,按照不同模块来划分不同的section,这样来避免频繁code check-in对性能波动的影响。

1
2
3
4
5
6
7
.text
{
     *(.module_a.text)
     *(.module_b.text)
     *(.module_c.text)
     ...
}

其实,再稍微的深入思考下,我们就能发现一个更细粒度的控制,那就是控制函数在可执行文件里的先后顺序。

1
2
3
4
5
6
7
8
9
void foo(void)  __attribute__((section(".in_name.1")));
void bar(void)  __attribute__((section(".in_name.2")));

 .text
{
     *(.in_name.1)
     *(.in_name.2)
     ...
}

这样做之后,在可执行文件里,foo就会在bar的前面,及foo和bar的地址紧挨着,bar紧跟在foo的后面。

我们在缩小一下我们的视角,从宏观上来看下这个链接脚本。

1
2
3
4
5
6
7
8
9
10
11
12
SECTIONS
{
     .text : {
          ...
     }
     .data : {
          ...
     }
     .bss:{
          ...
     }
}

这也是为什么可执行文件的内存布局先是代码段,接着数据段,再是bss段的原因,即链接脚本决定可执行文件的内存布局。在linux/freebsd机器上运行ld —verbose就可以获得ld使用的默认链接脚本。

Comments