0%

gcc 03

Compiling for debugging

Normally, an executable file does not contain any references to the original program source code, such as variable names or line-numbers—the exe- cutable file is simply the sequence of machine code instructions produced by the compiler.

通常,可执行文件不包含对原始程序源代码的任何引用,例如变量名或行号——可执行文件只是编译器生成的机器代码指令序列。这对于调试来说是不够的,因为如果程序崩溃,就没有简单的方法来找到错误的原因。

GCC provides the -g debug option to store additional debugging information in object files and executables. This debugging information allows errors to be traced back from a specific machine instruction to the corresponding line in the original source file. It also allows the execution of a program to be traced in a debugger, such as the GNU Debugger gdb.

Using a debugger also allows the values of variables to be examined while the program is running.

The debug option works by storing the names of functions and vari- ables (and all the references to them), with their corresponding source code line-numbers, in a symbol table in object files and executables.

Examining core files

In addition to allowing a program to be run under the debugger, another helpful application of the -g option is to find the circumstances of a program crash.When a program exits abnormally the operating system can write out a core file, usually named core, which contains the in-memory state of the program at the time it crashed. Combined with information from the symbol table produced by -g, the core file can be used to find the line where the program stopped, and the values of its variables at that point.

这在软件开发过程中和部署后都很有用——它允许在程序现场(in the field)崩溃时调查问题。

默认会在当前目录生成 core 文件,This core file contains a complete copy of the pages of memory used by the program at the time it was terminated. Incidentally, the term segmentation fault refers to the fact that the program tried to access a restricted memory segment outside the area of memory which had been allocated to it.

某些系统默认配置为不写入核心文件,因为这些文件可能很大,并会迅速填满系统的可用磁盘空间。在 GNU Bash shell 中,命令 ulimit -c 控制核心文件的最大大小。如果大小限制为零,则不会生成任何核心文件。设置不限制 core 文件大小 ulimit -c unlimited,只会对当前 shell 有效。

拿到核心文件怎么使用呢?gdb EXECUTABLE-FILE CORE-FILE,Note that both the original executable file and the core file are required for debugging, it is not possible to debug a core file without the corresponding executable.

会进入 debug 模式,停在程序崩溃的那一点,可以查看变量值等操作。backtrace 命令查看 stack backtrace。

The debugger can also show the function calls and arguments up to the current point of execution, this is called a stack backtrace.

Compiling with optimization

GCC is an optimizing compiler. It provides a wide range of options which aim to increase the speed, or reduce the size, of the executable files it generates.

优化是一个很复杂的过程。对于源代码中的每个高级命令,通常有许多可能的机器指令组合可用于实现适当的最终结果。编译器必须考虑这些可能性并从中进行选择。

一般来说,必须为不同的处理器生成不同的代码,因为它们使用不兼容的汇编和机器语言。每种处理器都有各自的特点,有些 CPU 提供大量寄存器来保存计算的中间结果,而有些 CPU 则必须将中间结果存储到内存中并从内存中取出。每种情况都必须生成适当的代码。

此外,不同的指令需要不同的时间来执行,具体取决于它们的排序方式(指令顺序)。GCC 会考虑所有这些因素,并在优化编译时尝试为给定系统生成最快的可执行文件。

Source-level optimization

GCC 使用的第一种优化形式发生在源代码级,不需要任何机器指令知识。有许多源代码级优化技术:这里提供两种:公共子表达式消除和函数内联 common subexpression elimination and function inlining.

Common subexpression elimination

1
2
3
4
x = cos(v)*(1+sin(u/2)) + sin(w)*(1-sin(u/2))
// 可以改写为
t = sin(u/2)
x = cos(v)*(1+t) + sin(w)*(1-t)

This rewriting is called common subexpression elimination (CSE), and is performed automatically when optimization is turned on. Common subexpression elimination is powerful, because it simultaneously increases the speed and reduces the size of the code.

Function inlining

Another type of source-level optimization, called function inlining, increases the efficiency of frequently-called functions.

Whenever a function is used, a certain amount of extra time is required for the CPU to carry out the call: it must store the function arguments in the appropriate registers and memory locations, jump to the start of the function (bringing the appropriate virtual memory pages into physical memory or the CPU cache if necessary), begin executing the code, and then return to the original point of execution when the function call is complete.

每当使用一个函数时,CPU 都需要一定的额外时间来执行调用:它必须将函数参数存储在适当的寄存器和内存位置中,跳转到函数的开头(如果需要,将适当的虚拟内存页面带入物理内存或 CPU 缓存),开始执行代码,然后在函数调用完成时返回到原来的执行点。

