用异或来交换两个变量是错误的

关于除以整数常数的优化 很多人都知道,整数变量乘上常数的运算我们可以通过移位和加减法运算来代替, 比如计算 X*2,可以通过X+X或X<<1来替换 而 X*9,可以通过(X<<3)+X来替换 X*7可以通过(X<<3)-X来替换,等等. 由于加法和移位运算在计算机中远远快于乘除运算,所以通过这种替换,运算速度可以得到极大的提高.现在一般的编译器都能够支持这种优化的,所以在写... 阅读详情
AI摘要

本文讨论了使用异或运算交换两个变量的常见做法,并指出这种方法在现代编译器下不仅不节省空间,反而可能导致性能下降。作者通过对比不同交换方法的汇编代码,强调了不应低估编译器的优化能力,以及代码清晰性的重要性。此外,还提到了C++模板在特定场景下的效率优势。

用异或来交换变量是错误的

 

陈硕 (giantchen_AT_gmail)

Blog.youkuaiyun.com/Solstice

 

翻转一个字符串,例如把 "12345" 变成 "54321",这是一个最简单的不过的编码任务,即便是 C 语言初学者的也能毫不费力地写出类似如下的代码:

 

// 版本一,用中间变量交换两个数,好代码

void reverse_by_swap(char* str, int n)

{

  char* begin = str;

  char* end = str + n - 1;

  while (begin < end) {

    char tmp = *begin;

    *begin = *end;

    *end = tmp;

    ++begin;

    --end;

  }

}

 

这个代码清晰,直白,没有任何高深的技巧。

 

不知从什么时候开始,有人发明了不使用临时变量交换两个数的办法,用“不用临时变量 交换 两个数”在 google 上能搜到很多文章。下面是一个典型的实现:

 

// 版本二,用异或运算交换两个数,烂代码

void reverse_by_xor(char* str, int n)

{

  // WARNING: BAD code

  char* begin = str;

  char* end = str + n - 1;

  while (begin < end) {

    *begin ^= *end;

    *end ^= *begin;

    *begin ^= *end;

    ++begin;

    --end;

  }

}

 

受一些过时的教科书的误导,有人认为程序里少用一个变量,节省一个字节的空间,会让程序运行更快。这是不对的,至少在这里不成立:

 

1.       这个所谓的“技巧”在现代的机器上只会更慢(我甚至怀疑它从来就不可能比原始办法快)。原始办法是两次内存读和写,这个"技巧"是六读三写加三次异或(或许编译器可以优化成两读三写加三次异或)。

2.       同样也不能节省内存,因为中间变量 tmp 通常会是寄存器(稍后有汇编代码供分析)。就算它在函数的局部堆栈(stack)上,反正栈已经开在那儿了,也没有进一步的函数调用,根本节约不了一丁点内存。

3.       相反,由于计算步骤较多,会使用更多的指令,编译后的机器码长度会增加。(这不是什么大问题,短的代码不一定快,后面有另外一个例子。)

 

这个技巧的意义完全在于应付变态的面试,所以知道就行,但绝对不能放在产品代码中。我也想不出问这样的面试题意义何在。

 

更有甚者,把其中三句:

    *begin ^= *end;

    *end ^= *begin;

    *begin ^= *end;

写成一句:

    *begin ^= *end ^= *begin ^= *end; // WRONG

这更是大有问题,会导致未定义的行为(undefined behavior)。C 语言的一条语句中,一个变量的值只允许改变一次,像 x = x++ 这种代码都是未定义行为。在C语言里没有哪条规则保证这两种写法是等价的。
(致语言律师:我知道,黑话叫序列点,一个语句可能不止一个序列点,请允许我在这里使用不精确的表述。)

 

这不是一个值得炫耀的技巧,只会丑化劣化代码。

 

翻转字符串这个问题在 C++ 有更简单的解法——调用标准库里的 std::reverse。有人担心调用函数会有开销,这种担心是多余的,现在的编译器会把std::reverse() 这种简单函数自动内联展开,生成出来的优化汇编代码和“版本一”一样快。

 

// 版本三,用 std::reverse 颠倒一个区间,优质代码

void reverse_by_std(char* str, int n)

{

  std::reverse(str, str + n);

}

 

 

======== 第二部分,编译器会分别生成什么代码 ========

 

