怎样编写高性能C/C++程序

本文主要讨论高性能编程,而且是那种“极致性能需求”。按照本人的粗浅认识,应该已经覆盖了绝大多数技术要点,但缺点是不够详细(篇幅有限)。本文共分为4个部分:总体论述、高性能网络编程、高性能数值计算、常规高性能需求。

1)总体论述

C/C++的重要性,在于所有操作系统内核都是C语言编写,嵌入式设备编程也基本都是C语言。虽然最新版本的Linux内核在讨论是否引入Rust。但未来10~20年,直接操作硬件的编程语言仍然是以C语言为主的。C++是对C兼容性最好的高级语言,它可以保留对硬件底层操作能力的同时,提供“零开销”的抽象能力,从而可以使用更抽象的软件工程成果。在所有高性能需求场合,C/C++是最重要,某种程度上也是唯一的方案。

下面简略谈一下别的编程语言。

Fortran 在数值计算领域的历史要早于 C,它一直是科学和工程应用的主要语言。但是Fortran 脱离计算机工业主流发展,一个大趋势是Fortran 的科学计算库逐渐被改为C++语言。比如说我刚工作的时候还是写过Fortran 代码的。但是工作5~6年以后,所有的Fortran 代码基本都改成C++了。

Julia 是专为高性能数值计算而设计的编程语言,比较适合科研。Julia 比较适合对性能有较高要求,同时不熟悉计算机体系结构的科学家或工程师使用(比如物理学、电气工程、气象预报、经济学等非计算机专业)。本文讨论的是极致性能需求,和较高性能要求还是有本质区别的。

Rust是除了C++,唯一既具有底层操作能力,又具有抽象能力的编程语言。Rust放弃了对C的兼容,在内存安全性、编译器等方面做了巨大改进。C++与Rust的对比经常是技术论坛争吵的话题。我个人认为相当长时间内,Rust仍然不能撼动C++的地位。

Java是主流编程语言。一般情况下,Java的性能比C++慢10倍,内存占用可能超过10倍。所以,Java是不能满足高性能需求的。比Java更慢的语言,例如Python,就更不适合高性能需求了。有人可能觉得用Python做AI应用,性能也很不错。那是因为你调用的别人写好的库,库的内部多半是用C++实现的,相当于在别人的地基上搭建自己的房子。

高性能编程需要有一个总体、历史的视角。大约2006年起,CPU的摩尔定律差不多“失效”了。在那之前,主频常常从一个版本跃升到下一个版本,从几百兆赫兹飙升到几G赫兹。但到了2000年代中期,这种飞速的增长放缓了,主要是由于功耗和热量问题。从那时起,CPU的升级主要是通过增加核心数、提高每个核心的效率、增加缓存大小等方法来提高总体芯片性能。但是不经过任何优化的程序,是在单个CPU核心上运行的,这是最吃亏的。这意味着常规程序不能享受信息工业的进步。

相对于CPU,其它硬件设备进步反而更大。比如说网卡,从 1Gbps 的速度(2000年左右)到 10Gbps、25Gbps 甚至 100Gbps(2014年左右),网卡的传输速度得到了显著的提升。甚至网络协议中的部分校验功能,也可由网络硬件来实现了。比如说GPU的最新发展,其浮点计算能力几乎超过CPU的10倍以上;还有FPGA、ASIC芯片的发展,很多特定的功能可以交给硬件实现。当然,非CPU设备专门用于特定任务,通用编程仍然是CPU完成的。因此,C/C++的编程,越来越像是“指挥官”的角色,把各种各样的硬件协调好、发挥最大功效。因此,真正高性能的系统一定是软硬件结合的。

对于传统的纯CPU程序,高性能优化的主要方向是多核并行(或者是用户空间上下文的快速切换例如协程技术),这同样需要深入的计算机组成原理的知识,比如存储的层次结构、多核下缓存的一致性问题、函数的汇编实现等。

传统上来说,“指挥官”的角色是由操作系统内核来承担的。即使是计算机专业的毕业生,也少有人对操作系统内核十分熟悉。我听说很多高校的操作系统课程是以讲授理论为主,很少有学生能深入真实的操作系统内核进行编程和调试的。这个现状已经越来越难以适应高性能技术的发展了。

下面再谈一下若干纯技术问题。

