性能优化的一般策略及方法

性能优化的一般策略及方法

在汽车嵌入式开发领域,性能优化始终是一个无法回避的问题:

  • 座舱 HMI 想要实现更流畅的人机交互

  • 通信中间件在给定的 CPU 资源下,追求更高的吞吐量

  • 更一般的场景:嵌入式设备 CPU 资源告急,需要降低 CPU 使用率...

不同的工程师会从不同的角度给出不同的优化建议:

  • 有人关注系统调用情况

  • 有人建议从算法和数据结构入手

  • 有人建议避免递归、循环嵌套

  • 有人会从存储器层次结构出发,建议修改代码提高缓存命中率来提升性能

  • ...

这些都是具体的代码调优技术/技巧,或许有效,但不够系统。本文不讨论具体的代码调优技术,而是想介绍下具体代码优化技巧之上,更高层次的优化策略。比起代码级别的调优,可能效果更好,成本更低。

开始之前,需要强调下:

Premature optimization is the root of all evil. — Donald Knuth

一、性能概述

代码调优只是代码性能优化的方法之一,还有其他性能优化的方法,也许效果更好、成本更低、对代码的负面影响(降低可读性/可维护性、引入 bug 等)也更少。

1.1 软件质量和性能

性能只是众多软件质量标准中的一个。比起单纯的代码执行速度,用户可能更在意其他方面,比如稳定可靠、简洁易用等。

性能也不只是代码的执行速度,过分追求代码的执行速度而忽略其他方面可能会影响整体性能及软件质量。

1.2 性能和代码调优

假如确定了把 Efficiency 作为首要目标,在代码调优之前,请优先考虑:

  • 性能需求

  • 程序设计

  • 类和方法设计

  • 操作系统交互

  • 编译器优化

  • 硬件升级

  • 代码调优

a. 性能需求

Barry Boehm 讲过一个故事:某系统一开始要求亚秒级的响应时间,导致非常复杂的设计,预估成本 1 亿美元。后来分析发现,90%的情况下,用户可以接受 4s 的响应时间。重新修改需求之后,节省了 7000 万美元。

再举一个例子,自动驾驶算法需要周期性获取某些车辆数据,当前的需求是 10ms 的周期上报。如果将周期改为 20ms 仍然可以满足需求,那么不需要任何额外的优化,CPU 占用率便可减少一半。

解决性能问题之前,先确认是否真的必要。

b. 程序设计

软件架构设计主要如何将程序分解到模块/类。有的设计决定了很难实现高性能,有的设计则容易实现高性能。

在软件的架构设计中,设定资源占用的目标很重要:如果每个组件都能达成目标,则整个系统自然也可以。如果某个组件无法达成目标,也可以及早发现,进行设计修改或代码优化。不仅如此,清晰的目标也更利于执行和实施。

c. 类和方法设计

在程序设计基础上更近一步,深入到类的内部。在这一层级,我们可以选择数据结构和算法,从而影响程序的执行速度和内存占用。

d. 操作系统交互

如果程序中涉及外部文件、动态内存、输出设备,通常会和操作系统交互。如果程序性能不好,有可能就是系统调用过多导致的。有时系统库或编译器会在你意想不到的地方产生系统调用。

e. 编译器

编译器优化比手工优化代码效果更好,也更安全!某种程度上来说,选择了正确的编译器,基本就不需要考虑代码级优化了。

f. 硬件

有时候升级硬件是解决性能问题成本最低的方案。不仅节省了性能优化的人力成本,同时还避免了由于性能优化引入的一系列隐性成本。同时,所有其他程序也因为硬件升级而得到性能提升。

g. 代码调优(Code Tuning)

“代码调优”指的是修改正确的代码,使之运行得更快。代码调优的前提是代码正确:设计良好,易于理解和修改。“调优”指的是小规模修改,一个类,一个函数或者几行代码。“调优”不包括大规模设计修改,以及更高层次的性能优化手段。

上面从程序设计到代码调优六个层级中,每一个层级都可能产生 10 倍的性能提升,不同层级的组合起来理论上可以有百万倍的提升。虽然实际不可能在每个层级都取得 10 倍的提升,但是这里想表达的是,性能优化的空间潜力是巨大的。

二、代码调优

2.1 二八法则

a. 优化哪里

