首页 > 编程语言 >CAD程序性能优化之一般优化方法

CAD程序性能优化之一般优化方法

时间:2024-07-13 23:30:02浏览次数:18  
标签:代码 程序 时间 内存 模块 优化 性能 运行 CAD

       建模效率是CAD软件至关重要的指标,会极大地影响应用中的设计效率,进而决定工业产品的研发周期。鉴于CAD程序的复杂性,在编写完成之前,几乎很难准确判断这段程序是否符合预期的性能要求,往往需要通过实际测试来确认。

       图1是提高程序性能的一般流程。第一步是测试总体性能,如果满足要求,则结束,否则,需要分析和定位问题,然后修改相应代码,再回到第一步重新测试,直到性能满足要求。当然,也可能存在一种情况,在现有方案下无法优化到目标性能,而面临推倒重来,这不是本文的关注点。在上述流程中,如何分析和定位问题、怎样修改代码是本文要讨论的重点。

在这里插入图片描述

图1. 提高程序性能的一般流程
 

1. 分析方法

       不管是哪种分析方法,基本原则是不变的,那就是查找运行热点,或者说是定位到耗时最久的部分。这个原则是非常关键的,因为优化非热点代码的性价比可能非常低。比如,有一段代码明显有问题,但只占到总时间的10%,即使将这段代码的运行效率提高100%,也仅仅能提高总效率的5%。而如果能将占总时间80%的代码提高100%,那么就能使总效率提高40%。

       分析热点的方法一般可以分成4种:

  • 根据经验判断。这只适用于小段程序,且非常依赖工程师的经验。
  • 查看汇编代码。由于汇编语言阅读性较差,这种方式一般只适用于深度优化。
  • 编写测试程序,分解运行时间。这种方法直观且灵活度高,使用的最多。
  • 运用统计和分析工具。本质上与上一种方法相同,可以减少甚至不写代码,表现形式一般也更加友好。
1.1. 计时工具

       首先,我们先建立一下对计算机运行速度的大概印象,以计算两点之间距离为例,计算1,000,000次,需要多少时间呢?笔者所用电脑的表现是3ms左右。因此,如果想准确查找到热点,需要有高精度的计时工具。

       以Windows系统为例,提供了多个计时器,比如clock_t。但是,clock_t的计时精度只有ms级别,通过上面的例子不难看出,如果距离的计算次数小于100,000次,clock_t就无法统计到了,因此无法满足优化性能的需求。Windows提供了高精度的性能计数器,通过QueryPerformanceCounter()和QueryPerformanceFrequency()两个函数实现,QueryPerformanceCounter()用来获取计数器当前值,QueryPerformanceFrequency()用来获取计数器的频率。实际使用中,在被测试程序之前和之后,分别调用QueryPerformanceCounter(),然后获得计数差值,除以计数器频率就是被测试程序的运行时间了。QueryPerformanceCounter()的分辨率小于1 us,计时精度可以到us级别。

       实际应用中需要注意的是:

  • 不要直接使用计数差值dif除以频率freq,这样得到的是以s为单位的整数值,很容易就得到0(运行时间小于1s)。如果想得到us时间,应该dif * 1000 * 1000 / freq。
  • 即使计时精度为us级别,但依然会存在无法统计到的项,比如在for循环中统计点距离的时间,并累加起来,得到的数值也可能是0。
1.2. 时间分解

       分析热点的核心思路就是将运行时间不断分解,一般是按照类或函数为单位模块。图2是对模块A运行时间的分解,首先,将A分解成3个子模块,分别统计运行时间,D的运行时间占比很低,而B、C的运行时间占比较多,并且相差不大。因此,需要B、C这2个子模块再次进行拆解,第三层的时间统计特点是:虽然E单次运行的时间占总时间不是很高,但是被不同的2个上级模块调用了,调用的总时间让E成为第三层的热点。继续分解E,可以看出R的时间占比很大。因此,子模块R是整个模块A的热点,是应该首先考虑优化的程序模块。

在这里插入图片描述

