`
javahigh1
  • 浏览: 1227641 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

C++ 中的浮点代码优化

 
阅读更多

原 作 者:MS
原 出 处:enet
发 布 者:loose_went
发布类型:转载

C++ 优化编译器不仅能够将源代码转换为机器码,而且能够对机器指令进行适当的排列以便改善性 能和/或减小大小。遗憾的是,许多常用的优化在应用于浮点计算时未必安全。在下面的求和算法 [1] 中,可以看到这方面的一个恰当的示例:

float KahanSum( const float A[], int n )
{
float sum=0, C=0, Y, T;
for (int i=0; i<n i></n> {
Y = A[i] - C;
T = sum + Y;
C = T - sum - Y;
sum = T;
}
return sum;
}


该函数将数组向量 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 函数的逻辑转换将是:

float KahanSum( const float A[], int n )
{
float sum=0; // C, Y & T are now unused
for (int i=0; i<n i></n> sum = sum + A[i];
return sum;
}


尽管转换后的算法更快,但它根本没有准确表达程序员的意图。精心设计的误差修正已经 被完全消除,只剩下一个具有所有其关联误差的简单的直接求和算法。

当然,完善的 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></n> {
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></n> 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></n4> {
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></n> 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 产生预期的结果。而且,在许多情况 下,优化结果的精确性可能“足够严密”。当优化提供了令人信服的速度优点时,尤其如此。例如, 视频游戏要求具有尽可能快的速度,但通常并不要求进行高度精确的浮点计算。因此,编译器制造商 必须为程序员提供一种机制,以便控制速度和精确性之间经常背离的目标。

某些编译器通过为每种类型的优化单独提供“开关”在速度和精确性之间进行折衷。这使开发人员 可以禁用可能为其特定应用程序的浮点精确性带来变化的优化。尽管该解决方案可能提供对编译器的 高度控制,但它也会带来其他一些问题:

&#8226; 通常很难搞清楚需要启用或禁用哪些开关。

&#8226; 禁用任一优化都可能对非浮点代码的性能带来不利影响。

&#8226; 每个附加的开关都会引起许多新的开关组合;组合数目将很快变得难以控制。


因此,尽管为每种优化提供单独的开关看起来似乎很有吸引力,但使用此类编译器可能非常麻烦并 且不可靠。

许多 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。

&#8226; 在 fp:precise 模式下,仅对浮点代码执行安全优化,并且与 /Op 不同,以最 高可行 精度一致性地执行中间计算。

&#8226; fp:fast 模式放松了浮点规则,允许以牺牲精确性为代价进行更为积极的优化。

&#8226; fp:strict 模式提供了 fp:precise 的所有常规正确性,同时启用了 fp- exception 语义,并禁止在存在 FPU 环境更改(例如,对寄存器精度、舍入方向的更改等等)时进行 非法转换。


可以通过命令行开关或编译器杂注单独控制浮点异常语义;默认情况下,在 fp:precise 模式下禁 用浮点异常语义,而在 fp:strict 模式下启用该语义。编译器还提供了对 FPU 环境敏感性和某些特 定于浮点的优化(如化简)的控制。这一简单明了的模型为开发人员提供了大量针对浮点代码编译的 控制,并且无须使用太多的编译器开关,也不会带来令人讨厌的副作用。
分享到:
评论

相关推荐

    审查代码规范(C++)

    审查代码规范 C++ 1. 范围 3 2. 规范性引用文件 3 3. 术语和定义 3 4. 开发工具 3 5. 目录结构 4 6. 布局规范 5 6.1. 对齐 5 6.2. 代码行内的空格 7 6.3. 空行 8 6.4. 代码行 9 6.5. 长行拆分 10 6.6. 注释 11 ...

    浮点计算编程原理、实现与应用

    前言  第1章 引论  1.1 计算有什么用?  1.1.1 基础科学  1.1.2 应用科学  1.1.3 工程项目  1.1.4 日常生活  1.2 超高精度计算有什么用?  1.3 计算编程概述 ... 第13章 浮点编程中的常见技巧 附录

    C++ Primer第四版【中文高清扫描版】.pdf

    【原书名】 C++ Primer (4th Edition) 【原出版社】 Addison Wesley/Pearson 【作者】 (美)Stanley B.Lippman,Josée LaJoie,Barbara E.Moo 【译者】 李师贤 蒋爱军 梅晓勇 林...18.1.1 C++中的内存分配 632 18.1.2 ...

    游戏编程--优化、调试

    C++编程人员在使用SSE指令函数编程时不必关心这些128位的寄存器,你可以使用128位的数据类型“__m128”和一系列C++函数来实现这些算术和逻辑操作,而决定程序使用哪个SSE寄存器以及代码优化是C++编译器的任务。...

    Visual C++音频视频处理技术及工程实践(分卷7)

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642实现视频编...

    Visual C++音频/视频处理技术及工程实践 (分卷1)

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642实现视频编...

    Visual C++音频视频处理技术及工程实践(分卷0)

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642实现视频编...

    Visual C++音频视频处理技术及工程实践地址

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642...

    为 Kendryte K210 AI加速器 开放深度学习编译器堆栈_C++_代码_下载

    功能 支持多输入输出网络,支持多分支结构 ...算子合并和优化 支持 float 和量化 uint8 推理 支持训练后量化,使用浮点模型和量化校准集 平坦模型,支持零拷贝加载 更多详情、使用方法,请下载后阅读README.md文件

    Visual C++音频/视频处理技术及工程实践(分卷2)

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642实现视频编...

    基于遗传算法的GPS病态方程的解算

    为准确解算仅有少数几个历元的GPS载波相位观测数据的病态定位方程,将GPS快速定位的病态法方程求解问题转化为一个函数优化问题,应用遗传算法求解病态方程,避免了法方程的求逆运算,从而可以得到参数的近似最优解。...

    Visual C++音频视频处理技术及工程实践(分卷9)

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642实现视频编...

    Visual C++音频视频处理技术及工程实践(分卷6)

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642实现视频编...

    Visual C++音频视频处理技术及工程实践(含源码2/2)

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642...

    Visual C++ 编程资源大全(源码 系统)

    instance.zip 使程序一运行一个实例(29KB)&lt;END&gt;&lt;br&gt;48,div.zip 这是一个异常处理的例子,主要处理被零除的浮点操作DbgOut.shtml.htm(14KB)&lt;END&gt;&lt;br&gt;49,cpu32.zip Intel的CPU检测程序的源代码,酷吧!...

    Visual C++ 音频/视频 处理技术及工程实践(分卷3)

    12.4.3 代码是浮点型还是整型 473 12.4.4 EMMS准则 474 12.4.5 CPUID的检测MMX技术的用法 474 12.4.6 数据对齐 474 12.4.7 数据安排 475 12.4.8 应用程序最后的调整 476 12.5 本章小结 476 第13章 用DM642实现视频编...

    Vivado HLS 视频课程总结

    8 Vivado HLS中的C++基本运算 23 9 测试平台的基本架构 25 9.1 Test Bench 25 9.2 C Test Bench 26 10 测试激励 28 11 测试输出检测与格式控制 28 11.1 Scoreboard 28 11.2 输出格式控制 30 12 接口综合基本介绍 33 ...

    数字IC设计流程整理

    这一阶段一般使用高级语言(如C\C++),利用算法级建模和仿真工具(如Matlab,SPW)进行浮点和定点的仿真,进而对算法进行评估和优化。 3、 架构设计。根据设计的功能需求和算法分析的结果,设计芯片的架构,并对...

    matlab集成c代码-Metaphor:令人印象深刻的机器学习和计算机视觉C++库

    Matlab集成的c代码隐喻 用于数值计算,机器学习和计算机视觉的C ++库 Impressive Machines LLC,2015年 该库的最终目标是使用机器学习为图像,视频和音频识别创建一套独立的工具。 库中名为Core的部分非常稳定,并...

    Turbo译码研究及其DSP实现

    Turbo译码采用并行算法,为提高程序执行效率,充分利用Max-Log-Map译码算法的结构特点,对程序进行寄存器级优化:把Viusal C++实现的浮点算法改为定点算法,将前后向累积路径度量计算的最内层循环展开,合理分配...

Global site tag (gtag.js) - Google Analytics