有研究和报告表明:

  • 20% 的函数占用了 80% 的程序执行时间

  • <4% 的代码甚至能占用 50% 的执行时间

不是每一行代码都要做到最快,真正值得花时间把性能调到极致的代码只有很小的一部分!

b. 谁来优化

项目中系统整体的 CPU 接近满负荷,其中 A 负责的模块 CPU 占用 5%,而 B 负责的模块 CPU 占用超过 60%。即便 A 再厉害,把自己优化没了,带来的整体收益也不过 5%,而 B 却因为有更大的优化空间,能轻松地地降低 10%的 CPU 占用。

2.2 常见误区

很多过时的、传说中的代码优化技巧都是无效的,甚至能够产生负面影响。

误区 1: 代码行数越少,程序越快

很容易找到一个反例:初始化大小为 N 的数组,直接写出 N 条赋值语句,其性能是循环赋值的 2.5~4 倍!

误区 2: xxxx 写法很很可能更快

对于性能而言,没有所谓的“很可能”,必须实际测量才知道到底是“优化”了还是“劣化”了。影响性能的因素很多:处理器架构、编程语言、编译器、编译器版本、库、库的版本、内存大小...“很可能”是非常不负责任的说法,对于特定的环境是优化,在另外环境下很能就是劣化。再次强调,必须要实际测量!

此外,为了“性能优化”而引入的特殊写法,反而会影响编译器的优化。

误区 3: 从一开始就写要出“快”的代码

在程序没最终完成之前,几乎不可能识别出真正的性能瓶颈,你所“优化”的代码中,96%其实不需要优化。过分关注执行速度反而会影响软件质量的其他方面。

Premature optimization is the root of all evil. — Donald Knuth

误区 4: “快”和“正确”同等重要

如果程序不能正确运行,或者运行结果不正确,即使再快也没有任何价值。

2.3 什么时候去调优

Jackson's Rule of Optimization:

Rule 1. Don't do it.

Rule 2 (for expert only). Don't do it yet -- that is, not until you have a perfectly clear and unoptimized solution.

简言之,非必要,不优化。先保证良好的设计,编写易于理解和修改的整洁代码。如果现有的代码很糟糕,先清理重构,然后再考虑优化。

2.4 编译器优化

现代编译器优化远比你想象中的更强大。例如编译器能够识别并优化循环嵌套,比手动优化更安全,效果也更好。不要自作聪明地用一些几十年前所谓的特殊“优化技巧”,大概率会给编译器造成困扰,适得其反。

  • 各家的编译器各有优缺点,选择最适合项目的编译器

  • 开启编译器的不同优化选项,性能可提升为原来的 2 倍甚至更多

程序员应该专注于写整洁代码(设计良好,意图明确清晰,可读性好,易于维护),优化的事情交给编译器就好啦!

三、导致性能问题的常见原因

3.1 常见性能问题元凶

a. 输入/输出操作

不必要的 I/O 操作是最常见的导致性能问题的罪魁祸首。比如频繁读写磁盘上的文件、通过网络访问数据库等。一般来说,内存的读写性能是磁盘的几千几万倍,如果有内存不是很 critical,可以将数据保存在内存中以减少不必要的 IO 操作从而改善性能。

几年前在一个基于 Qt 的座舱项目中,从 CarPlay 界面返回车机首页会有短暂的卡顿,导致无法通过 CarPlay 的认证。用 QmlProfiler 分析发现,切换卡顿是由于从磁盘加载背景图片导致的,将背景图片缓存在内存中,可以直接消除图片加载时间,大幅提升界面切换的流畅度。代价是牺牲了一定的内存,这是一个空间换时间的典型例子。

b. 缺页

有一个经典的例子:

// BAD
for (int col = 0; col < MAX_COLUMNS; ++col) {
  for(int row = 0; row < MAX_ROWS; ++row) {
      table[row][col] = GetDefaultValue();
  }
}

// GOOD
for (int row = 0; row < MAX_ROWS; ++row) {
  for(int col = 0; col < MAX_COLUMNS; ++col) {
      table[row][col] = GetDefaultValue();
  }
}

以上两种写法在特定场景下,性能差距可达 1000 倍。背后涉及到二维数组在内存中的存储方式以及缓存命中等知识,CSAPP 的第 5、6 章对此有详细阐述。

c. 系统调用