图2. 时间分解示例
 

       上面的分解思路是理想化的,实际过程中可能比较复杂。正如下面的代码,假设模块R中是一个for循环,其中调用了2个函数,一般的分解思路是:分别统计Fun1()和Fun2(),然后将单次调用时间累加。但是,如果2个函数单次运行的时间都在1us以下呢?累加的结果都是0。一种可行的方式是:首先,注释掉Fun2(),在for循环之外加计时代码,这样就可以统计出Fun1()的运行时间;然后,取消注释Fun2(),统计出两者的总运行时间,减去Fun1()的,就是Fun2()的运行时间。实际代码可能存在更加复杂的嵌套关系,可能需要开发人员设计更好的测试代码。

for (int i = 0; i < n; i++)
{
    //计时开始
    Fun1();
    //计时结束
    //累加时间

    //计时开始
    Fun2();
    //计时结束
    //累加时间
}

       其它需要注意的是:

  • 运行时间的统计和分解是一个易出错的工作,需要验证分解的正确性,确保所有子模块的时间之和近似等于父模块的时间。
  • 鉴于CAD程序的复杂性,不要盲目自信,主观臆断一段程序是不耗时的,可能会错失优化性能的机会。
  • 若有需要,可以在Debug模式下统计运行时间,但是,最终结果还是需要在Release模式下获取和验证,两种模式下的效率差异巨大,不同模式下得到的热点也可能截然不同。
1.3. 实用工具

       上面提到的时间分解方法需要编写测试代码,这是一项比较繁琐和易出错的工作。现在有一些好用的工具,可以用来测试程序性能,比如Intel推出的VTune Profiler 和 Visual Studio中内置的性能探查器。VTune Profiler可以提供采集并分析丰富的、多层次的性能数据,对运行在Intel平台上的程序非常友好。Visual Studio内置的性能探查器,可以对内存、CPU、GPU、IO等进行数据采样,针对图2中的程序,图3和图4分别是性能探查器给出的火焰图和函数运行时间排序,从这两张图中很容易发现,程序运行的热点是函数R。

在这里插入图片描述

图3. Visual Studio内置的程序运行火焰图
 

在这里插入图片描述

图4. Visual Studio内置的函数运行时间排序
 

2. 修改代码

       代码性能的优化没有固定的修改方式,可以从以下几个方面入手。

2.1. 减少重复操作

       下面的代码是关于重复操作的2个典型例子。一个是重复计算,如果几何体是不变的,不应该在循环中计算包容盒,而应该挪到循环前面。另一个是反复申请内存,如果循环内使用的std::vector对象的大小是固定的,肯定是应该放在循环前面的,甚至在声明时设定大小。哪怕是不固定的,也可能要写在循环前面,只要当前循环需要的空间少于前一次的,就能避免再次申请内存。

//重复操作的代码
for (int i = 0; i < n; i++)
{
    BoundingBox box = constGeometry.GetBoundingBox();
    //使用 box    
    std::vector<int> vct;
    //使用 vct
}

//重复利用的代码
BoundingBox box = constGeometry.GetBoundingBox();
std::vector<int> vct;
for (int i = 0; i < n; i++)
{
    //使用 box    
    //使用 vct
}

       上面的2个示例的影响范围比较小,只是将for循环之内的代码挪到前面。但是,实际编程遇到的问题会更加棘手,主要是因为复杂的调用关系:(1)广度,一个函数往往会调用多个函数;(2)深度,从第一个函数开始调用,调用层数非常多。图5简单说明了这一情况,函数A调用函数B,函数B调用函数C和D。假设在这几个函数中,都会用到某一计算结果(如上面的包容盒)或相似的内存空间(如上面的std::vector),那么就可能会出现重复操作的问题,而且这种情况在实际编程中非常常见。

在这里插入图片描述

图5. 嵌在复杂调用关系中的重复操作
 

       在不做大范围的更改的前提下,可以采取下面2种方式:

  • 通过传递参数的方式(如图5中A-B),将已有的计算结果或已申请的内存空间,传递给后续模块。
  • 通过归纳公共接口(如图5中E),存储已有的计算结果或已申请的内存空间,并对需要的模块开放获取接口。

       需要明确的一点是,要想重复利用已有资源,一般都会使程序模块之间的耦合度变高,这可能就是变快的代价。

