基于编译器特性浅析C++程序性能优化
最近在恶补计算机基础知识,学到CSAPP第五章的内容,在这里总结并且展开一下C++程序性能优化相关的内容。
衡量程序性能的方式
一般而言,程序的性能可以用CPE(Cycles Per Element)来衡量,其指的是处理每个元素所需的CPU时钟周期数,计算公式为:CPE = 总执行周期数/处理的元素数量。
计算方式为:
#include <iostream>
#include <chrono>const int N = 1000000;
int arr[N];void test_function() {for (int i = 0; i < N; i++) {arr[i] = i * 2;}
}int main() {auto start = std::chrono::high_resolution_clock::now();test_function();auto end = std::chrono::high_resolution_clock::now();double elapsed_cycles = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() * 2.5; // 假设CPU 2.5 GHzdouble cpe = elapsed_cycles / N; // 计算 CPEstd::cout << "CPE: " << cpe << std::endl;return 0;
}
影响编译器优化的因素
用gcc时,gcc -Og可以指定优化方式,但随着优化等级升高,程序规模也可能增加。
gcc优化等级
- -O1:不会进行激进优化(如函数内联、代码重排序),不会影响可读性,编译时间仍然较短。优化包括死代码消除、常量传播、循环优化等。
- -O2:在基本优化的基础上增加更高级优化:消除冗余计算、循环展开、指令调度、函数内联、分支预测优化,仍然不会进行极端优化。
- -O3:实现更激进的循环展开、自动使用SIMD指令,使函数尽可能内联,并消除冗余加载和存储,对复杂的数学运算进行优化。但可能导致代码膨胀,过度优化会导致性能下降,如缓存效率降低。
- -Os:基于-O2,但会避免增加代码大小的优化,适合嵌入式系统。
以下为一些妨碍编译器优化的因素:
内存别名使用
对于以下看似相同的代码段:
//代码段1
void twiddle1(long *xp, long *yp){*xp += *yp;*xp += *yp;
}//代码段2
void twiddle2(long *xp, long* yp){*xp += 2* *yp;
}
很显然,代码段2的执行所需耗费时间更短,其需要的内存访问次数更少。
然而,编译器无法将代码1优化为代码2,因为当yp=xp时,代码1等效于xp = 4 xp, 而代码2等效于 *xp = 3 * *xp,编译器不知道函数该如何被调用。这种两个指针可能指向同一个内存位置的情况称为内存别名使用,在只执行安全的优化中,编译器必须假设不同的指针可能会指向内存中同一位置。
修改全局程序状态的函数
对于以下看似相同的代码段:
long counter = 0;
long f(){return counter++;
}
//代码段1
long func1(){return f()+f()+f()+f();
}
//代码段2
long func2(){return 4*f();
}
当函数f的返回值涉及到全局变量counter时,可以看出,func1的输出为6,而func2的输出为0。
将函数定义为内联函数,可以直接将函数调用替换为函数体,例如,代码段1在o1优化下可以展开为:
long funclin(){long t = counter++;t += counter++;t += counter++;t += counter++;return t;
}
如果使用-o2及以上优化,可能会展开为:
long funclin() {long tmp = counter;counter += 4;return tmp + (tmp + 1) + (tmp + 2) + (tmp + 3);
}
直接优化方法
为了举例说明优化方法是如何实现的,我们定义向量数据结构如下:
typedef struct{long len;data_t *data;
} vec_rec, *vec_ptr;
data_t代表基本元素的数据类型。
定义初始化该向量、访问向量元素以及获取向量长度的方法如下:
/* Create vector of specified length */
vec_ptr new_vec(long len)
{/* Allocate header structure */vec_ptr result = (vec_ptr) malloc(sizeof(vec_rec));data_t *data = NULL;if (!result)return NULL; /* Couldn't allocate storage */result->len = len;/* Allocate array */if (len > 0) {data = (data_t *)calloc(len, sizeof(data_t));if (!data) {free((void *) result);return NULL; /* Couldn't allocate storage */}}/* Data will either be NULL or allocated array */result->data = data;return result;
}/** Retrieve vector element and store at dest.* Return 0 (out of bounds) or 1 (successful)*/
int get_vec_element(vec_ptr v, long index, data_t *dest)
{if (index < 0 || index >= v->len)return 0;*dest = v->data[index];return 1;
}/* Return length of vector */
long vec_length(vec_ptr v)
{return v->len;
}
采用计算向量元素乘积的初始代码如下:
#define IDENT 1
#define OP *
/* Implementation with maximum use of data abstraction */
void combine1(vec_ptr v, data_t *dest)
{long i;*dest = IDENT;for (i = 0; i < vec_length(v); i++) {data_t val;get_vec_element(v, i, &val);*dest = *dest OP val;}
}
对于这段初始代码,有一些方向可以进行优化改进。
提高循环效率
代码移动
代码移动指的是将在循环里需要执行多次但计算结果不会改变的计算移动到循环外:
#define IDENT 1
#define OP *
/* Implementation with maximum use of data abstraction */
void combine2(vec_ptr v, data_t *dest)
{long i;long length = vec_length(v);*dest = IDENT;for (i = 0; i < length; i++) {data_t val;get_vec_element(v, i, &val);*dest = *dest OP val;}
}
减少过程调用
上述函数可以继续简化为:
data_t *get_vec_start(vec_ptr v)
{return v->data;
}/* Direct access to vector data */
void combine3(vec_ptr v, data_t *dest)
{long i;long length = vec_length(v);data_t *data = get_vec_start(v);*dest = IDENT;for (i = 0; i < length; i++) {*dest = *dest OP data[i];}
}
这种写法和combine2相比,减少了索引与数组边界的比较,但优化效果并不明显。
消除不必要的内存引用
对于combine3的赋值过程:
*dest = *dest OP data[i];
需要访问*dest指针的值,再根据这个地址从内存中取dest数组的值,并在计算完成后赋值到对应的内存上,在每次迭代过程中都要完成这样一个从内存读写数据的过程,将函数继续简化,减少对内存的读写:
void combine4(vec_ptr v, data_t *dest)
{long i;long length = vec_length(v);data_t *data = get_vec_start(v);data_t cur = IDENT;for (i = 0; i < length; i++) {cur = cur OP data[i];}*data = cur;
}
考虑机器特性的优化方法
上述优化方法都没有依赖目标机器的任何特性,如果要进一步提升性能,则需要考虑利用处理器微体系结构进行优化。
现代处理器结构
现代微处理器的简化示意图如下图所示,其可以分为指令控制单元ICU和执行单元EU两部分。
- 取指控制:ICU从指令高速缓存中读取指令,并在译码后将对应的操作发送到EU。一般来说,会在当前执行的指令很早之前就进行取指。然而当程序遇到分支时,处理器采用分支预测技术,会猜测是否选择该分支并预测其目标地址。使用投机执行技术,处理器会在确定分支预测是否正确前就跳到分支对应的指令,甚至开始执行这些对应的操作。如果分支预测错误,则将状态重新设为分支点的状态。
- 指令译码:接收实际的程序指令并将其转换为一组基本操作。
- 加载和存储单元:内置加法器,用于读写内存。
- 分支单元:向ICU返回分支预测是否正确的结果。
- 算术运算单元:执行整数和浮点数操作的不同组合。
- 退役单元:记录正在进行的处理,并确保其遵守机器级程序的语义。退役单元包含了多种寄存器,并控制这些寄存器的更新。指令译码时,其信息被放置在一个队列中,直到分支点预测结果出现,若预测正确,则程序寄存器的更新将被实际执行。任何对程序寄存器的更新都只会在指令退役的时候才会发生。
功能单元的性能
对于功能单元进行运算的性能,有以下几个指标可以用来衡量:
延迟L:表示完成运算所需要的总时间
发射时间I:表示两个连续的同类型运算之间需要的最小周期数
容量C:表示能够执行该运算的功能单元的数量
操作的吞吐量=C/I
对于一个执行n个乘法的函数,若其需要L*n+K个周期,其中K为调用函数和初始化等开销,此时CPE=L,对于单个按照顺序执行的功能单元组成的函数,延迟L表明了CPE的最小值,而对于多个功能单元组成的函数,还需要考虑其吞吐量。
处理器操作的抽象模型
将函数combine4的循环部分转换为汇编代码:
Inner loop of combine44. data_t = double, OP = *
acc in %xmm0, data+i in %rdx, data+length in %rax
1 .L25:
2 vmulsd (%rdx), %xmm0, %xmm0 loop: Multiply acc by data[i]
3 addq $8, %rdx Increment data+i
4 cmpq %rax, %rdx Compare to data+length
5 jne .L25 If !=, goto loop
将其抽象为数据流图,并去除不影响数据流的指令:
可以看出,乘法和加法运算是制约循环性能的两个因素,而浮点乘法的延迟约为整数加法的5倍,其成为了最关键的制约原因,程序的CPE为5。循环中的其他操作与乘法器并行地执行。
循环展开
循环展开是一种程序变换,通过增加每次迭代计算元素的数量来减少循环的迭代次数。
其优点为,可以提高缓存命中率,增加循环体内语句并发执行的可能性,同时减少分支预测失败的可能性。
用循环展开继续改进上述代码为:
/* 2 x 1 loop unrolling */
void combine5(vec_ptr v, data_t *dest)
{long i;long length = vec_length(v);long limit = length - 1;data_t *data = get_vec_start(v);data_t cur= IDENT;/* Combine 2 elements at a time */for (i = 0; i < limit; i += 2) {cur= (cur OP data[i]) OP data[i + 1];}/* Finish any remaining elements */for (; i < length; i++) {cur = cur OP data[i];}*dest = cur;
}
编译器可以很轻松地执行循环展开,用GCC的优化等级大于等于3时就会执行循环展开。
提高并行性
我们知道,乘法操作和加法操作是可以并行化的,也就是说,不需要等待对方完成即可进行下一次操作,可以在每个时钟周期就开始一次新的操作。但目前的代码还并不能更高速率地执行乘法和加法,这是因为我们将累积值放在一个单独的变量cur中,在前面计算完成之前都不能计算cur的新值。
为了提高并行性,我们可以用多个累积变量分别计算:
void combine6(vec_ptr v, data_t *dest){long i;long length = vec_length(v);long limit = length - 1;data_t cur0 = IDNET;data_t cur1 = IDNET;for(i = 0; i <limit; i+=2){cur0 = cur0 OP data[i];cur1 = cur1 OP data[i+1];}for(; i < length; i++)cur0 = cur0 OP data[i];*dest = cur0 OP cur1;
}
我们可以将多个累积变量变换归纳为将循环展开k次,以及并行累积k个值,得到k×k的循环展开,当k足够大时,程序在所有情况下几乎都能达到吞吐量界限。通常,只有保持能执行该操作的所有功能单元的流水线都是满的,程序才能达到这个操作的吞吐量界限,对延迟为L,容量为C的操作而言,要求循环展开因子k ≥ L*C即可达到最大吞吐量。
除了以上并行累计的方式以外,还可以通过重新结合变换的方式对combine5进行继续优化:
void combine7(vec_ptr v, data_t *dest){long i;long length = vec_length(v);long limit = length-1;data_t *data = get_vec_start(v);data_t cur = IDENT;for(i = 0; i < limit; i+=2){cur = cur OP (data[i] OP data{i+1]);}for(; i < length; i++)cur = cur OP data[i];*dest = cur;
}
combine7和combine5的区别在于**data[i] OP data[i+1]**计算的先后顺序不同,而(data[i] OP data[i+1])时可以被并行计算的,因为它不依赖于cur的计算结果,可以提前计算。(现代CPU的超标量架构,可以在一个时钟周期内执行多个独立的指令,如果两个指令没有数据依赖,CPU可以同时执行它们。)
书写适用于条件传送的代码
条件传送(Conditional Move, CMOV) 是一种 CPU 指令优化技术,它允许根据条件决定是否执行数据传送,而不使用传统的条件跳转(branching)。
在 x86 架构中,CMOV
指令集(如 CMOVZ
, CMOVNZ
, CMOVL
等)可以在满足某些条件时,将值从一个寄存器传送到另一个寄存器,而不会触发分支预测失败的问题。
在 C++ 中,我们可以使用 条件运算符(?:
)、内联汇编(asm
)、标准库函数(std::max
) 以及 SIMD 指令 来实现 条件传送。
在现代C++编译器中,使用三元运算符可能被编译器优化为CMOV指令:
#include <iostream>//传统条件分支的代码
int branching(int x, int y){if (x > y)return x;elsereturn y;}
//使用条件传送的代码
int conditional_move(int x, int y) {return (x > y) ? x : y; // 编译器可能优化为 CMOV
}int main() {int a = 5, b = 10;std::cout << "Max: " << conditional_move(a, b) << std::endl;return 0;
}
除此之外,gcc在 -O2
或更高级别优化下,std::max(a, b)
可能会被优化为 CMOV
指令
相关文章:

基于编译器特性浅析C++程序性能优化
最近在恶补计算机基础知识,学到CSAPP第五章的内容,在这里总结并且展开一下C程序性能优化相关的内容。 衡量程序性能的方式 一般而言,程序的性能可以用CPE(Cycles Per Element)来衡量,其指的是处理每个元素…...

服务器上通过ollama部署deepseek
2025年1月下旬,DeepSeek的R1模型发布后的一周内就火了,性能比肩OpenAI的o1模型,且训练成本仅为560万美元,成本远低于openAI,使得英伟达股票大跌。 下面我们来看下如何个人如何部署deepseek-r1模型。 我是用的仙宫云的…...

Android Coil总结
文章目录 Android Coil总结概述添加依赖用法基本用法占位图变形自定义ImageLoader取消加载协程支持缓存清除缓存监听 简单封装 Android Coil总结 概述 Coil 是一个用于 Android 的 Kotlin 图像加载库,旨在简化图像加载和显示的过程。它基于 Kotlin 协程࿰…...

《安富莱嵌入式周报》第351期:DIY半导体制造,工业设备抗干扰提升方法,NASA软件开发规范,小型LCD在线UI编辑器,开源USB PD电源,开源锂电池管理
周报汇总地址:嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 视频版: https://www.bilibili.com/video/BV16C95YEEZs 《安富莱嵌入式周报》第351期:DIY半导体…...

