在C++中,为什么部分程序员喜欢在循环中写‘++i’而不是‘i++’?

news2025/1/11 12:59:06

自入行以来,无论是查阅资料、技术博客亦或是同事间的技术交流,都有一个共识:在循环的时候,务必使用前置操作符,因为其性能优于后置操作符,久而久之,这个就像一个不成文的规定,大家都在遵循,久而久之,成为潜移默化的编码习惯。而使得大家持有这个观点的原因就是后置操作会产生临时变量,而后置操作则不会

原因

后置操作和前置操作,一个会产生临时变量,一个不会产生临时变量,其原因是:前置操作遵循的规则是change-then-use,而后置操作遵循的规则是use-then-change。正因为后置操作的use-then-change原则,使得编译器在实现该操作的时候,先把之前的值进行拷贝备份,然后对值进行更改操作,最后返回之前备份的值。

以整型为例:

 // ++i
 i = i+1;
 return i;
 ​
 // i++
 temp = i;
 i=i+1;
 return temp;

同样,对于复杂类型:

 // ++i
 Object &operator++() {     
   ++value_;     
   return *this;   
 }    
 // i++    
 Object operator++(int) {     
   Object old = *this;    
       ++*this;     
   return old;  
 }  

也正是基于上述原因,我们通常会得出一个结论:前置操作比后置操作更快。

那么,真的是这样么?下面将分别从内置类型和非内置类型两个方面进行分析。

内置类型

为了便于分析二者的性能,写了个测试代码,用来比较前置++和后置++的性能差异,代码如下:

 void PreInc() {
   for (int i = 0; i < 100000; ++i) {
      for (int j = 0; j < 100000; ++j) {
        for (int k = 0; k < 1000; ++k);
      }
    }
 }
 int main() {
   PreInc();
   return 0;
 }  

 void PostInc() {
   for (int i = 0; i < 100000; i++) {
      for (int j = 0; j < 100000; j++) {
        for (int k = 0; k < 1000; k++);
      }
    }
 }
 int main() {
   PostInc();
   return 0;
 }  

编译运行之后,比较二者的运行时间,对比图如下:

耗时竟然一样 ,颠覆了之前对这块的认知。

使用下述命令生成汇编代码(使用-O0禁用优化以免结果产生偏差):

 $ g++ -O0 -S pre.cc
 $ g++ -O0 -S post.cc

查看上述两个汇编文件的不同点(使用vimdiff): 

 通过上述对比,发现前置++和后置++的汇编结果一致,这也就是说至少对于内置类型(上述代码使用的是int),前置++和后置++的性能一样。在进行搜索的时候,发现了下面这段话:

“The compiler will optimize it away” is an incredibly lazy justification for using i++ instead of ++i. Moreover, it is basically only true for built-in types, not for class types.

从上述可以看出,对于内置类型的后置++操作,编译器会进行优化,而对于非内置内存,则不会进行优化,那么到底是不是这样呢?

自定义类型

迭代器

对于C++开发人员,在遍历vector、list或者set等结构的时候,都习惯于使用迭代器即iterator进行遍历,而gcc实现中,对iterator(此处只罗列了vector相关)的定义如下:

 typedef __gnu_cxx::__normal_iterator<pointer, vector> iterator;

从上述定义可以看出,iterator不是内置类型,同内置类型一样,iterator也支持前置++和后置++,所以,在本节中使用迭代器的前置++和后置++对容器进行遍历,以测试其性能,代码如下:

#include <chrono>
#include <iostream>
#include <numeric>
#include <vector>
 ​
 int main(int, char**)
 {
   std::vector<unsigned> v1( 1000000 );
   std::vector<unsigned> v2( 1000000 );
 ​
   std::iota( v1.begin(), v1.end(), 1 );
   std::iota( v2.begin(), v2.end(), 2 );
 ​
   std::chrono::time_point<std::chrono::high_resolution_clock> t1, t2, t3;
 ​
   t1 = std::chrono::high_resolution_clock::now();
 ​
   for( auto it = v1.begin(); it != v1.end(); ++it )
     *it *= 2;
 ​
   t2 = std::chrono::high_resolution_clock::now();
 ​
   for( auto it = v2.begin(); it != v2.end(); it++ )
     *it *= 2;
 ​
   t3 = std::chrono::high_resolution_clock::now();
 ​
   std::chrono::duration<double> d1 = t2 - t1;
   std::chrono::duration<double> d2 = t3 - t2;
 ​
   std::cout << "pre time cost: " << std::chrono::duration_cast<std::chrono::microseconds>(d1).count() << "us" << std::endl;
   std::cout << "post time cost:  " << std::chrono::duration_cast<std::chrono::microseconds>(d2).count() << "us" << std::endl;
 ​
   return 0;
 }

编译并运行:

  g++ --std=c++11 test.cc -o test; ./test
  pre time cost: 44008us
  post time cost:  58283us

通过上述结果可以看出,对于非内置类型(或者更确切的说对于迭代器类型),前置操作的性能优于后置

上面从执行时间的角度分析了迭代器的前置操作和后置操作对性能的影响,下面是STL中对iterator的源码:

__normal_iterator&
       operator++() // 前置操作
       {
     ++_M_current;
     return *this;
       }

 __normal_iterator
       operator++(int) // 后置操作
       { return __normal_iterator(_M_current++); }

从上面代码可以看出,迭代器的前置和后置操作主要有以下两个区别:

  • 返回值:前置操作返回对象的引用,后置操作返回类型为对象,
  • 拷贝:前置操作无拷贝操作,后置操作存在一次对象拷贝

正式因为这两个原因,前置操作符就地修改对象,而后置操作符将导致创建临时对象,调用构造函数和析构函数(某些情况下编译器会做优化,此处不做讨论),导致了前置操作和后置操作的性能差异。

自定义对象

在上一节中,我们通过迭代器(前置地址和后置递增)遍历对vector进行遍历,证明了前置递增的性能优于后置递增,在本节中,将自定义一个对象,然后进行测试。

代码如下(在最开始的自定义对象中,只有整数value而没有v变量,这就导致测试结果很相近,所以为了更加明显的看出其差异,所以增加了vector ):

class Object {
  public:
    Object(int value)
      : value_(value) {
        v_.emplace_back(value_);
      }

    int Get() { return value_; }

    Object &operator++() // 前置操作
    {
      ++value_;
      v_.emplace_back(value_);
      return *this;
    }

    Object operator++(int) // 后置操作
    {
      Integer tmp = *this; // 拷贝,注意此处这个拷贝行为以及临时变量
      ++value_;
      v_.emplace_back(value_);
      return tmp; //
    }

  private:
    std::vector<int> v_;
    int value_;
};

int main() {
  std::chrono::time_point<std::chrono::high_resolution_clock> t1, t2, t3;

  t1 = std::chrono::high_resolution_clock::now();
  Integer i(0);
  for (int j = 0; j < 10000; ++j) {
    ++i;
  }
  t2 = std::chrono::high_resolution_clock::now();
  Integer k(0);
  for (int j = 0; j < 10000; ++j) {
    k++;
  }
  t3 = std::chrono::high_resolution_clock::now();
  
  std::chrono::duration<double> d1 = t2 - t1;
  std::chrono::duration<double> d2 = t3 - t2;

  std::cout << "pre time cost: " << std::chrono::duration_cast<std::chrono::microseconds>(d1).count() << "us" << std::endl;
  std::cout << "post time cost:  " << std::chrono::duration_cast<std::chrono::microseconds>(d2).count() << "us" << std::endl;
  
  return 0;
}

编译并运行:

g++ --std=c++11 test.cc -o test; ./test
pre time cost: 188us
post time cost:  29625us

从上述测试结果可以进一步看出,前置++的性能优于后置++。