时钟测试:这一点非常重要,很多人优化程序凭借自己的“感受”,其实主观感受经常非常不准。根据Amdahl定律,只有测试出最占时间的串行代码,并行优化才能取得最大功效。Linux经常采用gettimeofday函数,被认为是比较精确的时间测量函数。这个函数早期是系统调用,会产生较大的代价。但现在新的内核版本,gettimeofday函数已经是用户空间函数了,或者至少避免了上下文切换。除了gettimeofday函数,更精确的函数是rdtsc,这本质是汇编语句封装,直接读寄存器,获取CPU时间戳,每一个处理器时钟周期,它就增加 1。然后这个差值除以CPU频率就得到时间。使用 rdtsc 也需要注意一些技术要点:固定CPU频率,在高性能应用程序中,通常建议禁用CPU的动态调整功能,特别是在BIOS设置和内核裁剪编译两个环节都要禁止电源管理(防止降频);绑定线程到特定的CPU核,同时进行CPU亲和性设置,防止高性能线程的中断;测量时间过程中尽量不要进行系统调用,包括不限于:延迟和定时;文件和阻塞式网络I/O;同步和锁定;进程和线程管理;内存申请释放,等等。

高性能指标的矛盾:实时性是为了在给定的时间约束内完成特定的工作。例如,硬实时系统必须在固定的时间内响应,否则可能会导致系统崩溃或其他不良后果。吞吐量是系统在单位时间内可以处理的工作量。为了确保任务在固定的时间内完成,可能需要预留更多的计算资源(例如把特定的CPU核独占),这可能会降低系统的总体吞吐量。反之,为了最大化吞吐量,可能需要允许某些任务的完成时间超出其理想的时间范围。具体优先优化何种指标,需要根据实际的需求来定。还有一些其它的矛盾指标,例如功耗与性能、价格与性能等等。

推荐学习视频

面试高频技术:专为性能而生,linux c/c++开发必学的几个高性能组件(线程池、内存池、自旋锁、互斥锁、原子操作、定时器、内存泄漏、分布式锁)icon-default.png?t=N7T8https://www.bilibili.com/video/BV1Ug4y1Z7rt/

2)高性能网络编程

大多数实时的需求都建立在与外界通信基础上,比如说工业控制、量化交易、硬件在环仿真等。毕竟数字世界与真实世界交互,网络是最常用的交互方式。其它的接口,例如RS-485工业总线,在通信速度方面可能弱于以太网,但抗干扰能力远强于以太网。在电力系统我接触的范围,硬实时都是通过以太网或者无源光纤接口实现的。因此,本文的高性能网络编程默认为以太网网卡(或者光口)。

前面说过,传统网络编程任务全部交给内核。但内核是通用的,对于高性能网络通信任务是不擅长的。内核socket的各种系统调用、上下文切换、内存拷贝等,没有专门的高性能优化,这些都会增加延迟并减少吞吐量。假设我们把内核比作大山,山上确实有非常多的风景,但我们的目的并不是欣赏风景,而是快速通过。那么无非是两种方法:一是打隧道,也就是进行内核编程;二是修路从山旁边绕过去,也就是By-pass 技术,绕过操作系统的传统网络堆栈,直接在用户空间中处理网络数据包。

内核编程方面,Linux内核的新版本已经有很多改进,例如零拷贝(Zero-Copy),传统的数据传输需要多次的内存拷贝,这会消耗大量的CPU资源。零拷贝技术通过减少或消除这些拷贝操作来提高性能。还有非阻塞I/O、I/O多路复用,例如select、poll和epoll,它们可以让单一的线程监视多个文件描述符,有效地管理大量的并发连接。对于上层的应用程序,我们一般不直接调用socket,而是使用封装的网络库,例如boost::asio库,是非常好的工具。但充分发挥库能力的前提是对内核处理网络通信的过程,不能一无所知。

在嵌入式开发需求方面,即使是升级后的Linux内核都不能胜任了,我们可以进行内核裁剪、编程,自己增加特定的系统调用(或者驱动)。我所知的,有人把IEC 61850的实现放在内核中完成。其实IEC 61850实现本身并没有复杂到哪里去,主要是内核的开发调试需要掌握的知识点非常多。

另外一个技术路径是By-pass 技术,在用户空间中处理报文,减少上下文切换,避免不必要的内存拷贝。许多 by-pass 解决方案使用轮询模式来检测新的数据包,而不是依赖中断。这可以减少中断的开销,尤其是在高流量环境中。还有一点是很关键的(但是从未看到别人提到):用户态程序开发调试的麻烦程度是比内核小太多了,可能小10倍都不止。