注意:查看编译器生成的汇编代码固然是了解程序行为的一个重要手段,但是千万不要认为看到的东西是永恒真理,它只是一时一地的真相。将来换了硬件平台或编译器,情况可能会变化。重要的不是为什么版本一比版本二快,而是如何发现这个事实。不要“猜 guess”,要“测 benchmark”。

 

g++ 版本 4.4.1,编译参数-O2 -march=core2,x86 Linux 系统。

版本一编译的汇编代码是:

 

.L3:

        movzbl  (%edx), %ecx

        movzbl  (%eax), %ebx

        movb    %bl, (%edx)

        movb    %cl, (%eax)

        incl    %edx

        decl    %eax

        cmpl    %eax, %edx

        jb      .L3

 

我用 C 语言翻译一下:

register char bl, cl;

register char* eax;

register char* edx;

 

L3:

cl = *edx; // 读

bl = *eax; // 读

*edx = bl; // 写

*eax = cl; // 写

++edx;

--eax;

if (edx < eax) goto L3;

 

一共两读两写,临时变量没有使用内存,都在寄存器里完成。考虑指令级并行和cache的话,中间六条语句估计能在3、4个周期执行完。

 

版本二

 

.L9:

        movzbl  (%edx), %ecx

        xorb    (%eax), %cl

        movb    %cl, (%eax)

        xorb    (%edx), %cl

        movb    %cl, (%edx)

        decl    %edx

        xorb    %cl, (%eax)

        incl    %eax

        cmpl    %edx, %eax

        jb      .L9

 

C 语言翻译:

// 声明与前面一样

cl = *edx;   // 读

cl ^= *eax;  // 读,异或

*eax = cl;   // 写

cl ^= *edx;  // 读,异或

*edx = cl;   // 写

--edx;

*eax ^= cl;  // 读、写,异或

++eax;

if (eax < edx) goto L9;

 

一共六读三写三次异或,多了两条指令。指令多不一定就慢,但是这里异或版实测比临时变量版要慢许多,因为它每条指令都用到了前面一条指令的计算结果,没法并行执行。

 

版本三,生成的代码与版本一一样快。

 

.L21:

        movzbl  (%eax), %ecx

        movzbl  (%edx), %ebx

        movb    %bl, (%eax)

        movb    %cl, (%edx)

        incl    %eax

.L23:

        decl    %edx

        cmpl    %edx, %eax

        jb      .L21

 

 

这告诉我们,不要想当然地优化,也不要低估编译器的能力。关于现在的编译器有多聪明,这里有一个不错的介绍 http://www.linux-kongress.org/2009/slides/compiler_survey_felix_von_leitner.pdf

 

 

Bjarne Stroustrup 说过, I like my code to be elegant and efficient. The logic should be straightforward to make it hard for bugs to hide, the dependencies minimal to ease maintenance, error handling complete according to an articulated strategy, and performance close to optimal so as not to tempt people to make the code messy with unprincipled optimizations. Clean code does one thing well. 中文据韩磊的翻译《代码整洁之道》 http://www.china-pub.com/196266 (陈硕对文字有修改,出错责任在我):我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;以某种全局策略一以贯之地处理全部出错情况;性能调校至接近最优,省得引诱别人实施无原则的优化(unprincipled optimizations),搞出一团乱麻。整洁的代码只做好一件事。

 

这恐怕就是Bjarne提及的没有原则的优化,甚至根本连优化都不是。代码的清晰性是首要的。

 

 

======== 第三部分,为什么短的代码不一定快 ========

 

我前两天的一篇博客谈到负整数的除法运算 http://blog.youkuaiyun.com/Solstice/archive/2010/01/06/5139302.aspx ,其中引用了一段把整数转为字符串的代码。函数反复计算一个整数除以10的商和余数。我原以为编译器会用一条DIV除法指令来算,实际生成的代码让我大吃一惊:

 

.L2:

        movl    $1717986919, %eax

        imull   %ebx

        movl    %ebx, %eax

        sarl    $31, %eax

        sarl    $2, %edx

        subl    %eax, %edx

        movl    %edx, %eax

        leal    (%edx,%edx,4), %edx

        addl    %edx, %edx

        subl    %edx, %ebx

        movl    %ebx, %edx

        movl    %eax, %ebx

        movzbl  (%edi,%edx), %eax

        movb    %al, (%esi)

        addl    $1, %esi

        testl   %ebx, %ebx

        jne     .L2

 