系统调用需要进行上下文切换,保存程序状态、恢复内核状态等一些步骤,开销相对较大。对磁盘的读写操作、对键盘、屏幕等外设的操作、内存管理函数的调用等都属于系统调用。

Linux 系统调用可以通过 strace 查看,qnx 也有 tracelogger 等工具

d. 解释型语言

一般来说,C/C++/VB/C# 这种编译型语言的性能好于 Java 的字节码,好于 PHP/Pyhon 等解释型语言。这也是为什么汽车嵌入式领域还是 C/C++ 天下等主要原因。

e. 错误

还有很大很一部分导致性能问题的原因可以归为错误:忘了把调试代码(如保存 trace 到文件)关闭,忘记释放资源/内存泄漏、数据库表设计缺陷(常用表没有索引)等。

3.2 常见操作的相对开销

注:上表仅供参考,不同处理器、不同语言、不同编译器、不同测试环境所得结果可能相差很大!

代码调优的方式之一就是用低开销的操作替代高开销操作。一般操作(赋值、函数调用、算数运算)的开销基本相同,除法运算开销较大,超越函数开销尤其巨大,多态函数的调用较普通函数调用有一定额外开销。

四、测量

代码执行耗时和代码量不成比例,必须经过测量才知道时间花在哪里。找到问题,优化,重新测量。

性能优化很多时候是反直觉的(比如代码量越少不一定越快),只有测量了才知道是否有效果。

过往的经验可能不会有太多帮助,针对旧的机器、语言、编译器的优化经验在现在可能完全不适用,必须要实际测量了才知道!

比如在旧版本的编译器中,把二维数组的操作转为对单个指针操作可以提升性能,而在新的编译器却完全没有效果,因为新版编译器会自动进行这样的转化。而手动修改代码只会降低代码的可读性。

测量要准确

  • 用专门的 Profiling 工具或者系统时间

  • 只测量你自己的代码部分

  • 必要时需要用 CPU 时钟 tick 数来替代时间戳以获得更准确的测量结果

要想准确的测量是一件非常困难的事情。不同的硬件、进程的优先级、线程调度策略、测量时其他的进程的运行、甚至外界环境都可能对测量结果产生影响。我们能做的就是尽可能地控制变量,剔除无关因素影响。

五、迭代

很难只用一个技巧就把性能提升 10 倍,但是可以不断尝试,组合不同技巧,最终实现巨大的性能提升。下面是一个通过不断迭代优化,将执行时间从 21 分 40 秒优化到 22 秒的例子:

六、调优一般方法

  1. 程序设计良好,易于理解和修改(前提)

  1. 如果性能不佳:

a. 保存当前状态

b. 测量,找出时间主要消耗在哪里

c. 分析问题:是否因为高层设计、数据结构、算法导致的,如果是,返回步骤 1

d. 如果设计、数据结构、算法没问题,针对上述步骤中的瓶颈进行代码调优

e. 每进行一项优化,立即进行测量

f. 如果没有效果,恢复到 a 的状态。(大多数的调优尝试几乎不会对性能产生影响,甚至产生负面影响。代码调优的前提是代码设计良好,易于理解和修改。Code tuning 通常会对设计、可读性、可维护性产生负面影响,如果 tuning 改良了设计或者可读性,那么不应该叫 tuning,而是属于步骤 1)

  1. 重复步骤 2

七、总结

  • 性能只是众多软件质量指标中的一个,而且一般不是最重要的那个。精心调优之后的代码也只能对整体性能产生部分影响,程序架构、详细设计、数据结构/算法的选择、编译器通常比代码本身对性能的影响更大。

  • 准确地测量至关重要绝大多数程序的大部分时间都耗在少数代码上,只有测量了才知道时间花在了哪里,优化重点在哪里很多“优化技巧”实际上不仅不会提高性能,甚至会降低性能,只有测量了才能知道测量越接近真实环境越好,模拟的测试环境和程序实际运行环境可能得到完全不同的结果!

  • 通常需要多轮优化迭代才能达到预期性能目标

  • 如果想为今后(可能)的性能优化提前作准备,最好的准备就是编写易于理解和修改的整洁代码

7.1 检查清单

  1. 明确需求,是否真的有这么高的性能要求?

  1. 尝试提高编译器优化选项?

  1. 考虑升级/更换编译器?

  1. 考虑过升级/更换硬件?

5.程序的 high-level design、类设计是否合理?

6.检查是否有不必要的系统调用、I/O 操作?