This additional work is referred to as function-call overhead.

Function inlining eliminates this overhead by replacing calls to a function by the code of the function itself (known as placing the code in-line).

函数内联通过用函数本身的代码替换对函数的调用(称为将代码置于内联)来消除这种开销。

In most cases, function-call overhead is a negligible fraction of the total run-time of a program.

大多数情况下,函数调用开销只是程序总运行时间中微不足道的一部分。

It can become significant only when there are functions which contain relatively few instructions, and these func- tions account for a substantial fraction of the run-time—in this case the overhead then becomes a large proportion of the total run-time.

仅当某些函数包含的指令相对较少,并且这些函数占据了运行时间的很大一部分时,它才会变得很重要 —— 在这种情况下,开销就会占到总运行时间的很大一部分。

Inlining is always favorable if there is only one point of invocation of a function. It is also unconditionally better if the invocation of a function requires more instructions (memory) than moving the body of the function in-line.

如果函数调用需要更多的指令(内存),那么比将函数主体移入内联也无条件地更好。

此外,通过将几个单独的函数合并为一个大函数,内联还可以促进进一步的优化,例如消除公共子表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
double sq(double x)
{
return x * x;
}

for (i = 0; i < 10000000; i++)
{
sum += sq(i + 0.5);
}

//inline
for (i = 0; i < 10000000; i++)
{
double t = (i + 0.5); /* temporary variable */
sum += t * t;
}

GCC 使用多种启发式方法选择要内联的函数,例如函数是否足够小。作为优化,内联仅在每个目标文件内执行。可以使用 inline 关键字明确要求尽可能内联特定函数,包括在其他文件中使用它。

Speed-space tradeoffs

While some forms of optimization, such as common subexpression elimination, are able to increase the speed and reduce the size of a program simultaneously, other types of optimization produce faster code at the expense of increasing the size of the executable. This choice between speed and memory is referred to as a speed-space tradeoff. Optimizations with a speed-space tradeoff can also be used to make an executable smaller, at the expense of making it run slower.

虽然某些形式的优化(例如公共子表达式消除)能够同时提高速度并减小程序大小,但其他类型的优化会以增加可执行文件大小为代价来产生更快的代码。速度和内存之间的这种选择被称为速度-空间权衡。速度-空间权衡的优化也可用于使可执行文件更小,但代价是使其运行速度变慢。

Loop unrolling

循环展开。速度和空间权衡的优化的一个主要例子是循环展开。This form of optimization increases the speed of loops by eliminating the end of loop condition on each iteration. 这种优化形式通过消除每次迭代的循环结束条件来提高循环速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for (i = 0; i < 8; i++)
{
y[i] = i;
}
// 每次循环都要检查 i < 8,最终会执行 9 次;
// 展开循环
y[0] = 0;
y[1] = 1;
y[2] = 2;
y[3] = 3;
y[4] = 4;
y[5] = 5;
y[6] = 6;
y[7] = 7;
// 这个代码不需要任何条件判断,执行速度最快,并且由于每个赋值语句都是独立的,还允许编译器在支持并行性的处理器上使用并行性。

Loop unrolling is an optimization that increases the speed of the resulting executable but also generally increases its size。

如果正确处理了开始和结束条件,当循环的上限未知时,也可以展开循环。例如,具有任意上限的相同循环

1
2
3
4
for (i = 0; i < n; i++)
{
y[i] = i;
}

可以改写为:

1
2
3
4
5
6
7
8
9
for (i = 0; i < (n % 2); i++)
{
y[i] = i;
}
for ( ; i + 1 < n; i += 2) /* no initializer */
{
y[i] = i;
y[i+1] = i+1;
}

The first loop handles the case i = 0 when n is odd, and the second loop handles all the remaining iterations.

第二个循环能够并发执行,并且减少了一半的条件检查。Higher factors can be achieved by unrolling more assignments inside the loop, at the cost of greater code size. 可以多展开减少判断,但会增加可执行文件的大小。

Scheduling

最低级别的优化是调度,其中编译器确定各个指令的最佳顺序。大多数 CPU 允许一个或多个新指令在其他指令完成之前开始执行。许多 CPU 还支持流水线,即多个指令在同一 CPU 上并行执行。

启用调度后,必须对指令进行安排,以便其结果在正确的时间可供后续指令使用,并允许最大程度的并行执行。调度可以提高可执行文件的速度,而不会增加其大小,但在编译过程中需要额外的内存和时间(由于其复杂性)。