一条 DIV 指令被替换成了十来条指令,编译器不是傻子,必然有原因。这里我不详细解释到底是怎么算的,基本思路是把除法转换为乘法,用倒数来算。其中出现了一个魔数 1717986919,转换成16进制是 0x66666667,等于 (2**33+3)/5。

 

现代处理器上乘法运算和加减法一样快,比除法快一个数量级左右,编译器生成这样的代码是有理由的。10多年前出版的神作《程序设计实践》上介绍过如何做 micro benchmarking,方法和结果都值得一读,当然里边的数据恐怕有点过时了。

 

有本奇书《Hacker's Delight》,国内译作《高效程序的奥秘》 http://www.china-pub.com/18801 ,展示了大量这种速算技巧,第10章专门讲整数常量的除法。我不会把书中如天书般的技巧应用到产品代码中,但是我相信现代编译器的作者是知道这些技巧的,他们会合理地使用这些技巧来提高生成代码的质量。现在已经不是那个懂点汇编就能打败编译器的时代了。有一篇文章《The “C is Efficient” Language Fallacy》http://scienceblogs.com/goodmath/2006/11/the_c_is_efficient_language_fa.php 的观点我非常赞同:

Making real applications run really fast is something that's done with the help of a compiler. Modern architectures have reached the point where people can't code effectively in assembler anymore - switching the order of two independent instructions can have a dramatic impact on performance in a modern machine, and the constraints that you need to optimize for are just more complicated than people can generally deal with.

So for modern systems, writing an efficient program is sort of a partnership. The human needs to careful choose algorithms - the machine can't possibly do that. And the machine needs to carefully compute instruction ordering, pipeline constraints, memory fetch delays, etc. The two together can build really fast systems. But the two parts aren't independent: the human needs to express the algorithm in a way that allows the compiler to understand it well enough to be able to really optimize it.

 

最后,说几句C++模板。假如要编写一个任意进制的转换程序。C 语言的函数声明是:

bool convert(char* buf, size_t bufsize, int value, int radix);

 

既然进制是编译期常量,C++ 可以用带非类型模板参数的函数模板来实现,函数里边的代码与 C 相同。

template<int radix>

bool convert(char* buf, size_t bufsize, int value);

 

模板确实会使代码膨胀,但是这样的膨胀有时候是好事情,编译器能针对不同的常数生成快速算法。滥用 C++ 模板当然是错的,适当使用不会有问题。

 

======== 全文完,欢迎转载,请保留署名和链接 ========

 

Ubuntu 20.04下IgH EtherCAT主站部署实战 本文详细介绍了在Ubuntu 20.04系统上部署IgH EtherCAT主站的完整流程,包括环境准备、源码编译、内核模块配置、网络接口设置及实时性优化等关键步骤。通过实战案例和常见问题排查,帮助开发者快速掌握EtherCAT主站部署技巧,提升工业自动化系统的稳定性和实时性。 阅读详情

相关推荐

UOS的ARM机编译QT5.14.2记录

我需要的qt版本是Qt5.14.2,所以选择编译Qt5.14.2源码;几个月前尝试了编译一次,当时编译过程中出现了报错,当时时间很紧最后安装的默认版本来使用,最近有时间重新进行了一次编译,历经3个星期(4G的内存)终于解决了各种坑,编译成功了,现在将编译过程中遇见的一些坑记录一下,方便日后能记得这一次可恶的编译过程,如有描述欠缺的地方,还希望各位大佬给点意见指导指导🙏。

NiF693zzz的博客 1618

我为什么不喜欢用异或做两数值的交换

大家在初学编程的过程中,肯定会学习到使用代码去交换两元素的值虽然已经过去了很久,但笔者对当时老师上课所讲的话记忆犹新:“交换两个值,就像是把醋瓶子里的酒装入酒罐,把酒罐子里的醋倒入醋瓶。要完成此任务,就要多找一个空瓶子,作为两者的交换的暂存罐”同理,代入代码中,我们可以多定义一个变量,作为数值的“暂存罐”int a = 5; int b = 3; int tmp; tmp = a; a = b;...

Days_of_April的博客 2191

异步FIFO的隐形陷阱:复位信号同步化实战解析