Redis在人员管理系统中的应用示例
用户会话管理 场景:用户登录后存储会话信息,支持多服务器共享 实现: 用户登录成功后,生成唯一Token(如JWT),作为Redis的Key Value存储用户ID、角色、权限等信息,设置过期时间&…...

The Wedding Juicer POJ - 2227
采取从外层边界,一步一步向内部拓展的策略,具体来说,一开始将最外面一层的点加入队列,并标记这些点的坐标已经被访问 取出队列中高度最低的点,将其弹出,查看其上下左右的点,如果新点没有被访问…...

# 深入理解RNN(一):循环神经网络的核心计算机制
深入理解RNN:循环神经网络的核心计算机制 RNN示意图 引言 在自然语言处理、时间序列预测、语音识别等涉及序列数据的领域,循环神经网络(RNN)一直扮演着核心角色。尽管近年来Transformer等架构逐渐成为主流,RNN的基本原理和思想依然对于理…...

分布式锁—6.Redisson的同步器组件
大纲 1.Redisson的分布式锁简单总结 2.Redisson的Semaphore简介 3.Redisson的Semaphore源码剖析 4.Redisson的CountDownLatch简介 5.Redisson的CountDownLatch源码剖析 1.Redisson的分布式锁简单总结 (1)可重入锁RedissonLock (2)公平锁RedissonFairLock (3)联锁MultiL…...