7.考虑用编译型语言替代解释型语言?

  1. 代码调优是否作为最后手段?

7.2 代码调优方法

  1. 调优的前提:代码正确,设计良好,易于理解和修改

  1. 测量,找出瓶颈

  1. 每次优化后,立即重新测量

  1. 如果没有效果,撤销改动

  1. 尝试多种方法,不断迭代

文章转载自:Zijian/TENG

原文链接:https://www.cnblogs.com/tengzijian/p/17858112.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/196607.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Web前端开发技术:图像与多媒体文件

在现代的Web开发中&#xff0c;图像和多媒体文件在各种网站和应用程序中扮演着至关重要的角色。它们不仅能提供更丰富的内容&#xff0c;还能大大提高应用程序的吸引力和用户体验。本文将深入介绍一些关键的Web前端开发技术&#xff0c;这些技术将有助于开发者在处理图像和多媒…

【Python3】【力扣题】367. 有效的完全平方数

【力扣题】题目描述&#xff1a; 【Python3】代码&#xff1a; 1、解题思路&#xff1a;Python函数。num的平方根 或者 num的0.5次幂。 知识点&#xff1a;float.is_integer(...)&#xff1a;判断浮点数的值是否等于整数。也可以&#xff1a;浮点数.is_integer()。 pow(a,b)&…

JAVA多线程总结

一、概念&#xff1a; 1、什么是多任务 多任务就是在同一时间做多件事情&#xff0c;如边吃饭边玩手机等。看起来是多个任务都在做&#xff0c;本质上我们的大脑在同一时间依旧只做了一件件事情 2、什么是程序 程序是指令和数据的有序集合&#xff0c;其本身没有任…

“一键转换JPG到BMP:轻松优化图片管理的革命性工具“

亲爱的用户们&#xff0c;您是否曾经因为图片格式不兼容而感到烦恼&#xff1f;是否曾经为了转换图片格式而耗费大量时间&#xff1f;现在&#xff0c;我们为您带来了一款全新的图片转换工具&#xff0c;它可以轻松解决您的问题&#xff01; 首先&#xff0c;我们进入首助编辑高…

同旺科技 USB 转 RS-485 适配器 -- 隔离型

内附链接 1、USB 转 RS-485 适配器 隔离版主要特性有&#xff1a; ● 支持USB 2.0/3.0接口&#xff0c;并兼容USB 1.1接口&#xff1b; ● 支持USB总线供电&#xff1b; ● 支持Windows系统驱动&#xff0c;包含WIN10 / WIN11 系统32 / 64位&#xff1b; ● 支持Windows …

idea打开.class文件没有反编译

1 问题描述 新安装的idea开发工具&#xff0c;打开.class文件查看内容时发现没有将文件进行反编译&#xff0c;所以具体的代码实现看不到。如图所示&#xff1a; 尝试了各种办法解决&#xff0c;最终都没有解决我的问题&#xff0c;其他同事的idea开发工具都可以打开.class文件…

基于SpringBoot与Vue的增城高校二手物品交易系统

基于SpringBoot 与 Vue 的增城高校二手物品交易系统的设计与实现 摘要&#xff1a;随着生活水平和在校大学生消费能力的提高&#xff0c;学生用品的迭代速度越来越快&#xff0c;导致大量的闲置物品无法及时完成处理&#xff0c;而传统的线下摆摊等方式处理不仅效率低&#xf…

Java-认识异常

本章重点&#xff1a; 1. 异常概念与体系结构 2. 异常的处理方式 3. 异常的处理流程 4. 自定义异常类 1. 异常的概念与体系结构 1.1 异常的概念 在Java中&#xff0c;将程序执行过程中发生的不正常行为称为异常。比如之前写代码时经常遇到的&#xff1a; 1. 算术异常 2. 数组…

详解STL库—map和set

目录 一、关联式容器 二、键值对 SGI-STL中关于键值对的定义&#xff1a; 三、set 3.1 set的介绍 3.2 set的使用 1.set的模板参数列表​编辑 2. set的构造 3. set的迭代器 4. set的容量 5. set修改操作 6. set的使用举例 四、map 4.1map的介绍 4.2 map的使用 1…

揭秘!9个月完成亚运会的整体数字化观测