本文深入解析异步FIFO设计中复位信号同步化的隐形陷阱,重点分析赛灵思xpm_fifo_async原语在FPGA应用中的常见问题。通过实战代码和系统集成方案,提供复位同步化的最佳实践,帮助工程师避免跨时钟域通信故障,提升系统可靠性。

keras9composer的博客 245

使用按位异或交换两个数字的问题

在C++中,如果要交换两个变量的值,我们通常会创建一个临时数据以它为桥梁来交换。熟悉位运算的同学也会这么做: int a = 1, b = 2; a ^= b; b ^= a; a ^= b; 这样做省去了额外变量的创建,显得比较优雅。但是这么做有一个问题:如果a==b,那么交换完后的a与b就都是0了,这是因为两个相同的数字按位异或的结果是0。因此,三次按位异或的方法只适用于交换两个不同的数字,所以,在一些算法设计中,为了避免一些神奇的bug,请避免使用三次异或。 ...

锦恢的博客 955

【ESP32实战】SPI驱动ILI9341屏幕:从基础绘图到汉字显示

本文详细介绍了如何使用ESP32通过SPI驱动ILI9341屏幕,从基础连接、SPI初始化到实现基础绘图、数字显示及汉字显示的全过程。文章包含实用的代码示例和优化技巧,帮助开发者快速掌握屏幕驱动技术,提升显示性能。特别针对常见问题如花屏、刷新率等提供了解决方案。

weixin_29164091的博客 621

使用 DeepSeek 配合 即梦AI 生成视频的详细教程

访问即梦AI官网,选择“AI作图”功能,将DeepSeek生成的提示词逐条粘贴至输入框,选择适配的模型(如“通用XL Pro”)和参数(如画幅比例、风格强度)1113。“我要制作一个关于普洱茶文化的宣传视频,请设计一个30秒的脚本,包含时间轴、画面描述、文案和分镜提示词,以表格形式输出。:云南古茶树群,清晨薄雾,金色晨光,露珠特写,国风山水画质感,16:9画幅,使用Nikon D850参数模拟513。:结合DeepSeek生成的知识点脚本与即梦AI的插画风格,制作科普动画211。

weixin_48995695的博客 3万+

代码优化-之-优化除法

                                                >                                                   HouSisong@GMail.comtag:代码优化,除法,牛顿迭代,减法代替除法,除法优化   说明:文章中的很多数据可能在不同的CPU或不同的系统环境下有不同的结果,数据仅供参考   x

HouSisong的专栏 1万+

【项目实战】使用Python - Mediapipe、OpenCV进行手部面部检测(Python源码)

本文介绍了使用MediaPipe和OpenCV库进行实时面部和手部关键点检测的方法。通过MediaPipe的Holistic模型,可以获取468个面部特征点及每只手21个特征点。文章详细说明了环境配置、模型初始化参数设置,并提供了完整的代码实现,包括视频流处理、关键点绘制和FPS计算等功能。这套方案可应用于手语识别、疲劳检测等计算机视觉场景,为开发者提供了一套高效的开箱即用解决方案。

梦想起飞的地方 481

汇编除法转换

今天写了个小程序,用 IDA 反汇编过来看了一下,如下一条语句: num/= 5; 却被转化成了这种形式: mov     ecx, eax   ;num mov     eax, 66666667h imul    ecx sar     edx, 1 mov     eax, edx 最终 eax 就是 num 的值,为什么会

Aegeaner的专栏 1777

ROS机器人未知环境自主探索功能包explore_lite总结

本系列文章主要针对ROS机器人常使用的未知环境自主探索功能包explore_lite展开全源码的详细解析,并进行概括总结。 本系列文章共包含六篇文章,前五篇文章主要介绍explore_lite功能包中 explore.cpp、costmap_tools.h、frontier_search.cpp、costmap_client.cpp等源码实现文件中全部函数的详细介绍,第六篇文章进行概括总结,包含自主探索功能包explore_lite的简介,实现未知环境自主探索功能的原理及流程总结,效果演示,使用功能包exp

慕羽★的博客 4461

工艺边与定位孔设计经验规则总结

工艺边是在SMT焊接时,为了PCB和导轨接触预留的PCB边。工艺边和PCB的接触部分一般需要开槽,或者使用邮票孔,以方便后期去除工艺边。

fydar的博客 1985

用异或来交换两个变量能提高速度是错误的