DPDK是By-pass 技术的典型代表,最近一段时间我一直在调试DPDK,充分感受到DPDK的博大精深,它的性能强到让人吃惊的地步。DPDK使用大页内存和高效的缓冲区管理,以及零拷贝、轮询等技术,能够快速地处理和转发数据包。DPDK完全接管网卡,所以网络协议栈必须自己写,会遇到很多socket编程根本设想不了的问题。

推荐学习视频

linux c/c++开发之高性能网络编程,面试与开发都非常重要的技术,不容错过!(tcp/ip、udp、epoll、网络io、reactor、网络协议栈)icon-default.png?t=N7T8https://www.bilibili.com/video/BV1Ru4y1J7E3/C/C++程序员的未来方向,DPDK高性能网络开发教程(dpdk/网络协议栈/vpp/OvS/DDos/SDN/NFV/虚拟化/spdk)icon-default.png?t=N7T8https://www.bilibili.com/video/BV1x64y1A7C2/

3)高性能数值计算

前面提到,数值计算方面在GPU的推动下,“异构”已经是主流编程方式了。首先,高度并行的、计算密集型的部分适合 GPU,而 I/O 密集型、分支密集型或需要复杂数据结构的部分适合 CPU。很显然,不是所有的任务都适合GPU来做。除此以外,CPU 和 GPU 之间的数据传输可能会成为瓶颈。尽量减少数据传输的次数,尤其是在频繁的计算迭代中。使用异步数据传输,允许 CPU 和 GPU 同时工作,而不是等待数据传输完成。大多数情况而言,到达微秒的实时性要求级别,GPU不太适合了,这种情况下更多的是使用FPGA。另外,多利用成熟的库,如 CUDA、cuBLAS、cuDNN 等,它们经过优化,可以提供很好的性能。

对于纯粹基于CPU的高性能计算任务,优先采用高性能的库。例如,MATLAB的底层矩阵库就是MKL(稠密矩阵)/SuiteSparse(稀疏矩阵)。这些高性能库千锤百炼,有大量的优化甚至汇编优化、大概率比你自己写的计算程序要好。只有在极其狭窄的功能或者场合,自己写的库要更好些。比如我自己写的稀疏矩阵加法的性能比Eigen略好一些。

我自己用过这些数值计算库:MKL(但是其稀疏矩阵的计算性能比较让人失望);SuiteSparse(Tim Davis教授的经典库,稀疏矩阵,最近增加了图计算库);Eigen(比较好的C++矩阵库);OpenBLAS(张先轶的作品,朋友圈有他);GLPK(优化算法库);fftw(快速傅里叶分析)。

如果是非常大型的高性能计算,需要动用数据中心的力量,甚至是不同城市的数据中心。这就是分布式系统架构的概念了,例如OpenAI公司已经使用Kubernetes训练自然语言大模型。

4)常规高性能需求

对于常规高性能需求,主要是日常编程的时候有些技术准则要遵循。《深入理解计算机系统》(第三版)这本书已经总结得很不错了,主要包括:循环的时候要注意流水线问题(第4章),减少分支预测失败,排好序的循环性能会提高很多;充分利用编译器(第5章),包括让编译器实现SIMD并行与循环展开、减少不必要的内存操作、减少过程调用;理解存储的层次结构(第6章),充分利用局部性原理和提高缓存命中率,这点可能是对常规软件影响最大的方面,JAVA无法像C/C++那样控制内存分布,这就导致进行不了这么深入的优化。这本书后面还讨论了系统调用、IO、网络编程等内容,都对高性能编程有很好的参考价值。《深入理解计算机系统》写得非常好,也很基础。这本书完全掌握后,才谈得上对高性能编程有些概念(可能还算不上入门)。

对于C++编程,还有一些准则需要遵循。这方面可以寻找专门C++的专著来看。C++的特殊之处是编译器可能在程序员背后做很多事情,有时候会让程序员大吃一惊。特别是C++中的字符串、vector等标准库设施,在内存申请之前一定要reserve;还有避免不必要的复制,这在初始化、函数传参等方面都要注意,可以适当使用移动语义。C++的并发编程也一直是很难的问题,稍微不仔细就会遇到性能损失或者漏洞,可以谨慎的使用原子操作和无锁数据结构。