同步 Fork 仓库的命令
同步 Fork 仓库的命令 要将您 fork 的仓库的 main 分支与原始仓库(fork 源)同步,您可以使用以下命令: 首先,确保您已经添加了原始仓库作为远程仓库(如果尚未添加): git remote add…...

基于PySide6的CATIA零件自动化着色工具开发实践
引言 在汽车及航空制造领域,CATIA作为核心的CAD设计软件,其二次开发能力对提升设计效率具有重要意义。本文介绍一种基于Python的CATIA零件着色工具开发方案,通过PySide6实现GUI交互,结合COM接口操作实现零件着色自动化。该方案成…...

OpenManus 的提示词
OpenManus 的提示词 引言英文提示词的详细内容工具集的详细说明中文翻译的详细内容GitHub 仓库信息背景分析总结 引言 OpenManus 是一个全能 AI 助手,旨在通过多种工具高效地完成用户提出的各种任务,包括编程、信息检索、文件处理和网页浏览等。其系统提…...

Ubuntu-docker安装mysql
只记录执行步骤。 1 手动下载myql镜像(拉去华为云镜像) docker pull swr.cn-east-3.myhuaweicloud.com/library/mysql:latest配置并启动mysql 在opt下创建文件夹 命令:cd /opt/ 命令:mkdir mysql_docker 命令:cd m…...