项目背景与业务场景 2023 第 19 届亚运会在杭州举办&#xff0c;这将提高杭州的国际知名度&#xff0c;促进杭州经济、社会的全面发展&#xff0c;并将进一步推动奥林匹克运动在中国的发展&#xff0c;并且提升杭州城市形象和国际影响力。为亚运村村民提供便捷周到的服务和丰富…

【NI-RIO入门】为CompactRIO供电

在大多数情况下&#xff0c;您可以使用可直接连接系统的电源&#xff0c;例如墙上的电源插座。但是&#xff0c;某些应用程序或环境缺乏可用电源&#xff0c;您必须使用其他电源&#xff0c;例如电池。无论您是否有可用电源&#xff0c;您可能都希望通过为系统提供一些冗余来确…

京东秒杀之商品展示

1 在gitee上添加.yml文件 1.1 添加good-server.yml文件 server:port: 8084 spring:datasource:url: jdbc:mysql://localhost:3306/shop_goods?serverTimezoneGMT%2B8driverClassName: com.mysql.cj.jdbc.Drivertype: com.alibaba.druid.pool.DruidDataSourceusername: rootpa…

自动驾驶HWP功能规范

HWP功能规范 Highway Pilot Functional Specification 文件状态&#xff1a; 【√】草稿 【】正式发布 【】正在修改 文件起草分工 撰写&#xff1a; 审核&#xff1a; 编制&#xff1a; 签名&#xff1a; 日期&#xff1a; 审核&#xff1a; 签名&#xff1a; 日期&am…

抖音视频如何无水印下载,怎么批量保存主页所有视频没水印?

现在最火的短视频平台莫过于抖音&#xff0c;当我们刷到一个视频想下载下来怎么办&#xff1f;我们知道可以通过保存到相册的方式下载&#xff0c;但用这种方法下载的视频带有水印&#xff0c;而且有些视频不能保存到相册&#xff08;这是视频作者设置了禁止下载&#xff09;。…

c# 简单web api接口实例源码分析

CreateHostBuilder(args).Build().Run();这句语句处于c#webapi程序的第一句&#xff0c;它的作用是&#xff1a;启动接口的三个步骤&#xff1a; 创建一个HostBuilder对象。执行IHostBuilder.Build()方法创建IHost对象。执行IHost.Run()方法启动。 创建和配置Host&#xff08;…

蚁剑低版本反制

蚁剑低版本反制 漏洞概述 中国蚁剑是一款开源的跨平台网站管理工具&#xff0c;它主要面向于合法授权的渗透测试安全人员以及进行常规操作的网站管理员。影响范围 AntSword <2.0.7 蚁剑实验版本&#xff1a;2.0.7 环境搭建&#xff1a; 172.16.1.233&#xff08;蓝队服…

【Python深度学习第二版】学习笔记之——什么是深度学习

机器学习是将输入&#xff08;比如图像&#xff09;映射到目标&#xff08;比如标签“猫”&#xff09;的过程。 这一过程是通过观察许多输入和目标的示例来完成的。 深度神经网络通过一系列简单的数据变换&#xff08;层&#xff09;来实现这种输入到目标的映射&#xff0c;这…

解读 | 从谷歌AI判定阿波罗登月“造假“来谈谈合成图片检测技术

大家好&#xff0c;我是极智视界&#xff0c;欢迎关注我的公众号&#xff0c;获取我的更多前沿科技分享 邀您加入我的知识星球「极智视界」&#xff0c;星球内有超多好玩的项目实战源码和资源下载&#xff0c;链接&#xff1a;https://t.zsxq.com/0aiNxERDq 整个事情可以爬楼看…

【2023.11.28】关于Servlet路径的学习

创建Servlet 这是Tomcat配置的初始路径&#xff0c;在web项目内&#xff0c;该路径代表了webapp下index.html所在的页面。 WebServlet(name "login", value "/login",loadOnStartup 1) public class LoginServlet extends HttpServlet { 使用注解的方…

leetcode:2133. 检查是否每一行每一列都包含全部整数(python3解法)

难度&#xff1a;简单 对一个大小为 n x n 的矩阵而言&#xff0c;如果其每一行和每一列都包含从 1 到 n 的 全部 整数&#xff08;含 1 和 n&#xff09;&#xff0c;则认为该矩阵是一个 有效 矩阵。 给你一个大小为 n x n 的整数矩阵 matrix &#xff0c;请你判断矩阵是否为一…