Ada 样式指南/改进性能
在许多方面,性能与可维护性和可移植性相矛盾。为了提高速度或内存使用率,最清晰的算法有时会让位于混乱的代码。为了利用专用硬件或操作系统服务,引入了不可移植的实现依赖项。在关注性能时,您必须确定每个算法如何满足其性能和可维护性目标。谨慎使用本章中的指南;它们可能对您的软件有害。
构建满足其性能要求的系统的最佳方法是通过良好的设计。您不应假设加速代码会导致系统执行速度的明显提高。在大多数应用程序中,系统的整体吞吐量不是由代码的执行速度定义的,而是由并发进程之间的交互以及系统外围设备的响应时间定义的。
本章中的大多数指南都写着“......当衡量的性能表明”。“表明”意味着您已确定,在您的环境中,应用程序性能提升带来的好处胜过由此产生的代码的可理解性、可维护性和可移植性方面的负面影响。许多指南示例展示了您需要衡量的替代方案,以便确定是否需要使用该指南。
性能至少有四个方面:执行速度、代码大小、编译速度和链接速度。虽然所有四个方面都很重要,但大多数人在提到性能时都会想到执行速度,本章中的大多数指南都侧重于执行速度。
性能受许多因素的影响,包括编译软件、硬件、系统负载和编码风格。虽然通常只有编码风格受程序员控制,但其他因素的影响如此之大,以至于不可能做出“case语句比if-then-else结构更高效”之类的断言。当性能至关重要时,不能假设在一种系统上证明更高效的编码风格在另一种系统上也会更高效。为了提高性能而做出的决定必须基于对应用程序将要运行的实际系统的替代方案进行测试。
虽然大多数知名的性能测量工具都是独立程序,专注于执行速度,但有一个全面的工具涵盖了性能的四个方面。Ada编译器评估系统(ACES)是合并了两个早期产品的结果:美国国防部的Ada编译器评估能力和英国国防部的Ada评估系统。它提供了一套全面的近2000个性能测试,以及自动设置、测试管理和分析软件。该系统报告(并对统计数据进行分析)编译时间、链接时间、执行时间和代码大小。分析工具可以比较多个编译执行系统,并比较使用不同编码风格以实现类似目的的测试的运行时性能。
性能问题工作组(PIWG)套件。Quick-Look工具被宣传为易于下载、安装和执行,并在不到一天的时间内提供与PIWG套件生成的相同有用的信息。此外,sw-eng.falls-church.va.us,目录 /public/AdaIC/testing/aces。对于万维网访问,请使用以下统一资源定位器(URL):http://sw-eng.falls-church.va.us/AdaIC/testing/aces/.
虽然测量性能似乎是一件比较简单的事情,但任何计划进行这种测量的人或工具集都必须解决一些重要问题。有关详细信息,请参阅以下资料:ACES(1995a、1995b、1995c);Clapp、Mudge 和 Roy(1990);Goforth、Collard 和 Marquardt(1990);Knight(1990);Newport(1995);以及Weidermann(1990)。
- 当测量性能表明时,使用块(见指南 5.6.9)引入延迟初始化。
...
Initial : Matrix;
begin -- Find_Solution
Initialize_Solution_Matrix:
for Row in Initial'Range(1) loop
for Col in Initial'Range(2) loop
Initial (Row, Col) := Get_Value (Row, Col);
end loop;
end loop Initialize_Solution_Matrix;
Converge_To_The_Solution:
declare
Solution : Matrix := Identity;
Min_Iterations : constant Natural := ...;
begin -- Converge_To_The_Solution
for Iterations in 1 .. Min_Iterations loop
Converge (Solution, Initial);
end loop;
end Converge_To_The_Solution;
...
end Find_Solution;
延迟初始化允许编译器在寄存器使用优化方面有更多选择。根据具体情况,这可能会带来显著的性能提升。
有些编译器在引入声明块时会造成性能损失。程序员需要仔细分析和计时测试,以识别应该删除的那些声明块。
很难通过代码检查准确预测哪些声明块会提高性能,哪些声明块会降低性能。但是,通过这些一般准则和对特定实现的熟悉,可以提高性能。
- 当测量性能表明时,使用约束数组。
如果数组边界在运行时之前未知,那么这些边界的计算可能会影响运行时性能。使用命名常量或静态表达式作为数组边界,可能会比使用变量或非静态表达式提供更好的性能。因此,如果 Lower 和 Upper 的值在运行时之前没有确定,那么
... is array (Lower .. Upper) of ...
可能会导致地址和偏移量计算延迟到运行时,从而造成性能损失。有关权衡和替代方案的详细讨论,请参阅 NASA(1992)。
- 当测量性能表明时,为数组使用基于零的索引。
对于某些编译器,下界为 0(整数零或枚举类型的第一个值)的数组的偏移量计算会简化。对于其他编译器,如果下界为 1,则更有可能进行优化。
- 当测量性能表明时,为记录使用固定大小的组件。
subtype Line_Range is Integer range 0 .. Max_Lines;
subtype Length_Range is Integer range 0 .. Max_Length;
-- Note that Max_Lines and Max_Length need to be static
type Paragraph_Body is array (Line_Range range <>, Length_Range range <>) of Character;
type Paragraph (Lines : Line_Range := 0; Line_Length : Length_Range := 0) is
record
Text : Paragraph_Body (1 .. Lines, 1 .. Line_Length);
end record;
确定无约束记录的大小和速度影响,这些记录的组件取决于鉴别符。有些编译器会为类型的每个对象分配最大可能的大小;其他编译器会使用指向相关组件的指针,从而可能会造成堆性能损失。考虑使用固定大小的组件的可能性。
- 当测量的性能表明时,将记录数组定义为并行数组。
-- Array of records
Process (Student (Index).Name, Student (Index).Grade);
-- Record of arrays
Process (Student.Name (Index), Student.Grade (Index));
-- Parallel arrays
Process (Name (Index), Grade (Index));
确定将数据结构化为记录数组、包含数组的记录或并行数组的影响。Ada 的一些实现会在这些示例中显示出显著的性能差异。
- 当测量的性能表明时,使用一系列赋值来进行聚合。
确定使用聚合与一系列赋值的影响。使用聚合通常需要使用临时变量。如果聚合是“静态的”(即,其大小和组件在编译或链接时已知,允许链接时分配和初始化),那么它通常比一系列赋值更高效。如果聚合是“动态的”,那么一系列赋值可能更高效,因为不需要临时变量。
有关从可读性和可维护性角度讨论聚合,请参见指南 5.6.10。
有关扩展聚合的讨论,请参见指南 10.6.1。
- 当测量的性能表明时,使用增量方案而不是 mod 和 rem。
-- Using mod
for I in 0 .. N loop
Update (Arr (I mod Modulus));
end loop;
-- Avoiding mod
J := 0;
for I in 0 .. N loop
Update (Arr (J));
J := J + 1;
if J = Modulus then
J := 0;
end if;
end loop;
确定使用 mod 和 rem 运算符的影响。上面的一种样式可能比另一种明显更高效。
- 当测量的性能表明时,使用短路控制形式。
-- Nested "if"
if Last >= Target_Length then
if Buffer (1 .. Target_Length) = Target then
...
end if;
end if;
-- "and then"
if Last >= Target_Length and then Buffer (1 .. Target_Length) = Target then
...
end if;
确定使用嵌套 if 语句与使用 and then
或 or else
运算符的影响。上面的一种可能比另一种明显更高效。
- 当测量的性能表明时,使用 case 语句。
subtype Small_Int is Integer range 1 .. 5;
Switch : Small_Int;
...
-- Case statement
case Switch is
when 1 => ...
when 2 => ...
when 3 => ...
when 4 => ...
when 5 => ...
end case;
-- "elsif construct"
if Switch = 1 then
...
elsif Switch = 2 then
...
elsif Switch = 3 then
...
elsif Switch = 4 then
...
elsif Switch = 5 then
...
end if;
确定使用 case 语句与 elsif 结构的影响。如果 case 语句是使用小型跳转表实现的,那么它可能比 if .. then .. elsif 结构明显更高效。
有关表驱动编程替代方案的讨论,另请参见指南 8.4.6。
- 当测量的性能表明时,使用硬编码约束检查。
subtype Small_Int is Positive range Lower .. Upper;
Var : Small_Int;
...
-- Using exception handler
Double:
begin
Var := 2 * Var;
exception
when Constraint_Error =>
...
end Double;
-- Using hard-coded check
if Var > Upper / 2 then
...
else
Var := 2 * Var;
end if;
确定使用异常处理程序来检测约束错误的影响。如果异常处理机制很慢,那么硬编码检查可能更高效。
- 当测量的性能表明时,使用二维数组的列优先处理。
type Table_Type is array (Row_Min .. Row_Max, Col_Min .. Col_Max) of ...
Table : Table_Type;
...
-- Row-order processing
for Row in Row_Min .. Row_Max loop
for Col in Col_Min .. Col_Max loop
-- Process Table (Row, Col)
end loop;
end loop;
-- Column-order processing
for Col in Col_Min .. Col_Max loop
for Row in Row_Min .. Row_Max loop
-- Process Table (Row, Col)
end loop;
end loop;
确定以行优先顺序处理二维数组与以列优先顺序处理二维数组的影响。虽然大多数 Ada 编译器可能使用行优先顺序,但这不是必需的。在存在良好优化的前提下,上面的示例可能没有显著差异。在这里,使用静态数组边界也可能很显著。请参见指南 10.4.1 和 10.4.2。
- 当测量的性能表明时,使用覆盖来进行条件赋值。
-- Using "if .. else"
if Condition then
Var := One_Value;
else
Var := Other_Value;
end if;
-- Using overwriting
Var := Other_Value;
if Condition then
Var := One_Value;
end if;
确定分配备选值的样式的影响。示例说明了两种常见的执行方法;对于许多系统,性能差异是显著的。
- 当测量的性能表明时,通过使用切片赋值而不是重复位赋值来执行压缩布尔数组移位操作。
subtype Word_Range is Integer range 0 .. 15;
type Flag_Word is array (Word_Range) of Boolean;
pragma Pack (Flag_Word);
Word : Flag_Word;
...
-- Loop to shift by one bit
for Index in 0 .. 14 loop
Word (Index) := Word (Index + 1);
end loop;
Word (15) := False;
-- Use slice assignment to shift by one bit
Word (0 .. 14) := Word (1 .. 15);
Word (15) := False;
确定切片操作在移位压缩布尔数组时的影响。对于使用压缩布尔数组的 Ada 83 实现,当使用切片赋值而不是 for 循环每次移动一个组件时,移位操作可能快得多。对于 Ada 95 实现,考虑使用模块类型(参见指南 10.6.3)。
- 当测量的性能表明时,使用静态子程序调度。
本例中的“静态调度”一词指的是使用 if/elsif 序列来显式确定要调用的子程序,具体取决于某些条件。
-- (1) Dispatching where tag is not known at compile time
-- (See ACES V2.0 test "a9_ob_class_wide_dynamic_01")
-- Object_Type is a tagged type
-- The_Pointer designates Object_Type'Class;
-- Subclass1_Pointer designates Subclass1 (derived from Object_Type)
-- Subclass2_Pointer designates Subclass2 (derived from Subclass1)
-- Subclass3_Pointer designates Subclass3 (derived from Subclass2)
Random_Value := Simple_Random; -- Call to a random number generator
if Random_Value < 1.0/3.0 then
The_Pointer := Subclass1_Pointer;
elsif Random_Value > 2.0/3.0 then
The_Pointer := Subclass2_Pointer;
else
The_Pointer := Subclass3_Pointer;
end if;
Process (The_Pointer.all); -- Tag is unknown
-- (2) Tag is determinable at compile time (static dispatching)
-- (See ACES V2.0, test "a9_ob_class_wide_static_01")
-- Object_Type is a tagged type
-- The_Pointer designates Object_Type'Class;
-- Subclass1_Pointer designates Subclass1 (derived from Object_Type)
-- Subclass2_Pointer designates Subclass2 (derived from Subclass1)
-- Subclass3_Pointer designates Subclass3 (derived from Subclass2)
Random_Value := Simple_Random; -- Call to a random number generator
if Random_Value < 1.0/3.0 then
Process (Subclass1_Pointer.all);
elsif Random_Value > 2.0/3.0 then
Process (Subclass2_Pointer.all);
else
Process (Subclass3_Pointer.all);
end if;
-- (3) No tagged types are involved (no dispatching)
-- (See ACES V2.0, test "ap_ob_class_wide_01")
-- Object_type is a discriminated type with variants; possible
-- discriminant values are Subclass1, Subclass2, and Subclass3
-- All the pointers designate values of Object_Type
-- Subclass1_Pointer := new Object_Type (Subclass1);
-- Subclass2_Pointer := new Object_Type (Subclass2);
-- Subclass3_Pointer := new Object_Type (Subclass3);
-- There is only one "Process" procedure (operating on Object_Type)
Random_Value := Simple_Random; -- Call to a random number generator
if Random_Value < 1.0/3.0 then
Process (Subclass1_Pointer.all);
elsif Random_Value > 2.0/3.0 then
Process (Subclass2_Pointer.all);
else
Process (Subclass3_Pointer.all);
end if;
确定动态和静态子程序调度的影响。编译器可能为一种调度形式而不是另一种形式生成更高效的代码。
动态调度几乎肯定比显式的 if . . . elsif 序列更有效。但是,您应该注意编译器可能做出的任何影响这种情况的优化决策。
- 当测量的性能表明时,只使用简单的聚合。
type Parent is tagged
record
C1 : Float;
C2 : Float;
end record;
type Extension is new Parent with
record
C3 : Float;
C4 : Float;
end record;
Parent_Var : Parent := (C1 => Float_Var1, C2 => Float_Var2);
Exten_Var : Extension;
...
-- Simple aggregate
-- (See ACES V2.0, test "a9_ob_simp_aggregate_02")
Exten_Var := (C1 => Float_Var1, C2 => Float_Var2,
C3 => Float_Var3, C4 => Float_Var4);
-- Extension aggregate
-- (See ACES V2.0, test "a9_ob_ext_aggregate_02")
Exten_Var := (Parent_Var with C3 => Float_Var3, C4 => Float_Var4);
确定使用扩展聚合的影响。简单聚合的评估与扩展聚合的评估之间可能存在显著的性能差异。
- 对于互斥,当测量的性能表明时,使用受保护类型作为任务会合的替代方案。
- 要实现中断处理程序,当性能测量表明时,使用受保护过程。
-- (1) Using protected objects
-- (See ACES V2.0, test "a9_pt_prot_access_02")
protected Object is
function Read return Float;
procedure Write (Value : in Float);
private
Data : Float;
end Object;
protected body Object is
function Read return Float is
begin
return Data;
end Read;
procedure Write (Value : in Float) is
begin
Data := Value;
end Write;
end Object;
task type Modify is
end Modify;
type Mod_Bunch is array (1 .. 5) of Modify;
task body Modify is
...
begin -- Modify
for I in 1 .. 200 loop
The_Value := Object.Read;
Object.Write (The_Value - 0.125);
if The_Value < -1.0E7 then
The_Value := 1.0;
end if;
end loop;
end Modify;
...
-- Block statement to be timed
declare
Contending_Tasks : array (1 .. 5) of Modify;
begin
null; -- 5 tasks contend for access to protected data
end;
------------------------------------------------------------------------------
-- (2) Using monitor task
-- (See ACES V2.0, test "tk_rz_entry_access_02")
Task Object is
entry Write (Value : in Float);
entry Read (Value : out Float);
end Object;
task body Object is
Data : Float;
begin -- Object
loop
select
accept Write (Value : in Float) do
Data := Value;
end Write;
or
accept Read (Value : out Float) do
Value := Data;
end Read;
or
terminate;
end select;
end loop;
end Object;
-- Task type Modify declared as above
-- Block statement to be timed as above
受保护对象旨在比用于相同目的的任务快得多(参见指南 6.1.1)。确定使用受保护对象在并发程序中安全地访问封装数据的性能影响。
- 当测量的性能表明时,使用模块类型而不是压缩布尔数组。
-- (1) Packed Boolean arrays
-- (See ACES V2.0, test "dr_ba_bool_arrays_11")
type Set is array (0 .. 15) of Boolean;
pragma Pack (Set);
S1 : Set;
S2 : Set;
Empty : Set := (Set'Range => False);
Result : Boolean;
...
-- Is S1 a subset of S2?
Result := ((S1 and not S2) = Empty);
---------------------------------------------------------------------
-- (2) Modular types
-- (See ACES V2.0, test "a9_ms_modular_oper_02")
type Set is mod 16;
S1 : Set;
S2 : Set;
Empty : Set := 0;
Result : Boolean;
...
-- Is S1 a subset of S2?
Result := ((S1 and not S2) = Empty);
确定对模块类型执行位操作的影响。这些操作的性能可能与对压缩布尔数组执行的类似操作有很大不同。另请参见指南 10.5.7。
- 当可预测的性能是一个问题并且测量的性能表明时,使用预定义的定长字符串。
不定长字符串可能分配在堆上。如果定长字符串没有分配在堆上,那么它们可能会提供更好的性能。确定在 Ada.Strings.Bounded.Generic_Bounded_Length 的实例中声明的字符串类型与在 Ada.Strings.Unbounded 中声明的类型之间的性能影响。
预定义的 Ada 95 语言环境定义了支持定长和不定长字符串的包。使用定长字符串可以避免与使用堆存储相关的延迟的不可预测持续时间。
- 当测量的性能指标表明时,使用字符串处理子程序的程序形式。
确定 Ada.Strings.Fixed、Ada.Strings.Bounded、Ada.Strings.Unbounded 以及包含 Wide 的相应子包中具有相同名称和功能的函数和过程的相对性能成本。
虽然函数式表示法通常会导致更清晰的代码,但它可能会导致编译器生成额外的复制操作。
- 当测量的性能指标表明时,使用强类型和精心选择的约束来减少运行时约束检查。
在这个例子中,消除了两个潜在的约束检查。如果函数 Get_Response 返回 String,那么变量 Input 的初始化将需要约束检查。如果变量 Last 的类型为 Positive,那么循环中的赋值将需要约束检查。
...
subtype Name_Index is Positive range 1 .. 32;
subtype Name is String (Name_Index);
...
function Get_Response return Name is separate;
...
begin
...
Find_Last_Period:
declare
-- No Constraint Checking needed for initialization
Input : constant Name := Get_Response;
Last_Period : Name_Index := 1;
begin -- Find_Last_Period
for I in Input'Range loop
if Input(I) = '.' then
-- No Constraint Checking needed in this `tight' loop
Last_Period := I;
end if;
end loop;
...
end Find_Last_Period;
由于运行时约束检查与性能下降有关,因此添加约束子类型实际上可以提高性能并不直观。但是,无论是否使用约束子类型,都需要在许多地方进行约束检查。即使是对使用预定义子类型的变量进行赋值也可能需要约束检查。通过始终如一地使用约束子类型,可以消除许多不必要的运行时检查。相反,检查通常被移到与系统输入相关的执行频率较低的代码中。在这个例子中,函数 Get_Response 可能需要检查用户提供的字符串的长度并引发异常。
一些编译器可以根据约束子类型提供的信息进行额外的优化。例如,虽然非约束数组没有固定大小,但它具有可以从其索引范围确定的最大大小。通过将此最大大小限制为“合理”的数字,可以提高性能。请参考 NASA (1992) 中关于非约束数组的讨论。
- 对于那些既有 rendezvous 又有受保护类型都不高效的情况,请考虑使用实时系统附录 (Ada 参考手册 1995,附录 D [带注释的])。
Ada.Synchronous_Task_Control 和 Ada.Asynchronous_Task_Control 包已被定义为提供对任务和受保护类型的替代方案,用于在需要最小运行时的应用程序中 (Ada 参考手册 1995,附录 D [带注释的])。
- 当测量的性能指标表明时,在调用开销是例程执行时间的重要部分时,使用 pragma Inline。
procedure Assign (Variable : in out Integer;
Value : in Integer);
pragma Inline (Assign);
...
procedure Assign (Variable : in out Integer;
Value : in Integer) is
begin
Variable := Value;
end Assign;
如果调用开销是子程序执行时间的重要部分,那么使用 pragma Inline 可能会减少执行时间。
过程和函数调用包括在代码很小时不必要的开销。这些小的例程通常是为了维护包的实现隐藏特性而编写的。它们也可能只是将它们的參數不变地传递给另一个例程。当这些例程之一出现在需要更快运行的某些代码中时,要么需要违反实现隐藏原则,要么需要引入 pragma Inline。
使用 pragma Inline 确实有其缺点。它会创建对主体文件的编译依赖;也就是说,当规范使用 pragma Inline 时,规范和对应的主体文件都需要在规范可以被使用之前进行编译。随着代码的更新,例程可能会变得更加复杂(更大),并且继续使用 pragma Inline 可能不再合理。
虽然很少见,但内联代码可能会增加代码大小,这可能会导致由于额外的分页而导致性能下降。pragma Inline 实际上可能会阻止编译器使用某些其他优化技术,例如寄存器优化。
当编译器已经很好地选择例程进行内联时,pragma 可能几乎不会带来任何性能提升。
- 使用 pragma Restrictions 来表达用户遵守某些限制的意图。
这可能有助于构建更简单的运行时环境 (Ada 参考手册 1995,§§13.12 [带注释的], D.7 [带注释的], 和 H.4 [带注释的])。
- 在允许的情况下使用 pragma Preelaborate。
这可能会减少加载后内存写入操作(Ada 参考手册 1995,§§10.2.1 [注释] 和 C.4 [注释])。
- 在允许的情况下使用 Pragma Pure。
这可能允许编译器省略对库单元中库级子程序的调用,如果在调用之后不需要结果(Ada 参考手册 1995,§10.2.1 [注释])。
- 当应用程序不需要名称且数据空间非常宝贵时,使用 Pragma Discard_Names。
这可能会减少存储 Ada 实体名称所需的内存,其中没有操作使用这些名称(Ada 参考手册 1995,§C.5 [注释])。
- 在需要的情况下使用 Pragma Suppress 以实现性能要求。
参见指南 5.9.5。
- 使用 Pragma Reviewable 来帮助分析生成的代码。
- 谨慎使用本章中的指南;它们可能对您的软件有害。
- 当测量到的性能表明时,使用块来引入延迟初始化。
- 当测量性能表明时,使用约束数组。
- 当测量性能表明时,为数组使用基于零的索引。
- 当测量性能表明时,为记录使用固定大小的组件。
- 当测量的性能表明时,将记录数组定义为并行数组。
- 当测量的性能表明时,使用一系列赋值来进行聚合。
- 当测量的性能表明时,使用增量方案而不是 mod 和 rem。
- 当测量的性能表明时,使用短路控制形式。
- 当测量的性能表明时,使用 case 语句。
- 当测量的性能表明时,使用硬编码约束检查。
- 当测量的性能表明时,使用二维数组的列优先处理。
- 当测量的性能表明时,使用覆盖来进行条件赋值。
- 当测量的性能表明时,通过使用切片赋值而不是重复位赋值来执行压缩布尔数组移位操作。
- 当测量到的性能表明时,使用静态子程序调度。
- 当测量的性能表明时,只使用简单的聚合。
- 对于互斥,当测量的性能表明时,使用受保护类型作为任务会合的替代方案。
- 为了实现中断处理程序,当测量到的性能表明时,使用受保护的过程。
- 当测量的性能表明时,使用模块类型而不是压缩布尔数组。
- 当可预测的性能是一个问题并且测量的性能表明时,使用预定义的定长字符串。
- 当测量的性能指标表明时,使用字符串处理子程序的程序形式。
- 当测量的性能指标表明时,使用强类型和精心选择的约束来减少运行时约束检查。
- 对于既是 rendezvous 又受保护类型效率低下的情况,请考虑使用实时系统附录((Ada 参考手册 1995,附录 D [注释])。
- 当测量的性能指标表明时,在调用开销是例程执行时间的重要部分时,使用 pragma Inline。
- 使用 pragma Restrictions 来表达用户遵守某些限制的意图。
- 在允许的情况下使用 pragma Preelaborate。
- 在允许的情况下使用 Pragma Pure。
- 当应用程序不需要名称且数据空间非常宝贵时,使用 Pragma Discard_Names。
- 在需要的情况下使用 Pragma Suppress 以实现性能要求。
- 使用 Pragma Reviewable 来帮助分析生成的代码。