当然,数据结构和算法还是很重要的,但这已经不属于“极致优化”,而属于程序员的常识。我经常看到同事写多重循环,每个循环都要从头到尾遍历一遍,每当看到这样的代码都感觉有些心痛,大量的CPU周期和数据中心电力都这样被浪费了,而且确实遇到过算法复杂性没做好导致的现场问题。C++的标准库在数据结构和算法是非常经典的,这点无可置疑。另外,还有一些库是对C++标准库的进一步优化,例如folly库。

本文最后写出几个常数,希望对大家有启发作用:

3GHz的CPU时钟周期是0.3纳秒,而光在真空中跑这么久,只能前进10cm,只有一个手掌的长度。(缓存一致性的协议可能比你想的复杂,特别是涉及ccNUMA缓存一致性非均匀内存访问机器);

寄存器、L1、L2缓存都可以在10ns以内完成操作;L3可能需要20ns,内存至少需要100ns甚至更多。缓存的优化非常重要。

数据中心到你家的时间延迟,大概是10ms以上,这看起来比较长,但这是物理规律决定的。任何优化,都不能超越物理。

硬盘的IO读写延迟为15ms,所以尽量不要动用硬盘,实在没办法,我们可以用内存当做缓存,这就是Redis的用法。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【揭秘】ForkJoinTask全面解析

内容摘要 ForkJoinTask的显著优点在于其高效的并行处理能力,它能够将复杂任务拆分成多个子任务,并利用多核处理器同时执行,从而显著提升计算性能,此外,ForkJoinTask还提供了简洁的API和强大的任务管理机制&#xff0c…

常规的管理系统除了适用该有的范儿一定要有!气质上不能输

hello宝子们...我们是艾斯视觉擅长ui设计和前端开发10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩! 常规的管理系统除了适用该有的范儿一定要有!气质上不能输 在现今快速发展的商业环境中…

Android音量调节修改

前言 今日公司,安卓设备的音量显示不正常,让我来修复这个bug,现在已修复,做个博客,记录一下,以后碰到类似一下子就好解决。 Android音量调节相关 路径 frameworks\base\services\core\java\com\android…

NIO-Selector详解

NIO-Selector详解 Selector概述 Selector选择器,也可以称为多路复⽤器。它是Java NIO的核⼼组件之⼀,⽤于检查⼀个或多个Channel的状态是否处于可读、可写、可连接、可接收等。通过⼀个Selector选择器管理多个Channel,可以实现⼀个线程管理…

Spring boot + Azure OpenAI 服务 1.使用 GPT-35-Turbo

Azure OpenAI 服务使用 GPT-35-Turbo 先决条件 maven 注意 beta.6 版本 <dependency><groupId>com.azure</groupId><artifactId>azure-ai-openai</artifactId><version>1.0.0-beta.6</version></dependency>问答工具类 pack…

C++的关键字,命名空间,缺省参数,函数重载以及原理

文章目录 前言一、C关键字(C98)二、命名空间命名空间介绍命名空间的使用 三、C输入【cin】& 输出【cout】四、缺省参数缺省参数概念缺省参数分类缺省参数的使用小结一下 五、函数重载函数重载介绍函数重载类型 六、C支持函数重载的原理--名字修饰(name Mangling)【重点】 前…

二分算法模版

二分算法模版 实数二分算法模版实数二分模版题 整数二分算法模版向上取整二分模版向下取整二分模版二分模版的注意点二分模版中check函数的实现能够使用二分的条件 二分主要分两类&#xff0c; 一类是对实数进行二分&#xff0c;一类是对整数进行二分 对整数二分又分成2种&…

python-自动化篇-运维-监控-简单实例-道出如何使⽤Python进⾏系统监控?

如何使⽤Python进⾏系统监控&#xff1f; 使⽤Python进⾏系统监控涉及以下⼀般步骤&#xff1a; 选择监控指标&#xff1a; ⾸先&#xff0c;确定希望监控的系统指标&#xff0c;这可以包括 CPU 利⽤率、内存使⽤情况、磁盘空间、⽹络流量、服务可⽤性等。选择监控⼯具&#x…

Java实现加权平均分计算程序WeightedAverageCalculator

成绩加权平均分计算程序&#xff0c;带UI界面和输入保存功能。 因为本人对成绩的加权均分有所关注&#xff0c;但学校的教务系统查分时往往又不显示个人的加权均分&#xff0c;加之每次手动敲计算器计算很麻烦就花了点时间写了一个加权均分计算程序自用&#xff0c;顺便开源。…

STM32标准库——(5)EXTI外部中断

