在 Object Pascal 中,内联函数和方法是一种低级语言特性,可以带来显著的优化效果。一般来说,当你调用一个方法时,编译器会生成一些代码,让你的程序跳转到一个新的执行点。这意味着要设置堆栈框架并进行一些操作,可能需要十几条机器指令。不过,你执行的方法可能非常简短,甚至可能是一个访问方法,只是设置或返回某个私有字段。在这种情况下,将实际代码复制到调用位置,避免堆栈帧设置和其他一切操作,是非常有意义的。去掉这些开销后,程序的运行速度会更快,尤其是当调用发生在一个执行了数千次的紧凑循环中时。
对于一些非常小的函数,内联所产生的代码甚至可能更少,因为复制过去的代码可能比函数调用所需的代码更少。但要注意的是,如果一个较长的函数被内联,并且这个函数在程序中的许多不同地方被调用,就可能会出现代码膨胀,即不必要地增加了可执行文件的大小。
在 Object Pascal 中,您可以使用 inline
指令要求编译器内联一个函数(或方法),该指令放置在函数(或方法)声明之后。在定义中不需要重复此指令。始终记住,inline
指令只是对编译器的提示,编译器可以决定该函数不适合内联并忽略您的请求(而不以任何方式警告您)。编译器还可能在分析调用代码后,根据调用位置的 $INLINE
指令的状态,内联部分(但不一定是所有的)函数调用。此指令可以假定三个不同的值(请注意,此功能与优化编译器开关无关):
{$INLINE OFF}
您可以在程序、程序的某个部分或特定调用点抑制内联,而不考虑被调用函数中是否存在 inline
指令。{$INLINE ON}
,对于由 inline
指令标记的函数,启用内联。{$INLINE AUTO}
编译器通常会内联您用指令标记的函数,以及自动内联非常短的函数。请注意,此指令可能导致代码膨胀。 在 Object Pascal 运行时库中,有许多函数已被标记为内联候选函数。例如,System.Math 单元的 Max 函数有如下定义:
function Max(const A, B: Integer): Integer; overload; inline;
为了测试内联此函数的实际效果,我在 InliningTest 示例中编写了以下循环:
var
Sw: TStopWatch;
I, J: Integer;
begin
J := 0;
Sw := TStopWatch.StartNew;
for I := 0 to LoopCount do
J := Max(I, J);
Sw.Stop;
Show('Max ' + J.ToString +
' [' + Sw.ElapsedMilliseconds.ToString + ']');
在这段代码中,System.Diagnostics 单元的 TStopWatch 记录是一种结构,用于跟踪在 Start(或 StartNew)和 Stop 调用之间经过的时间(或系统时钟滴答声)。
该窗体有两个按钮,它们都调用了相同的代码,但其中一个按钮在调用处禁用了内联。请注意,您需要使用 Release 配置进行编译才能看到任何区别(因为内联是 Release 的优化)。在我的电脑上,重复运行两千万次(LoopCount 常量的值)后,我得到了以下结果:
// 在 Windows 上(在虚拟机中运行)
Max on 20000000 [17]
Max off 20000000 [45]
// 在 Android 上(在设备上)
Max on 20000000 [280]
Max off 20000000 [376]
我们该如何解读这些数据呢?在 Windows 上,内联可使程序执行速度提高一倍多,而在 Android 上,内联可使程序运行速度提高约 35%。不过,在移动设备上,程序的运行速度要慢得多(慢一个数量级),因此,在 Windows 上,我们可以节省 30 毫秒,而在我的 Android 设备上,这种优化可以节省约 100 毫秒。
同一程序还使用 Length 函数进行了第二次类似测试,这是一个编译器魔法函数,经过专门修改后进行了内联。同样,在 Windows 和 Android 上,内联版本的速度明显更快:
// 在 Windows 上(在虚拟机中运行)
Length inlined 260000013 [11]
Length not inlined 260000013 [40]
// 在 Android 上(在设备上)
Length inlined 260000013 [401]
Length not inlined 260000013 [474]
这是这个第二个测试循环使用的代码:
var
Sw: TStopWatch;
I, J: Integer;
Sample: string;
begin
J := 0;
Sample := 'sample string';
Sw := TStopWatch.StartNew;
for I := 0 to LoopCount do
Inc(J, Length(Sample));
Sw.Stop;
Show('Length not inlined ' + IntToStr(J) +
' [' + IntToStr(Sw.ElapsedMilliseconds) + ']');
end;
Object Pascal 编译器并没有明确限制可内联函数的大小,也没有列出阻止内联的特定结构(for 或 while 循环、条件状态)。不过,由于内联一个大函数几乎没有什么好处,但却有可能带来一些真正的坏处(就代码臃肿而言),因此应该避免内联。
内联的一个限制是,方法或函数不能引用单元implementation部分定义的标识符(如类型、全局变量或函数),因为在调用位置无法访问这些标识符。不过,如果你调用的是一个局部函数,而该函数恰好也是内联的,那么编译器就会接受你的请求,将你的例程内联。
内联的一个缺点是需要更频繁地重新编译单元,因为当你修改一个内联函数时,每个调用位置的代码都需要重新编译。在一个单元中,您可以在调用内联函数之前编写内联函数的代码,但最好将它们放在implementation部分的开头。
注解:Delphi是单次扫描编译器,因此它无法引用尚未看到的函数代码。
在不同的单元中,您需要在 uses 语句中特别添加其他单元的内联函数,即使您不直接调用这些方法。假设单元 A 调用了单元 B 中定义的一个内联函数,如果该函数反过来又调用了单元 C 中的另一个内联函数,那么单元 A 也需要引用单元 C。否则,编译器会发出警告,指出由于缺少单元引用,调用没有被内联。一个相关的影响是,当存在循环单元引用(通过其实现部分)时,函数就永远不会被内联。