在现代软件开发中,编译器优化技术是提高程序性能和资源利用率的关键手段。然而,这些优化技术在调试过程中可能会带来一些挑战。本文将探讨链接时优化(Link-time optimization, LTO)技术在编译过程中的应用及其对调试的影响,特别是针对AVR微控制器和Arduino程序。
链接时优化是一种编译器优化技术,它在最终链接生成二进制文件之前,考虑整个程序。LTO是相对较新的技术,大约十年前成为GCC工具链的一部分。与传统的只针对单个编译单元(即源文件)的优化技术不同,LTO可以跨所有链接的编译单元消除未使用的函数、方法和类成员。
LTO的一个缺点是这种优化可能需要很长时间。与传统的链接阶段仅解决地址问题(仅需几秒钟)不同,全局链接时优化可能需要几分钟。此外,由于跨编译单元的函数内联,可能会导致更大的堆栈帧,从而可能导致堆栈溢出。最后,由于删除代码部分和重新排列其他部分,调试可能会变得更加困难。
自从ArduinoIDE 1.8.11版本(2020年初发布)以来,LTO默认启用,因为它通常可以显著降低对闪存内存的需求。然而,直到昨天,都没有注意到这一点,这是一个好兆头。
编写了一个小型面向对象程序,旨在作为正在开发的调试器的测试案例。下面是一个简化的版本:
class TwoDObject {
public:
int x, y;
TwoDObject(int xini, int yini) {
x = xini; y = yini;
}
void move(int xchange, int ychange) {
x = x + xchange; y = y + ychange;
}
};
class Rectangle : public TwoDObject {
public:
int height, width;
Rectangle(int xini, int yini, int heightini, int widthini) :
TwoDObject(xini, yini) {
height = heightini; width = widthini;
}
int area(void) {
return (width * height);
}
};
Rectangle r(10, 11, 5, 8);
void setup(void) {
Serial.begin(9600);
Serial.print(F("\nr position: ")); Serial.print(r.x);
Serial.print(","); Serial.println(r.y);
Serial.print(F("\narea: ")); Serial.print(r.area());
Serial.println();
Serial.println(F("\nMove r by +10, +10:"));
r.move(10, 10);
Serial.print(F("\nr position: ")); Serial.print(r.x);
Serial.print(","); Serial.println(r.y);
}
void loop() { }
有一个基类TwoDObject和一个派生类Rectangle,其中包含一些成员变量和函数,以及最小的继承。没有什么特别花哨的。
现在,使用ArduinoIDE或arduino-cli编译这个草图,并设置优化级别为调试友好(参见之前关于启用Arduino IDE进行调试的文章),即,指定优化选项-Og。在启动调试器并将二进制文件上传到Arduino板之后,可能想知道实例变量r的类型(使用调试器命令ptype)以及成员变量r.x的值。
在添加了-fno-lto选项到构建标志之后,即有效地禁用了LTO优化。使用arduino-cli时,可以通过添加--build-property选项来实现:
arduino-cli compile -b ... -e --build-property "build.extra_flags=-Og -fno-lto"
再次调用调试器,现在会得到一个更“现实”的画面:
(gdb) ptype r
type = class Rectangle : public TwoDObject {
public:
int height;
int width;
Rectangle(int, int, int, int);
int area(void);
}
(gdb) print r.x
$1 = 0
(gdb) print r
$2 = {
= {x = 0, y = 12544}, height = 59, width = 0
}
这看起来是人们最初预期的样子。因此,关键的教训是,在调试时应该禁用flto选项。特别是当想要调试面向对象程序时。