Electron桌面应用开发:自定义菜单
完成初始应用的创建Electron桌面应用开发:创建应用,随后我们就可以自定义软件的菜单了。菜单可以帮助用户快速找到和执行命令,而不需要记住复杂的快捷键,通过将相关功能组织在一起,用户可以更容易地发现和使用应用程序…...

理解 JavaScript 中的浅拷贝与深拷贝
在 JavaScript 开发中,我们经常需要复制对象或数组。然而,复制的方式不同,可能会导致不同的结果。本文将详细介绍 浅拷贝 和 深拷贝 的概念、区别以及实现方式,帮助你更好地理解和使用它们。 1. 什么是浅拷贝? 定义 …...

【Java开发指南 | 第三十五篇】Maven + Tomcat Web应用程序搭建
读者可订阅专栏:Java开发指南 |【CSDN秋说】 文章目录 前言Maven Tomcat Web应用程序搭建1、使用Maven构建新项目2、单击项目,连续按两次shift键,输入"添加",选择"添加框架支持"3、选择Java Web程序4、点击&…...

从0到1入门Linux
一、常用命令 ls 列出目录内容 cd切换目录mkdir创建新目录rm删除文件或目录cp复制文件或目录mv移动或重命名文件和目录cat查看文件内容grep在文件中查找指定字符串ps查看当前进程状态top查看内存kill终止进程df -h查看磁盘空间存储情况iotop -o直接查看比较高的磁盘读写程序up…...

golang 从零单排 (一) 安装环境
1.下载安装 打开网址The Go Programming Language 直接点击下载go1.24.1.windows-amd64.msi 下载完成 直接双击下一步 下一步 安装完成 环境变量自动设置不必配置 2.验证 win r 输入cmd 打开命令行 输入go version...