在进行两个变量的时候,经常会看到有些书误人子弟的推荐使用异或的方式: 方式一 {   x = x ^ y;   y = x ^ y;   x = x ^ y; } 而不是采用临时变量实现交换: 方式二 {   int temp;   temp = a;        a = b;   b = temp; } 美其名曰:节省内存,提高运行速度。 临时变量交换的方式:

tietao的专栏 2782

【Unity】【WebRTC】如何用Unity而不是浏览器接收远程画面

之前几篇我们讨论了如何设置信令服务器,如何发送画面给远端以及如何用浏览器查看同步画面,今天来讨论如何实现Unity内部接收画面。看本篇之前请先看过之前将web服务器设置和基本远程画面功能的几篇博文。在Unity编辑器中打开一个示例项目,点击BroadCast按钮。4. 再点击Receiver按钮,Start。Build一个示例项目,并运行这个项目。启动Web.app。

每日出拳老爷子的博客 1721

【转】用异或来交换两个变量是错误的

翻转一个字符串,例如把"12345"变成"54321",这是一个最简单的不过的编码任务,即便是C语言初学者的也能毫不费力地写出类似如下的代码: 1 // 版本一,用中间变量交换两个数,好代码 2 3 void reverse_by_swap(char* str, int n) 4 5 { 6 7 char* begin = str; 8...

weixin_33826268的博客 117

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

[Z]用异或来交换两个变量是错误的

陈硕 (giantchen_AT_gmail) Blog.youkuaiyun.com/Solstice 原文:http://blog.youkuaiyun.com/Solstice/article/details/5166912   翻转一个字符串,例如把 "12345" 变成 "54321",这是一个最简单的不过的编码任务,即便是 C 语言初学者的也能毫不费力地写出类似如下的代码:   // 版本一,用中...

weixin_33816611的博客 234

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

用异或来交换两个变量能提快速度是错误的

在进行两个变量的时候,常常会看到有些书误人子弟的推荐使用异或的方式: 方式一 {   x = x ^ y;   y = x ^ y;   x = x ^ y; } 而不是採用暂时变量实现交换: 方式二 {   int temp;   temp = a;        a = b;   b = temp; } 美其名曰:节省内存,提高执行速度。 暂时变量交换的...

weixin_33795806的博客 315

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

C位运算,& | ^,异或运算交换变量的错误性原因

1.&与运算(只有1&1才为1,其他全为0) 应用:可以实现指定位清零; 应用:取出给出值的指定位; 2.|或运算(只有0|0才为0,其他全为1) 应用:结合|和&实现大小写字母转换? 3.^异或(或者XOR)运算符(相同为0不同为1,支持逆运算,常用于加密算法) 应用:特定位翻转:与0异或不变,与1异或则翻转。比如,使01111010前四位翻转,则跟...

xzengwei1313的博客 1968

不引入变量 异或交换的缺点

文章提供了标准的交换方法(使用中间变量)和异或交换方法,并指出异或方法在i=j时会导致数据错误归零的问题。建议直接使用引入中间变量的标准交换方法。

m0_75257168的博客 490

c语言交换两个变量的值 讲课,从交换两个变量值看c语言学习历程,语言学习历程...

从交换两个变量值看c语言学习历程,语言学习历程在开始你使用这种方法int tmp;tmp = a;a = b;b = tmp;后来你知道了模块化编程,你知道这样是不好的,于是你开始使用函数.void swap(int *a, int *b);{int tmp;tmp = *a;*a = *b;*b =tmp;}//下面我就不写函数了随着c语言的学习,后来你发现下面代码也能完成两变量的值交换的任x ...

weixin_39568172的博客 153

XOR 异或的性质与应用总结

下面的内容均来自于网络的收集和汇总。 异或的实质是按位模2加,简单理解就是不进位加法如: 1⊕1 = 1^1 =0; 1⊕0= 1^0 =1; 0⊕0= 0^0=0; 按位模2加: 101 ^ 110 ^ 111 = 100 101 110 111 -------- 322 %2 -------- 100 性质 交换律:A ^ B = B ^ A; 结合律:A ^ B ^ C = (A ^ B) ^ C =A ^ (B ^ C); 恒等...

eleday的电子世界 1362
上一篇: 请访问 blog.youkuaiyun.com/Solstice
下一篇: 多核时代不宜再用 x86 的 RDTSC 指令测试指令周期和时间
陈硕
博客等级 码龄26年 1万+粉丝 144原创
评论 43
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值