对于内置类型来说,前置++和后置++的性能一样,这是因为编译器会对其进行优化;而对于自定义类型的前置和后置操作,你可能会有疑问,为什么编译器不能像优化内置类型一样,优化自定义类型呢?这是因为依赖于场景。在某些场景下编译器可以进行优化(主要是拷贝部分),但是在某些情况下,编译器无法在不更改代码含义的情况下对其进行优化。所以,除非需要后置操作,否则建议使用前置操作

结语

除非必须使用i++以满足编码场景,否则,在任何情况下都建议使用++i这种前置操作,这是因为:前置操作不会比后置操作性能差(之前有结论是前置操作比后置操作性能优,这个结论不准确)

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

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

相关文章

C++异常介绍

目录 一.异常 1.1C异常概念 1.2异常的使用 1.3异常和栈帧,重新抛出 二.异常体系 2.1自定义异常体系 2.2C标准库的异常体系 2.3异常规范 3.异常的优缺点 3.1优点 3.2缺点 一.异常 1.1C异常概念 语言传统的处理错误的方式&#xff1a; 1. 终止程序&#xff0c;如assert…

浮点类型的比较

浮点类型的比较一.浮点数精度的损失二.浮点数的比较1.方法一2.方法二3.方法三&#xff1a;系统方案一.浮点数精度的损失 关于浮点数的比较就不得不提到浮点数在内存中的存储&#xff0c;但这里篇幅太大&#xff0c;故我将其放在另一篇博客里&#xff0c;&#xff08;如果不了解…

laravel对于百万级别数据导出的一些经验

业务上的需求&#xff0c;我们开发的供应链系统某些业务表也陆续突破了百万级别。 原先使用 \Maatwebsite\Excel 插件导出的效率越来越慢&#xff0c;5w条数据导出基本要达到20min&#xff0c;甚至于30w数据导出基本上都超时。 为了解决这个问题&#xff0c;多种尝试&#xf…

AI 让观众成为 3D 版《老友记》的导演了?

《老友记》上线 3D 版了&#xff1f; 允许用户旋转镜头&#xff0c;且从近景切换到全景观看故事&#xff1f; 今年出炉的 3D 方向 AI 项目 SitCom3D&#xff0c;能够自动补齐《老友记》原剧中的三维拍摄空间&#xff0c;用户可以选择主视图、侧视图等不同角度欣赏剧集。镜头的…

[ vulhub漏洞复现篇 ] solr 远程命令执行 (CVE-2019-17558)

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

优秀的内部知识库对企业的重要性

我们都知道在客户服务方面&#xff0c;选择正确的知识库软件的重要性。但我们经常忘记的是&#xff0c;我们的员工也是我们的客户。根据盖洛普公司最近的研究&#xff0c;世界正在经历一场员工参与危机。只有大约三分之一的美国员工在工作中具有参与感&#xff0c;而在全球范围…

一文读懂Docker、K8s

目标&#xff1a; docker原理以及在运维工作的地位和作用&#xff0c;运维工作进化论&#xff0c;docker、微服务、k8s的联系、devops和docker的关系&#xff0c;docker的前世今生容器、镜像和仓库、容器和虚拟化&#xff0c;优势和劣势&#xff0c;底层的核心容器除了docker还…

什么是项目管理软件,能带来哪些作用?

在这个信息化时代&#xff0c;企业的项目管理除了需要一位出色的项目管理者外&#xff0c;还需要借助项目管理软件来对项目进行全面管理。因为如今的项目需求多样化&#xff0c;内容也愈加丰富&#xff0c;传统的项目管理方式已经难以满足&#xff0c;所以很多项目管理软件也应…

[附源码]JAVA毕业设计小型医院药品及门诊管理(系统+LW)

[附源码]JAVA毕业设计小型医院药品及门诊管理&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项…

CMake中add_subdirectory的使用

CMake中的add_subdirectory命令用于将子目录添加到构建&#xff0c;其格式如下&#xff1a; add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM]) source_dir指定源CMakeLists.txt和代码文件所在的目录。如果它是相对路径&#xff0c;则将相对于当前目录(…