1.中断系统 中断&#xff1a;在主程序运行过程中&#xff0c;出现了特定的中断触发条件&#xff08;中断源&#xff09;&#xff0c;使得CPU暂停当前正在运行的程序&#xff0c;转而去处理中断程序&#xff0c;处理完成后又返回原来被暂停的位置继续运行 中断优先级&#xff…

《WebKit 技术内幕》学习之十五(6):Web前端的未来

6 Chromium OS和Chrome的Web应用 6.1 基本原理 HTML5技术已经不仅仅用来编写网页了&#xff0c;也可以用来实现Web应用。传统的操作系统支持本地应用&#xff0c;那么是否可以有专门的操作系统来支持Web应用呢&#xff1f;当然&#xff0c;现在已经有众多基于Web的操作系统&…

uniapp小程序:内存超过2mb解决方法(简单)message:Error: 上传失败:网络请求错误 代码包大小超过限制。

分析&#xff1a;这种情况是代码文件内存超过2mb无法进行预览上传 解决方法&#xff1a; 1、Hbuilder中点击运行-->运行到小程序模拟器--->运行时是否压缩代码 2、在微信小程序中点击详情--->本地设置&#xff1a; 3、点击预览即可运行了

两个近期的计算机领域国际学术会议(软件工程、计算机安全):欢迎投稿

近期&#xff0c;受邀担任两个国际学术会议的Special session共同主席及程序委员会成员&#xff08;TPC member&#xff09;&#xff0c;欢迎广大学界同行踊跃投稿&#xff0c;分享最新研究成果。期待这个夏天能够在夏威夷檀香山或者加利福尼亚圣荷西与各位学者深入交流。 SERA…

南方故乡吹来的风

故乡的风 - 张明敏 词&#xff1a;刘因国 曲&#xff1a;刘因国 南方故乡吹来的风 带着潮水的呼唤 吹着你的秀发 飘散着茉莉的香 茉莉的香哟 南方故乡吹来的风 带着渔船的归航 吹着你的欢畅 吹着渔帆点点醉哟 点点的醉哟 远方的姑娘 你是否听见 我的心在嘿哟 你…

上位机图像处理和嵌入式模块部署(c/c++ opencv)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 opencv可以运行在多个平台上面&#xff0c;当然windows平台也不意外。目前来说&#xff0c;opencv使用已经非常方便了&#xff0c;如果不想自己编译…

81 C++对象模型探索。数据语义学 - 静态成员变量的存取,非静态成员变量的存取

一&#xff0c;静态成员变量的存取 静态成员变量只有一个实体&#xff0c;保存在可执行文件的数据段中&#xff0c;如果没有初始化则保存在数据段的BBS中&#xff0c;由于存储在执行文件的数据段中&#xff0c;因此在编译阶段就会确定地址。当程序编译完成后&#xff0c;不管运…

20240127如何在线识别德语字幕?

20240127如何在线识别德语字幕&#xff1f; 2024/1/27 11:42 1945[科尔贝格]Kolberg 01:48:49 接近109分钟 德语视频的字幕OCR适配&#xff1a; 1、whisper&#xff0c;8:39-8:58&#xff0c;使用GTX1080需要接近20分钟。对整机性能要求比较重&#xff0c;特别吃显卡&#xff…

LabVIEW信号时间间隔测量

用LabVIEW软件平台开发一个用于测量两路信号时间间隔的系统。系统利用LabVIEW的数据采集和处理能力&#xff0c;能够准确测量并分析来自不同硬件板卡的信号时间间隔&#xff0c;这对于精确控制和数据分析至关重要。 系统主要由以下几部分组成&#xff1a;数据采集卡、信号处理…

力扣(LeetCode)227. 基本计算器 II

给你一个字符串表达式 s &#xff0c;请你实现一个基本计算器来计算并返回它的值。 整数除法仅保留整数部分。 你可以假设给定的表达式总是有效的。所有中间结果将在 [-231, 231 - 1] 的范围内。 注意&#xff1a;不允许使用任何将字符串作为数学表达式计算的内置函数&#…

三角函数、反三角函数

一、三角函数 二、反三角函数&#xff1a;已知三角函数值&#xff0c;反算角度大小 因为严格单调函数才有反函数一个y对应一个x&#xff0c;显然ysinx&#xff0c;ycosx&#xff0c;ytanx在其定义域并不是严格单调&#xff0c;所以需要人为划定范围。 1. 研究yarcsinx、yarcco…