Optimization levels

为了控制编译时间和编译器内存使用情况,以及生成的可执行文件的速度和空间之间的权衡,GCC 提供了一系列通用优化级别,编号从 0 到 3,以及针对特定类型优化的单独选项。

使用命令行选项 -OLEVEL 选择优化级别,其中 LEVEL 是从 0 到 3 的数字。不同优化级别的效果如下所述:

  1. -O0 or no -O option (default)
    在这个优化级别,GCC 不会执行任何优化,而是以最直接的方式编译源代码。源代码中的每个命令都会直接转换为可执行文件中的相应指令,而无需重新排列。这是调试程序时使用的最佳选项。选项 -O0 相当于不指定 -O 选项。
  2. -O1 or -O
    此级别启用最常见的优化形式,不需要任何速度空间权衡。使用此选项,生成的可执行文件应该比使用 -O0 时更小、更快。更昂贵的优化(如指令调度)不在此级别使用。使用选项 -O1 进行编译通常比使用 -O0 进行编译花费的时间更少,因为简单优化后需要处理的数据量减少了。
  3. -O2
    此选项除了启用 -O1 所使用的优化外,还启用了进一步的优化。这些额外的优化包括指令调度。仅使用不需要任何速度空间权衡的优化,因此可执行文件的大小不会增加。与使用 -O1 相比,编译器将花费更长的时间来编译程序,并且需要更多的内存。此选项通常是程序部署的最佳选择,因为它提供了最大程度的优化,而不会增加可执行文件的大小。它是 GNU 软件包版本的默认优化级别。
  4. -O3
    此选项除了启用较低级别 -O2-O1 的所有优化外,还启用了更昂贵的优化,例如函数内联。-O3 优化级别可能会提高生成的可执行文件的速度,但也会增加其大小。在这些优化不利的某些情况下,此选项实际上可能会使程序变慢。
  5. -funroll-loops
    此选项可打开循环展开,并且与其他优化选项无关。它将增加可执行文件的大小。此选项是否产生有益结果必须根据具体情况进行检查。
  6. -Os
    此选项选择优化以减小可执行文件的大小。此选项的目的是为受内存或磁盘空间限制的系统生成尽可能小的可执行文件。在某些情况下,较小的可执行文件也会运行得更快,因为缓存使用得更好。

It is important to remember that the benefit of optimization at the highest levels must be weighed against the cost. The cost of optimization includes greater complexity in debugging, and increased time and memory requirements during compilation. For most purposes it is satisfactory to use -O0 for debugging, and -O2 for development and deployment.

Optimization and debugging

使用 GCC 时,可以将优化与调试选项 -g 结合使用。许多其他编译器不允许这样做。当同时使用调试和优化时,优化器执行的内部重新排列可能使你在调试器中检查优化程序时难以查看正在发生的情况。例如,临时变量通常会被消除,语句的顺序可能会改变。但是,当程序意外崩溃时,任何调试信息都比没有好 - 因此建议对优化程序使用 -g,无论是开发还是部署。调试选项 -g 在 GNU 软件包版本中默认启用,与优化选项 -O2 一起使用。

Optimization and compile warning

当打开优化时,GCC 会产生未优化编译时不会出现的额外警告。作为优化过程的一部分,编译器会检查所有变量及其初始值的使用情况 — 这称为数据流分析 data-flow analysis。它构成了其他优化策略(如指令调度)的基础。数据流分析的一个副作用是编译器可以检测到未初始化变量的使用情况。

-Wuninitialized 选项(包含在-Wall中)会警告未初始化就读取的变量。它仅在程序经过优化编译以启用数据流分析时才有效。

1
2
3
4
5
6
7
8
9
10
11
int sing(int x)
{
int s;
if (x > 0)
s = 1;
else if (x < 0)
s = -1;
return s;
}

// x = 0 会报错

单独使用 -Wall 选项编译程序不会产生任何警告,因为没有优化就不会进行数据流分析。

要产生警告,程序必须同时使用 -Wall 和优化进行编译。实际上,需要优化级别 -O2 才能给出良好的警告:gcc -Wall -O2 -c uninit.c 这正确地检测出了变量 s 未被定义而被使用的可能性。

请注意,虽然 GCC 通常可以找到大多数未初始化的变量,但它使用启发式方法,有时会错过一些复杂的情况或对其他情况发出错误警告。在后一种情况下,通常可以以更简单的方式重写相关行,从而消除警告并提高源代码的可读性。