如何下载和使用Git:初学者指南
🌟 如何下载和使用Git:初学者指南 在当今的软件开发中,Git已经成为不可或缺的版本控制系统。无论你是独立开发者还是团队成员,掌握Git的基本操作都能帮助你更高效地管理代码。今天,我将详细介绍如何下载和使用Git&…...
SQL_语法
1 数据库 1.1 新增 create database [if not exists] 数据库名; 1.2 删除 drop database [if exists] 数据库名; 1.3 查询 (1) 查看所有数据库 show databases; (2) 查看当前数据库下的所有表 show tables; 2 数据表 2.1 新增 (1) 创建表 create table [if not exists…...

基于Python实现的智能旅游推荐系统(Django)
基于Python实现的智能旅游推荐系统(Django) 开发语言:Python 数据库:MySQL所用到的知识:Django框架工具:pycharm、Navicat 系统功能实现 总体设计 系统实现 系统首页模块 统首页页面主要包括首页,旅游资讯,景点信息…...

安孚科技携手政府产业基金、高能时代发力固态电池,开辟南孚电池发展新赛道
安孚科技出手,发力固态电池。 3月7日晚间,安孚科技(603031.SH)发布公告称,公司控股子公司南孚电池拟与南平市绿色产业投资基金有限公司(下称“南平绿色产业基金”)、高能时代(广东横…...

p5.js:模拟 n个彩色小球在一个3D大球体内部弹跳
向 豆包 提问:编写一个 p5.js 脚本,模拟 42 个彩色小球在一个3D大球体内部弹跳。每个小球都应留下一条逐渐消失的轨迹。大球体应缓慢旋转,并显示透明的轮廓线。请确保实现适当的碰撞检测,使小球保持在球体内部。 cd p5-demo copy…...

Kali WebDAV 客户端工具——Cadaver 与 Davtest
1. 工具简介 在 WebDAV 服务器管理和安全测试过程中,Cadaver 和 Davtest 是两款常用的命令行工具。 Cadaver 是一个 Unix/Linux 命令行 WebDAV 客户端,主要用于远程文件管理,支持文件上传、下载、移动、复制、删除等操作。Davtest 则是一款…...

MySQL复习笔记
MySQL复习笔记 1.MySQL 1.1什么是数据库 数据库(DB, DataBase) 概念:数据仓库,软件,安装在操作系统(window、linux、mac…)之上 作用:存储数据,管理数据 1.2 数据库分类 关系型数据库&#…...

六十天前端强化训练之第十四天之深入理解JavaScript异步编程
欢迎来到编程星辰海的博客讲解 目录 一、异步编程的本质与必要性 1.1 单线程的JavaScript运行时 1.2 阻塞与非阻塞的微观区别 1.3 异步操作的性能代价 二、事件循环机制深度解析 2.1 浏览器环境的事件循环架构 核心组件详解: 2.2 执行顺序实战分析 2.3 Nod…...

集合论--形式化语言里的汇编码
如果一阶逻辑是数学这门形式化语言里的机器码,那么集合论就是数学这门形式化语言里的汇编码。 基本思想:从集合出发构建所有其它。 构建自然数构建整数构建有理数构建实数构建有序对、笛卡尔积、关系、函数、序列等构建确定有限自动机(DFA) 全景图 常…...

2025最新群智能优化算法:山羊优化算法(Goat Optimization Algorithm, GOA)求解23个经典函数测试集,MATLAB
一、山羊优化算法 山羊优化算法(Goat Optimization Algorithm, GOA)是2025年提出的一种新型生物启发式元启发式算法,灵感来源于山羊在恶劣和资源有限环境中的适应性行为。该算法旨在通过模拟山羊的觅食策略、移动模式和躲避寄生虫的能力&…...

MySQL数据实时同步至Elasticsearch的高效方案:Java实现+源码解析,一文搞定!
引言:为什么需要实时同步? MySQL擅长事务处理,而Elasticsearch(ES)则专注于搜索与分析。将MySQL数据实时同步到ES,可以充分发挥两者的优势,例如: 构建高性能搜索服务 实时数据分析…...

Spring-事务
Spring 事务 事务的基本概念 🔹 什么是事务? 事务是一组数据库操作,它们作为一个整体,要么全部成功,要么全部回滚。 常见的事务场景: 银行转账(扣款和存款必须同时成功) 订单系统…...

Git系列之git tag和ReleaseMilestone
以下是关于 Git Tag、Release 和 Milestone 的深度融合内容,并补充了关于 Git Tag 的所有命令、详细解释和指令实例,条理清晰,结合实际使用场景和案例。 1. Git Tag 1.1 定义 • Tag 是 Git 中用于标记特定提交(commit…...