2.3. 选择高效操作

       自从上世纪40年代,冯 · 诺依曼提出了经典的计算机体系结构,虽然有所发展,但现代计算机的设计基本还在遵循这些关键原则。那么,我们的程序是不是也要适应某些与计算机体系相关的原则呢?是的,同样的需求可能会有不同的实现方式,根据计算特点,选择执行效率高的操作,可以提升程序运行的效率。

       如图6所示,影响程序运行性能的最关键的2个部件是:存储器(内存)和运算器(CPU)。为了讲述方便,本文简单假设程序运行的过程是:存储器读取程序中的原始数据,并送入运算器中,运算器根据程序中的计算代码,完成计算任务。

在这里插入图片描述

图6. 程序与内存、CPU的关系
 

       本文仅打算给出几个选择高效操作的实例:

  • 申请内存时,栈内存比堆内存快,条件允许的情况下,优先选择栈内存,但要注意栈溢出。
  • 对于整数的2的n次幂乘法/除法,可以使用移位运算符(左移/右移)实现,速度更快。
  • CPU和内存之间存在高速缓存机制,CPU访问内存数据时,如果缓存中已有,则CPU直接从缓存中读取数据,否则,才会从内存中读取,而缓存比内存快的多。正如下面的代码, i i i- j j j遍历要比 j j j- i i i遍历快的多,因为C++中的二维数组是按照行优先存储的, i i i- j j j遍历会提高数据缓存命中率。
for (int i = 0; i < n; i++)
{
   for (int j = 0; j < n ; j++)
   {
      arrData[i][j] = 0.0;   //速度快
      arrData[j][i] = 0.0;   //速度慢
   }
}

       如何选择合适的、高效的操作/指令,是一个异常复杂和深入的主题,读者可以通过研读参考文献[2],获得更加系统、清晰的分析。

2.4. 指标权衡

       对于一个CAD功能来说,除了效率,还有一些指标是必须要考虑的,而这些指标往往是与效率矛盾的,有时不得不权衡再三,做出取舍。

       稳定性和精度。根据用户的指令,稳健且精确地求解各种问题是CAD设计软件发挥良好作用的基石,但这并不意味着要追求100%稳定和精确。如图7,计算一点 P 0 P_0 P0​到曲线 C ( u ) C(u) C(u)的最近点,以参数 u u u为自变量,可以得到 C ( u ) C(u) C(u)上点到 P 0 P_0 P0​的平方距离公式:
h ( u ) = ∣ ∣ C ( u ) − P 0 ∣ ∣ 2 . h(u)=||C(u)-P_0||^2. h(u)=∣∣C(u)−P0​∣∣2.
令 h ′ ( u ) = 0 h'(u)=0 h′(u)=0,求解方程就可以得到极值点。首先,上述方程一般是一个高阶方程(5次以上),没有通解公式,只能使用数值方法得到近似解,而近似解的精度与迭代次数有关,进而影响计算效率,因此,CAD接口的设计目标一般是满足一定误差的最近点。其次,图中所示的情况,方程是有多解的( P 1 P_1 P1​和 P 2 P_2 P2​),求解可能陷入局部最优。如何能保证找到 P 1 P_1 P1​?有很多种补救的措施,也可以采用其它方法,但会不会导致效率低下,需要权衡。

在这里插入图片描述

图7. 计算一点在曲线上的最近点
 

       适用场景。任何接口都是有适用场景的,适用场景越广泛,意味需要处理和检查的情况越多,可能会导致效率低下。考虑这样的需求:通过一根曲线,沿某一方向拉伸成曲面,拉伸曲面不能有自相交,否则会导致后续操作失败。那么,开发人员必须要对输入(曲线或方向)进行自相交检测吗,不一定,一种可能是真实使用场景中此曲线不可能在此方向下自相交,另一种可能是已经在其它接口中检查过了,当前接口就不用检查了,这属于开发约定,约定本接口不处理曲线有自相交的情况。

总结

  • 程序性能优化应当从全局出发,不应该过于看重单一模块的性能提升还是降低。
  • 关键是找到运行热点,而不是揪住每一处细节,否则只会事倍功半。
  • 程序性能优化往往会使代码变得复杂或更难维护,优化的过程就是在不断的权衡过程中,找到能满足整体需求的平衡点,这是个循序渐进的过程。

参考文献

[1] C++应用程序性能优化,冯宏华等,电子工业出版社。

[2] 深入理解计算机系统(第3版),Randal E.Bryant等,机械工业出版社。

 