毕业设计 - java web 酒店管理系统的设计与实现【源码+论文】

文章目录前言一、项目设计1. 模块设计总体设计具体模块数据库部分设计2. 实现效果二、部分源码项目源码前言 今天学长向大家分享一个 优秀的毕业设计项目: 酒店管理系统的设计与实现 源码获取方式: https://gitee.com/sinonfin/L-javaWebSha/tree/master 一、项目设计 1. 模…

FreeRTOS任务切换过程深层解析

FreeRTOS 系统的任务切换最终都是在 PendSV 中断服务函数中完成的&#xff0c;uCOS 也是在 PendSV 中断中完成任务切换的。 【为什么用PendSV异常来做任务切换】 PendSV 可以像普通中断一样被 Pending&#xff08;往 NVIC 的 PendSV 的 Pend 寄存器写 1&#xff09;&#xff…

Spark零基础入门实战(五)使用Eclipse创建Scala项目

本节讲解在Windows中使用Scala for Eclipse IDE编写Scala程序。 安装Scala for Eclipse IDE Scala for Eclipse IDE为纯Scala和混合Scala与Java应用程序的开发提供了高级编辑功能,并且有非常好用的Scala调试器、语义突出显示、更可靠的JUnit测试查找器等。 Scala for Eclip…

重磅首发!腾讯前晚最新爆出的“JVM学习笔记”,GitHub已评“钻级”,看完我爱了!

前言 “JVM”&#xff0c;一个虚构出来的计算机&#xff0c;是通过在实际的计算机上仿真模拟各种计算机功能来实现的。有了JVM后&#xff0c;Java语言在不同平台上运行时不需要重新编译&#xff0c;为我们提供了极大的便利性&#xff0c;现在在面试当中“JVM”相关的知识是必问…

5分钟部署云计算|云原生监控平台Prometheus-尚文网络xUP楠哥

~~全文共1277字&#xff0c;阅读需约5分钟。 进Q群11372462&#xff0c;领取专属报名福利&#xff0c;包含云计算学习路线图代表性实战训练大厂云计算面试题资料! # Prometheus介绍 Prometheus是由Go编写的时间序列监控数据库&#xff0c;在目前云计算|云原生时代非常流行&am…

分析linux内核qspi驱动层次

【推荐阅读】 需要多久才能看完linux内核源码&#xff1f; 概述Linux内核驱动之GPIO子系统API接口 https://mp.csdn.net/mp_blog/creation/editor/127819883 一篇长文叙述Linux内核虚拟地址空间的基本概括 纯干货&#xff0c;linux内存管理——内存管理架构&#xff08;建议收藏…

【LeetCode每日一题】——237.删除链表中的节点

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 链表 二【题目难度】 中等 三【题目编号】 237.删除链表中的节点 四【题目描述】 有一个单链…

[附源码]JAVA毕业设计小区失物招领网站(系统+LW)

[附源码]JAVA毕业设计小区失物招领网站&#xff08;系统LW&#xff09; 项目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术…

网红家电逐渐沉寂,家电企业如何利用APS排产调整生产?

随着生活水平的提高&#xff0c;近年来的消费行业逐渐呈现出消费升级、个性化、多元化趋势。在这些趋势下&#xff0c;一大批网红小家电产品迅速出现&#xff0c;以创新性的功能和设计&#xff0c;满足消费者新需求。 近年来&#xff0c;小家电领域已经成为网红爆款产品的集中地…

OpenAI ChatGPT注册步骤(超详细!!!)

最近&#xff0c;很火的OpenAI ChatGPT&#xff0c;大伙都跃跃欲试。 由于注册过程比较麻烦&#xff0c;我整理了一下注册步骤。 一、前期准备&#xff1a; 1、梯子&#xff08;需要科学上网&#xff0c;准备墙外代理&#xff09; 2、国外接码平台&#xff0c;推荐sms-activ…