该函数将数组向量 A 中的 n 个浮点值相加。在循环体中,算法计算 一个“修正”值,然后将其应用于求和的下一步。与简单的求和相比,该方法大大减小了累积性舍入 误差,同时保持了 O(n) 时间复杂性。
一个不完善的 C++ 编译器可能假设浮点算法遵循与实数算法相同的代数规则。这样的编译器可能 继而错误地断定
C = T - sum - Y ==> (sum+Y)-sum-Y ==> 0;
也就是说,C 得到的值总是常量零。如果随后将该常量值传播到后续表达式中,循环体将化简为简 单的求和。更准确地说,就是
Y = A[i] - C ==> Y = A[i]
T = sum + Y ==> T = sum + A[i]
sum = T ==> sum = sum + A[i]
因此,对于不完善的编译器而言,KahanSum 函数的逻辑转换将是:
尽管转换后的算法更快,但它根本没有准确表达程序员的意图。精心设计的误差修正已经 被完全消除,只剩下一个具有所有其关联误差的简单的直接求和算法。
当然,完善的 C++ 编译器知道实数算法的代数规则通常并不适用于浮点算法。然而,即使是完善 的 C++ 编译器,也可能错误地解释程序员的意图。
考虑一种常见的优化措施,它试图在寄存器中存放尽可能多的值(称为“登记”值)。在 KahanSum 示例中,这一优化可能试图登记变量 C、Y 和 T,因为这些变量仅在循环体内使用。如果寄存器精度为 52 位(双精度)而不是 23 位(单精度),这一优化可以有效地将 C、Y 和 T 的类 型提升为 double。如果没有以同样的方式登记 sum 变量,则它仍将编 码为单精度。这会将 KahanSum 的语义转换为下面的语义
|
float KahanSum( const float A[], int n ) { float sum=0; double C=0, Y, T; // now held in-register for (int i=0; i<n; i++) { Y = A[i] - C; T = sum + Y; C = T - sum - Y; sum = (float) T; } return sum; } |
尽管现在 Y、T 和 C 以更高的精度进行计算,但新的编码可能产生精确性较低的结果,具体取决 于 A[] 中的值。因而,即使看起来无害的优化也可能具有消极的后果。
这些种类的优化问题并不局限于“棘手”的浮点代码。即使是简单的浮点算法,在经过错误的优化 后也可能失败。考虑一个简单的直接求和算法:
|
float Sum( const float A[], int n ) { float sum=0; for (int i=0; i<n; i++) sum = sum + A[i]; return sum; } |
因为一些浮点单元能够同时执行多个运算,所以编译器可能选择采用标量简化 优化。这一 优化有效地将简单的 Sum 函数从上述形式转换为以下形式:
|
float Sum( const float A[], int n ) { int n4 = n-n%4; // or n4=n4&(~3) int i; float sum=0, sum1=0, sum2=0, sum3=0; for (i=0; i<n4; i+=4) { sum = sum + A[i]; sum1 = sum1 + A[i+1]; sum2 = sum2 + A[i+2]; sum3 = sum3 + A[i+3]; } sum = sum + sum1 + sum2 + sum3; for (; i<n; i++) sum = sum + A[i]; return sum; } |
该函数现在保持了四个独立的求和运算,它们可以在每个步骤同时处理。尽管优化后的函数现在要 快得多,但优化结果可能与非优化结果完全不同。在进行这一变化时,编译器采用了具有结合律的浮 点加法;即以下两个表达式等效:(a+b)+c == a+(b+c)。然而,对于浮点数而言,结合律并不总是适 用。现在,转换后的函数不是按以下方法求和:
sum = A[0]+A[1]+A[2]+...+A[n-1]
而是按以下方法计算结果:
sum = (A[0]+A[4]+A[8]+...)
+(A[1]+A[5]+A[9]+...)
+(A[2]+A[6]+A[10]+...)
+(A[3]+A[7]+A[11]+...)
+...
对于 A[] 的某些值而言,不同的加法运算顺序可能产生意外的结果。更为复杂的是,某些程序员 可能选择预先针对此类优化做准备,并相应地对这些优化进行补偿。在此情况下,程序可以按不同的 顺序构建数组 A,以便优化的 sum 产生预期的结果。而且,在许多情况 下,优化结果的精确性可能“足够严密”。当优化提供了令人信服的速度优点时,尤其如此。例如, 视频游戏要求具有尽可能快的速度,但通常并不要求进行高度精确的浮点计算。因此,编译器制造商 必须为程序员提供一种机制,以便控制速度和精确性之间经常背离的目标。
某些编译器通过为每种类型的优化单独提供“开关”在速度和精确性之间进行折衷。这使开发人员 可以禁用可能为其特定应用程序的浮点精确性带来变化的优化。尽管该解决方案可能提供对编译器的 高度控制,但它也会带来其他一些问题:
• 通常很难搞清楚需要启用或禁用哪些开关。
• 禁用任一优化都可能对非浮点代码的性能带来不利影响。
• 每个附加的开关都会引起许多新的开关组合;组合数目将很快变得难以控制。
因此,尽管为每种优化提供单独的开关看起来似乎很有吸引力,但使用此类编译器可能非常麻烦并 且不可靠。
许多 C++ 编译器提供了“一致性”浮点模型(通过 /Op 或 /fltconsistency 开关),从而使开 发人员能够创建符合严格浮点语义的程序。采用该模型时,可以防止编译器对浮点计算使用大多数优 化,同时允许其对非浮点代码使用这些优化。但是,该一致性模型具有一个缺点。为了在不同的 FPU 体系结构中返回可预测的结果,几乎所有 /Op 实现都将中间表达式舍入到用户指定的精度;例如,考 虑下面的表达式:
float a, b, c, d, e;
. . .
a = b*c + d*e;
为了在使用 /Op 开关时产生一致的且可重复的结果,该表达式的计算方式按如下方式实现:
float x = b*c;
float y = d*e;
a = x+y;
现在,最终结果在计算该表达式的每一步 中都产生了单精度舍入误差。尽管这种解释在严 格意义上并未破坏任何 C++ 语义规则,但它几乎肯定不是计算浮点表达式的最佳方法。通常,以 尽可能高的可行精度计算中间结果 更为可取。例如,以如下所示的较高精度计算表达式 a=b*c+d*e 将会更好:
double x = b*c;
double y = d*e;
double z = x+y;
a = (float)z;
或者,采用以下方式会更好:
long double x = b*c;
long double y = d*e
long double z = x+y;
a = (float)z;
在以较高精度计算中间结果时,最终结果显然会更为精确。具有讽刺意味的是,如果采用一致性模 型,则当用户试图通过禁用不安全的优化来减少误差时,出现误差的可能性却恰恰增加了。因此,一 致性模型不仅严重降低了效率,同时还无法对精确性的提高提供任何保证。对于认真的数值程序员而 言,这看起来不像是一个很好的折衷,这也是该模型没有被广泛接受的主要原因。
从版本 8.0 (Visual C++?2005) 开始,Microsoft C++ 编译器提供了一种更好的选择。它使程序 员可以选择以下三种常规浮点模式之一:fp:precise、fp:fast 和 fp:strict。
• 在 fp:precise 模式下,仅对浮点代码执行安全优化,并且与 /Op 不同,以最 高可行 精度一致性地执行中间计算。
• fp:fast 模式放松了浮点规则,允许以牺牲精确性为代价进行更为积极的优化。
• fp:strict 模式提供了 fp:precise 的所有常规正确性,同时启用了 fp- exception 语义,并禁止在存在 FPU 环境更改(例如,对寄存器精度、舍入方向的更改等等)时进行 非法转换。
可以通过命令行开关或编译器杂注单独控制浮点异常语义;默认情况下,在 fp:precise 模式下禁 用浮点异常语义,而在 fp:strict 模式下启用该语义。编译器还提供了对 FPU 环境敏感性和某些特 定于浮点的优化(如化简)的控制。这一简单明了的模型为开发人员提供了大量针对浮点代码编译的 控制,并且无须使用太多的编译器开关,也不会带来令人讨厌的副作用。
浮点语义的 fp:precise 模式
默认的浮点语义模式是 fp:precise。如果选择该模式,编译器在优化浮点操作时将严格遵守一组 安全规则。这些规则使编译器可以在保持浮点计算精确性的前提下生成高效的机器码。为便于产生快 速的程序,fp:precise 模式禁用了浮点异常语义(尽管可以显式启用这些语义)。Microsoft 已经选 择 fp:precise 作为默认的浮点模式,因为这种模式能够创建既快速又精确的程序。
要使用命令行编译器显式请求 fp:precise,请使用 -fp:precise 开关:
cl -fp:precise source.cpp
这将指示编译器在为 source.cpp 文件生成代码时使用 fp:precise 语义。还可以使用 float_control 编译器杂注逐个函数地调用 fp:precise 模式。
在 fp:precise 模式下,编译器绝不会执行任何干扰浮点计算精确性的优化。编译器在执行赋值、 类型转换和函数调用时将始终正确地进行舍入,并且将按照与 FPU 寄存器相同的精度一致地执行中间 舍入。默认情况下,将启用安全优化(如化简)。默认情况下,将禁用异常语义和 FPU 环境敏感性。
fp:precise 浮点语义 |
解释 |
Rounding Semantics |
在执行赋值、类型转换和函数调用时进行显式舍入,并且按寄存器精 度计算中间表达式。 |
Algebraic Transformations |
严格遵守非结合性、非分配性的浮点代数,除非能够保证转换总是产 生相同的结果。 |
化简 |
默认情况下允许(另请参阅 The fp_contract Pragma)。 |
Order of Floating-point Evaluation |
如果不会改变最终结果,编译器可能重新排列浮点表达式的计算顺序。 |
FPU Environment Access |
默认情况下禁用(另请参阅 The fpenv_access Pragma)。采用默认的精度和舍入模式。 |
Floating-point Exception Semantics |
默认情况下禁用(另请参阅 fp:except switch)。 |
fp:precise 模式下浮点表达式的舍入语义
fp:precise 模式总是以最高的可行 精度执行中间计算,只在表达式计算过程中的特定位 置执行显式舍入。总是 在下列四个位置舍入到用户指定的精度:(a) 进行赋值时;(b) 执行 类型转换时;(c) 浮点值作为参数传递给函数时;(d) 从函数返回浮点值时。因为总是按寄存器精度 执行中间计算,所以中间结果的精确性与平台相关(尽管精度将总是起码与用户指定的精度相当)。
考虑以下代码中的赋值表达式。赋值运算符“=”右侧的表达式将按寄存器精度计算,然后显式舍 入到赋值运算符左侧的类型。
float a, b, c, d;
double a;
...
x = a*b + c*d;
被计算为
float a, b, c, d;
double a;
...
register tmp1 = a*b;
register tmp2 = c*d;
register tmp3 = tmp1+tmp2;
x = (double) tmp3;
要显式舍入中间结果,请引入类型转换。例如,如果通过添加显式类型转换来修改前面的代码,中 间表达式 (c*d) 将被舍入到类型转换的类型。
float a, b, c, d;
double a;
. . .
x = a*b + (float)(c*d);
被计算为
float a, b, c, d;
double a;
. . .
register tmp1 = a*b;
float tmp2 = c*d;
register tmp3 = tmp1+tmp2;
x = (double) tmp3
该舍入方法的一个含义是某些似乎等效的转换实际上并不具有完全相同的语义。例如,下面的转换 将一个赋值表达式拆分为两个赋值表达式。
float a, b, c, d;
. . .
a = b*(c+d);
“不”等效于
float a, b, c, d;
. . .
a = c+d;
a = b*a;
同样:
a = b*(c+d);
“不”等效于
a = b*(a=c+d);
这些编码方式不具有等效的语义,因为第二种编码方式引入了附加的赋值操作,由此引入了附加的 舍入点。
当函数返回浮点值时,该值将被舍入到函数的类型。当浮点值作为参数传递给函数时,该值将被舍 入到参数的类型。例如:
|
float sumsqr(float a, float b) { return a*a + b*b; } |
被计算为
|
float sumsqr(float a, float b) { register tmp3 = a*a; register tmp4 = b*b; register tmp5 = tmp3+tmp4; return (float) tmp5; } |
同样:
float w, x, y, z;
double c;
...
c = symsqr(w*x+y, z);
被计算为
float x, y, z;
double c;
...
register tmp1 = w*x;
register tmp2 = tmp1+y;
float tmp3 = tmp2;
c = symsqr( tmp3, z);
fp:precise 模式下特定于体系结构的舍入
处理器 |
中间表达式的舍入精度 |
x86 |
按照具有 16 位指数所提供的扩展范围的、默认的 53 位精度计算中 间表达式。当这些 53:16 值被“倒”入内存时(这在函数调用期间可能发生),扩展指数范围将被缩 小到 11 位。也就是说,倒入内存的值被转换为标准的双精度格式,只具有 11 位指数。
通过使用 _controlfp 改变浮点控制字或者通过启用 FPU 环境访问,用户可以为中间结果 的舍入切换到扩展的 64 位精度(请参阅 fpenv_access 杂注)。然而,当扩展精度寄存器值被倒入内存时,中间结果仍 将舍入到双精度。
这一特定语义随时可能更改。 |
ia64 |
中间表达式总是按 64 位精度(即扩展精度)计算。 |
amd64 |
amd64 上的 FP 语义与其他平台有所不同。出于性能原因,中间运算 按各个操作数的最高精度计算,而不是按可用的最高精度计算。要强迫计算使用比操作数更高的精度 执行,用户需要至少对子表达式中的一个操作数引入转换操作。
这一特殊语义随时可能更改。 |
fp:precise 模式下的代数转换
当启用 fp:precise 模式时,编译器绝不会执行代数转换,除非可以证明最终结果完全相同 。对于浮点算法而言,许多惯用的实数算法代数规则有时并不适用。例如,下列表达式对于实数 是等效的,但对于浮点数却未必等效。
形式 |
说明 |
(a+b)+c = a+(b+c) |
加法结合律 |
(a*b)*c = a*(b*c) |
乘法结合律 |
a*(b+c) = a*b + b*c |
乘法对于加法的分配律 |
(a+b)(a-b) = a*a-b*b |
代数因式分解 |
a/b = a*(1/b) |
通过乘法逆元素计算除法 |
a*1.0 = a |
乘法恒等式 |
正如本文开始时介绍的示例函数 KahanSum 中所描述的,编译器可能倾向于执行各种代数转换,以 便产生速度快得多的程序。尽管依赖于此类代数转换的优化几乎总是不正确的,但有时这些优化是完 全安全的。例如,有时将除以一个常量 值替换为乘以该常量的倒数是可取的:
const double four = 4.0;
double a, b;
...
a = b/four;
可以转换为
const double four = 4.0;
const double tmp0 = 1/4.0;
double a, b;
...
a = b*tmp0;
该转换是安全的,因为优化器在编译时可以确定:对于所有浮点值 x(包括无穷大和 NaN), x/4.0 == x*(1/4.0)。通过将除法运算替换为乘法运算,编译器可以省去多个循环 — 尤其是在未直 接实现除法的 FPU 上,但要求编译器生成倒数近似和乘法-加法指令的组合。在 fp:precise 模式下 ,只有当替换乘法能够产生与除法完全相同的结果时,编译器才会执行此类优化。在 fp:precise 模 式下,编译器还可能执行一些普通的转换,但前提是结果完全相同。这些转换包括:
形式 |
说明 |
(a+b) == (b+a) |
加法交换律 |
(a*b) == (b*a) |
乘法交换律 |
1.0*x*y == x*1.0*y == x*y*1.0 == x*y |
乘以 1.0 |
x/1.0*y == x*y/1.0 == x*y |
除以 1.0 |
2.0*x == x+x |
乘以 2.0 |
fp:precise 模式下的化简
许多现代浮点单元的一项主要体系结构功能,是能够将一个乘法与后面紧跟的加法作为单个运算执 行,且没有中间舍入误差。例如,Intel 的 Itanium 体系结构提供了一些指令,将三元运算 (a*b+c) 、(a*b-c) 和 (c-a*b) 中的每一个都组合为单个浮点指令(分别为 fma、fms 和 fnma)。这些单个 指令都比执行独立的乘法和加法指令快,并且因为没有中间乘法舍入,所以更为精确。该优化可以显 著提高那些含有多个交错乘法和加法运算的函数的速度。例如,考虑以下计算两个 n 维向量的点积的 算法。
|
float dotProduct( float x[], float y[], int n ) { float p=0.0; for (int i=0; i<n; i++) p += x[i]*y[i]; return p; } |
该计算可以通过一系列乘法-加法指令执行,形式为:p = p + x[i]*y[i] 。
该化简优化可以使用 fp_contract 编译器杂注单独进行控制。默认情况下,fp:precise 模式允许进行化简,因为它们能够提高精确性和速度。在 fp:precise 模式下,编译器不会化简带有 显式舍入运算的表达式。
示例
float a, b, c, d, e, t;
...
d = a*b + c; // may be contracted
d += a*b; // may be contracted
d = a*b + e*d; // may be contracted into a mult followed by a mult-add
etc...
d = (float)a*b + c; // won't be contracted because of explicit rounding
t = a*b; // (this assignment rounds a*b to float)
d = t + c; // won't be contracted because of rounding of a*b
fp:precise 模式下浮点表达式计算的顺序
能够保留浮点表达式计算顺序的优化总是安全的,因而在 fp:precise 模式下是允许的。考虑下面 的函数,该函数计算两个单精度 n 维向量的点积。下面的第一个代码块是程序员可能编写的原函数, 后面跟着的是经过局部循环展开优化之后的同一函数(改动部分用斜体 表示)。
|
//original function float dotProduct( float x[], float y[], int n ) { float p=0; for (int i=0; i<n; i++) p += x[i]*y[i]; return p; }
//after a partial loop-unrolling float dotProduct( float x[], float y[], int n ) { int n4= n/4*4; // or n4=n&(~3); float p=0; int i; for (i=0; i<n4; i+=4) { p+=x[i]*y[i]; p+=x[i+1]*y[i+1]; p+=x[i+2]*y[i+2]; p+=x[i+3]*y[i+3]; } // last n%4 elements for (; i<n; i++) p+=x[i]*y[i]; return p; } |
该优化的主要优点是它将条件循环分支的数量减少了 75%。同时,通过增加循环体内运算的数量, 编译器现在可以有更多的机会来进一步优化。例如,某些 FPU 或许能够在执行 p+=x[i]*y[i] 中的乘 法-加法的同时取出 x[i+1] 和 y[i+1] 的值,以便在下一步中使用。此类优化对于浮点计算而言是完 全安全的,因为它保留了运算的顺序。
对于编译器来说,重新排列全部运算的顺序以便产生更快的代码通常是有利的。考虑以下代码:
double a, b, c, d;
double x, y, z;
...
x = a*a*a + b*b*b + c*c*c;
...
y = a*a + b*b + c*c;
...
z = a + b + c;
C++ 语义规则表明,程序产生结果的顺序应该好像 它首先计算 x,然后计算 y,最后计算 z。假设编译器只有四个可用的浮点寄存器。如果编译器被迫按顺序计算 x、y 和 z,它可能选择用下 面的语义来生成代码:
double a, b, c, d;
double x, y, z;
register r0, r1, r2, r3;
...
// Compute x
r0 = a; // r1 = a*a*a
r1 = r0*r0;
r1 = r1*r0;
r0 = b; // r2 = b*b*b
r2 = r0*r0;
r2 = r2*r0;
r0 = c; // r3 = c*c*c
r3 = r0*r0;
r3 = r3*r0;
r0 = r1 + r2;
r0 = r0 + r3;
x = r0; // x = r1+r2+r3
. . .
// Compute y
r0 = a; // r1 = a*a
r1 = r0*r0;
r0 = b; // r2 = b*b
r2 = r0*r0;
r0 = c; // r3 = c*c
r3 = r0*r0;
r0 = r1 + r2;
r0 = r0 + r3;
y = r0; // y = r1+r2+r3
. . .
// Compute z
r1 = a;
r2 = b;
r3 = c;
r0 = r1 + r2;
r0 = r0 + r3;
z = r0; // z = r1+r2+r3
在这种编码方式中,有几个明显多余的运算(用斜体 表示)。如果编译器严格遵 守 C++ 语义规则,则这一顺序是必要的,因为程序可能在每次赋值内部访问 FPU 环境。但是, fp:precise 的默认设置允许编译器进行优化,就好像程序没有访问该环境一样,从而允许其重新排列 这些表达式的顺序。因而,编译器可以自由地通过按相反顺序计算上述三个值来消除冗余,如下所示 :
double a, b, c, d;
double x, y, z;
register r0, r1, r2, r3;
...
// Compute z
r1 = a;
r2 = b;
r3 = c;
r0 = r1+r2;
r0 = r0+r3;
z = r0;
...
// Compute y
r1 = r1*r1;
r2 = r2*r2;
r3 = r3*r3;
r0 = r1+r2;
r0 = r0+r3;
y = r0;
...
// Compute x
r0 = a;
r1 = r1*r0;
r0 = b;
r2 = r2*r0;
r0 = c;
r3 = r3*r0;
r0 = r1+r2;
r0 = r0+r3;
x = r0;
这一编码方式显然要更好,它将浮点指令的数量减少了将近 40%。x、y 和 z 的结果与原来相同, 但计算时花费了较少的系统开销。
在 fp:precise 模式下,编译器还可以交错 计算常见的子表达式,从而产生更快的代码。 例如,可以按以下方式编写用于计算二次方程根的代码:
double a, b, c, root0, root1;
...
root0 = (-b + sqrt(b*b-4*a*c))/(2*a);
root1 = (-b - sqrt(b*b-4*a*c))/(2*a);
尽管这些表达式之间的不同仅在于一个运算,但程序员这样编写代码可以保证以最高的可行精度计 算每个根值。在 fp:precise 模式下,编译器可以自由地交错计算 root0 和 root1,以便消除常见的 子表达式,而不会丢失精度。例如,下面的代码已经消除了几个多余的步骤,同时还能够产生完全相 同的答案。
double a, b, c, root0, root1;
...
register tmp0 = -b;
register tmp1 = sqrt(b*b-4*a*c);
register tmp2 = 2*a;
root0 = (tmp0+tmp1)/tmp2;
root1 = (tmp0-tmp1)/tmp2;
其他优化可能尝试移动某些独立表达式的计算。考虑下面的算法,它在循环体内含有一个条件分支 。
vector a(n);
double d, s;
. . .
for (int i=0; i<n; i++)
{
if (abs(d)>1.0)
s = s+a[i]/d;
else
s = s+a[i]*d;
}
编译器可能检测到表达式 (abs(d)>1) 的值在循环体内保持不变。这使编译器可以将 if 语句“提 升”到循环体外部,从而将上述代码转换为以下形式:
vector a(n);
double d, s;
. . .
if (abs(d)>1.0)
for (int i=0; i<n; i++)
s = s+a[i]/d;
else
for (int i=0; i<n; i++)
s = s+a[i]*d;
进行转换之后,在任一循环体内都不再有条件分支,从而显著提高了循环的总体性能。这种类型的 优化是完全安全的,因为表达式 (abs(d)>1.0) 的计算独立于其他表达式 。
如果存在 FPU 环境访问或浮点异常,则上述类型的优化是不可取的,因为它们改变了语义流。此 类优化仅在 fp:precise 模式下可用,因为 FPU 环境访问和浮点异常语义默认情况下被禁用。访问 FPU 环境的函数可以通过使用 fenv_access 编译器杂注显式禁用此类优化。同样,使用浮点 异常的函数应该使用 float_control(except…) 编译器杂注(或者使用 /fp:except 命令行开关)。
总之,fp:precise 模式允许编译器重新排列浮点表达式的计算顺序,前提是最终结果不会改变, 并且结果不依赖于 FPU 环境或浮点异常。
fp:precise 模式下的 FPU 环境访问
当启用 fp:precise 模式时,编译器将假设程序不会访问或改变 FPU 环境。如前所述,这一假设 使编译器能够重新排列或移动浮点运算,以便提高 fp:precise 模式下的效率。
某些程序可能通过使用 _controlfp 函数来改变浮点舍入方向。例如,某些程序通过执行 同一计算两次来计算算术运算的上下误差边界:第一次向负无穷舍入,第二次向正无穷舍入。因为 FPU 提供了控制舍入的方便方法,所以程序员可以选择通过改变 FPU 环境来更改舍入模式。下面的代 码通过改变 FPU 环境来计算浮点乘法的确切误差范围。
double a, b, cLower, cUpper;
. . .
_controlfp( _RC_DOWN, _MCW_RC ); // round to -a??
cLower = a*b;
_controlfp( _RC_UP, _MCW_RC ); // round to +a??
cUpper = a*b;
_controlfp( _RC_NEAR, _MCW_RC ); // restore rounding mode
在 fp:precise 模式下,编译器总是采用默认的 FPU 环境,因此编译器可以自由地忽略对 _controlfp 的调用并将上述赋值简化为 cUpper = cLower = a*b ;这显然会产生不正确的结果。要避免此类优化,请通过使用 fenv_access 编译器杂注来启 用 FPU 环境访问。
其他程序可能尝试通过检查 FPU 的状态字来检测某些浮点错误。例如,下面的代码检查是否存在 被零除和不精确的状态
double a, b, c, r;
float x;
. . .
_clearfp();
r = (a*b + sqrt(b*b-4*a*c))/(2*a);
if (_statusfp() & _SW_ZERODIVIDE)
handle divide by zero as a special case
_clearfp();
x = r;
if (_statusfp() & _SW_INEXACT)
handle inexact error as a special case
etc...
在 fp:precise 模式下,将表达式计算重新排序的优化可能改变某些错误发生的位置。访问状态字 的程序应该通过使用 fenv_access 编译器杂注来启用 FPU 环境访问。
有关更多信息,请参阅杂注 fenv_access
fp:precise 模式下的浮点异常语义
在 fp:precise 模式下,默认情况下禁用浮点异常语义。大多数 C++ 程序员在处理异常浮点状态 时不喜欢使用系统和 C++ 异常。而且,正如前面所述,在优化浮点运算时,禁用浮点异常语义可以使 编译器获得较大的灵活性。在使用 fp:precise 模式时,请使用 fp:except 开关或 fp_control 杂注 来启用浮点异常语义。
另请参阅
启用浮点异常语义
浮点语义的 fp:fast 模式
如果启用 fp:fast 模式,编译器在优化浮点运算时将放松 fp:precise 使用的规则。该模式允许 编译器进一步优化浮点代码,以牺牲浮点精确性和正确性为代价换取速度。通过启用 fp:fast 模式, 那些不依赖高度精确浮点计算的程序可能在速度方面获得显著的提高。
可以使用命令行编译器开关来启用 fp:fast 浮点模式,如下所示:
cl -fp:fast source.cpp
or
cl /fp:fast source.cpp
该示例指示编译器在为 source.cpp 文件生成代码时使用 fp:fast 语义。还可以使用 float_control 编译器杂注逐个函数地调用 fp:fast 模式。
另请参阅
float_control 杂注
在 fp:fast 模式下,编译器可能执行改变浮点计算精确度的优化。编译器在执行赋值、类型转换 或函数调用时可能无法正确地舍入,并且不总是执行中间舍入。特定于浮点的优化(如化简)总是被 启用。浮点异常语义和 FPU 环境敏感性被禁用,因而不可用。
fp:fast 浮点语义 |
解释 |
舍入语义 |
在执行赋值、类型转换和函数调用时的显式舍入可能被忽略。
可能按照性能要求以低于寄存器的精度舍入中间表达式。 |
代数转换 |
编译器可能按照实数结合性、分配性代数转换表达式;不能保证这些 转换的精确性和正确性。 |
化简 |
总是启用;不能通过杂注 fp_contract 禁用 |
浮点计算的顺序 |
编译器可以将浮点表达式的计算重新排序,即使这种更改可能改变最 终的结果。 |
FPU 环境访问 |
禁用。不可用 |
浮点异常语义 |
禁用。不可用 |
fp:fast 模式下浮点表达式的舍入语义
与 fp:precise 模式不同,fp:fast 模式以最方便的精度执行中间计算。在执行赋值、类型转换和 函数调用时不总是发生舍入。例如,下面的第一个函数引入了三个单精度变量(C、 Y 和 T)。编译器可能选择登记这些变量,以便将 C、Y 和 T 的类型提升到双精度。
原函数:
|
float KahanSum( const float A[], int n ) { float sum=0, C=0, Y, T; for (int i=0; i<n; i++) { Y = A[i] - C; T = sum + Y; C = T - sum - Y; sum = T; } return sum; } |
变量被登记:
|
float KahanSum( const float A[], int n ) { float sum=0; double C=0, Y, T; // now held in-register for (int i=0; i<n; i++) { Y = A[i] - C; T = sum + Y; C = T - sum - Y; sum = (float) T; } return sum; } |
在该示例中,fp:fast 推翻了原函数的意图。最终的优化结果(保存在变量 sum 中)可能 与正确的结果大不相同。
在 fp:fast 模式下,编译器通常会试图起码保持源代码所指定的精度。然而,在某些情况下,编 译器可能选择以比源代码所指定的精度更低的精度 来执行中间表达式。例如,下面的第一段 代码块调用了平方根函数的双精度版本。在 fp:fast 模式下,编译器可以选择将对双精度 sqrt 的调用替换为对单精度 sqrt 函数的调用。这样做的结果是在执行函数调用时引 入附加的低精度舍入。
原函数
double sqrt(double)...
. . .
double a, b, c;
. . .
double length = sqrt(a*a + b*b + c*c);
优化函数
float sqrtf(float)...
. . .
double a, b, c;
. . .
double tmp0 = a*a + b*b + c*c;
float tmp1 = tmp0; // round of parameter value
float tmp2 = sqrtf(tmp1); // rounded sqrt result
double length = (double) tmp2;
尽管精确性降低,但在面向那些提供函数(如 sqrt)的单精度、内部版本的处理器时,这 一优化可能特别有用。编译器究竟何时使用此类优化取决于平台和上下文。
而且,无法保证中间计算的精度的一致性,可以在编译器可用的任意精度级别执行中间计算。尽管 编译器将试图起码保持代码所指定的精度级别,但 fp:fast 允许优化器降低中间计算的精度,以便产 生更快或更小的机器码。例如,编译器可以进一步优化上述代码,将某些中间乘法舍入到单精度。
float sqrtf(float)...
. . .
double a, b, c;
. . .
float tmp0 = a*a; // round intermediate a*a to single- precision
float tmp1 = b*b; // round intermediate b*b to single- precision
double tmp2 = c*c; // do NOT round intermediate c*c to single- precision
float tmp3 = tmp0 + tmp1 + tmp2;
float tmp4 = sqrtf(tmp3);
double length = (double) tmp4;
这种附加舍入可能来源于使用较低精度的浮点单元(如 SSE2)执行某些中间计算。因此,fp:fast 舍入的精确性与平台相关;对于一个处理器能够顺利编译的代码,对于其他处理器可能未必有效。应 该由用户确定速度优点是否能够胜过任何精确性问题。
如果 fp:fast 优化对于特定函数而言特别成问题,则可以使用 float_control 编译器杂 注将浮点模式局部切换到 fp:precise。
fp:fast 模式下的代数转换
编译器可以通过 fp:fast 模式对浮点表达式执行特定的、不安全的代数转换。例如,在 fp:fast 模式下,可以采用下面的不安全优化。
double a, b, c;
double x, y, z;
. . .
y = (a+b);
z = y a |
【返回顶部】
【打印本页】
【关闭窗口】
|
|