文章首发于微信公众号:CAD智造干将,欢迎关注
 
请添加图片描述

标签:代码,程序,时间,内存,模块,优化,性能,运行,CAD
From: https://blog.csdn.net/wzyuchen1989/article/details/140408294

相关文章

  • 基于GA遗传优化算法的Okumura-Hata信道参数估计算法matlab仿真
    1.算法仿真效果matlab2022a仿真结果如下(完整代码运行后无水印):   2.算法涉及理论知识概要      遗传算法(GeneticAlgorithm,GA)是一种模拟自然界生物进化过程的全局优化搜索算法,由JohnHolland于1975年提出。它利用达尔文的自然选择和遗传学原理,通过选择、交叉、......
  • 如何更好的优化 ListView 控件的性能
    ......
  • 【CPO-TCN-BiGRU-Attention回归预测】基于冠豪猪算法CPO优化时间卷积双向门控循环单元
    %数据准备%假设有一个输入变量X和一个目标变量Y%假设数据已经存储在X和Y中,每个变量为列向量%参数设置inputWindowSize=10;%输入窗口大小outputWindowSize=1;%输出窗口大小numFeatures=1;%输入变量的数量numFilters=32;%TCN中的滤波器数......
  • 使用 GPT-4 和 ChatGPT 构建应用程序
    文章目录应用程序开发概述软件架构设计原则LLM驱动型应用程序的漏洞    GPT-4和ChatGPT的API服务为开发人员赋予了新的能力。无须深入了解A1技术,开发人员就可以构建能够理解和回应自然语言的智能应用程序。从聊天机器人和虚拟助手到内容创作和语言翻译......
  • MySQL优化SQL查询语句的策略与实践
    在数据库管理和优化中,针对MySQL的SQL查询优化是提升性能的关键步骤。以下是一些实践性强且高效的策略,旨在减少查询时间,提高系统响应速度:1.精准索引策略,减少全表扫描索引优化:确保在WHERE和ORDERBY子句中涉及的列上创建索引。这不仅加快了数据检索速度,还降低了服务器负担。替......
  • 基于SpringBoot+VueJS+微信小程序技术的图书森林共享小程序设计与实现:7000字论文+源
          博主介绍:硕士研究生,专注于信息化技术领域开发与管理,会使用java、标准c/c++等开发语言,以及毕业项目实战✌    从事基于javaBS架构、CS架构、c/c++编程工作近16年,拥有近12年的管理工作经验,拥有较丰富的技术架构思想、较扎实的技术功底和资深的项目管理经......
  • 导入Excel大量数据如何优化
    导入Excel大量数据如何优化背景线上导入excel的时候发现速度太慢了发现原来的代码是读入所有数据然后按行去数据库比对是否存在,不存在再插入。这样就相当于n条数据要有2n次的数据库请求,以前数据量小的时候没问题,数据量一大就暴露出问题了,下面的测试在数据库......
  • 【转发收藏】电力系统优化调度方向热点研究方向推荐及参考文献,创新点立马就有了!
     【转发收藏】电力系统优化调度方向热点研究方向推荐及参考文献,创新点立马就有了!(qq.com)专题推荐:论文推荐,代码分享,典藏级代码,视角,论文思路,免责声明(点击即可跳转)高创新组合模型和算法典藏级matlab代码,又双叒叕更新啦!(电力系统优化和时间序列预测方向)倾情推送24.7.6【代码......
  • 解决HBuilder X运行微信小程序模拟器Error: pages.json解析失败
    前言如果已经排查很久了,那这就不是你的问题了,大概率是由于你曾经创建了一个路径,在指定PagePath的时候又指向了这个路径,这个操作本身没有问题。但是,如果你曾经对这个路径修改过了,那编译器就会有问题,来品鉴一下这个错误。16:15:36.772请注意运行模式下,因日志输出、sourcem......
  • 探索贪心算法:解决优化问题的高效策略
    贪心算法是一种在每一步选择中都采取当前最佳选择的算法,以期在整体上达到最优解。它广泛应用于各种优化问题,如最短路径、最小生成树、活动选择等。本文将介绍贪心算法的基本概念、特点、应用场景及其局限性。贪心算法的基本概念贪心算法的核心思想是局部最优策略,即在每一步选择......