+运算符如何在C中工作?

当理解如何在C中实现+-*/等基本运算符时,我从一个有趣的答案中find以下片段。

 // replaces the + operator int add(int x, int y) { while(x) { int t = (x & y) <<1; y ^= x; x = t; } return y; } 

似乎这个函数演示+实际上在后台工作。 然而,我理解这一点太困惑了。 我相信这样的操作是使用编译器生成的汇编语言很长一段时间来完成的!

我的问题是:是+运营商实施的代码张贴在MOST实现? 这是否利用二进制补码或其他实现相关的function? 如果有人能解释它是如何工作的,我将非常感激。

嗯…也许这个问题是有点偏离主题,但我想通过这些运营商是有点好看的。

为了迂回,C规范没有规定如何实现添加。

但要实际操作,小于或等于CPU字大小的整数types的+运算符会直接转换为CPU的加法指令,而较大的整数types会被转换为多个加法指令,并带有一些额外的位来处理溢出。

CPU在内部使用逻辑电路来实现添加,并且不使用循环,位移或与C工作方式非常相似的任何东西。

当你添加两位时,结果如下:(真值表)

 a | b | sum (a^b) | carry bit (a&b) (goes to next) --+---+-----------+-------------------------------- 0 | 0 | 0 | 0 0 | 1 | 1 | 0 1 | 0 | 1 | 0 1 | 1 | 0 | 1 

所以,如果你按位异或,你可以得到没有进行的总和。 如果你按位进行,你可以得到进位。

扩展这个观察多位数字ab

 a+b = sum_without_carry(a, b) + carry_bits(a, b) shifted by 1 bit left = a^b + ((a&b) << 1) 

一旦b0

 a+0 = a 

所以algorithm归结为:

 Add(a, b) if b == 0 return a; else carry_bits = a & b; sum_bits = a ^ b; return Add(sum_bits, carry_bits << 1); 

如果你摆脱了recursion并将其转换为循环

 Add(a, b) while(b != 0) { carry_bits = a & b; sum_bits = a ^ b; a = sum_bits; b = carrry_bits << 1; // In next loop, add carry bits to a } return a; 

考虑到以上algorithm,从代码解释应该更简单:

 int t = (x & y) << 1; 

携带位。 如果在两个操作数中右边1位为1,则进位位为1。

 y ^= x; // x is used now 

无需进位(忽略进位)

 x = t; 

重新使用x将其设置为进行

 while(x) 

重复,而有更多的进位


recursion实现(更容易理解)将是:

 int add(int x, int y) { return (y == 0) ? x : add(x ^ y, (x&y) << 1); } 

似乎这个函数演示+实际上在后台工作

通常 (几乎总是)整数加法转换为机器指令加。 这只是演示一个替代实现使用按位异或和。

似乎这个函数演示+实际上在后台工作

不可以。这被翻译成ALU生的add机器指令,它实际上是使用硬件加法器。

如果你想知道电脑如何添加,这里是一个基本的加法器。

计算机中的所有东西都是用逻辑门完成的,逻辑门主要由晶体pipe组成。 全加器在其中有半加器。

关于逻辑门和加法器的基本教程,请看这个 。 video非常有帮助,虽然很长。

在该video中,显示了一个基本的半加器。 如果你想要简单的描述,那就是:

半加法器添加了两个给定的位。 可能的组合是:

  • 加0和0 = 0
  • 加1和0 = 1
  • 加1和1 = 10(二进制)

那么现在半加器如何工作呢? 那么,它是由三个逻辑门组成的, andxornand 。 如果两个input都是负的,则nand给出正电stream,这意味着这解决了0和0的情况。 xor给出了正输出,其中一个input是正的,另一个是负的,这意味着它解决了1和0的问题。只有两个input都是正的,才能给出正的输出,这样就解决了1和1的问题。所以基本上我们已经有了我们的半加器。 但是我们仍然只能添加比特。

现在我们做我们的全加器。 全加器由反复调用半加器组成。 现在这有一个进展。 当我们加1和1时,我们得到一个进位1.那么全加器是做什么的,它把来自半加器的进位,存储它,并作为另一个parameter passing给半加器。

如果你感到困惑,你怎样才能通过进位,你基本上是先用半加器来加位,然后再加上和进位。 所以现在你已经增加了进位,两位。 所以你一次又一次地做,直到你必须添加的位都结束了,然后你得到你的结果。

惊讶吗? 这是实际发生的。 它看起来像一个漫长的过程,但计算机在半纳秒内完成,或者在半个时钟周期内完成更具体的操作。 有时甚至在一个时钟周期内执行。 基本上,电脑有ALUCPU的主要部分),内存,总线等。

如果你想学习计算机硬件,从逻辑门,内存和ALU,模拟计算机,你可以看到这个课程,从中我学到了这一点: 从第一原则build立一个现代计算机

如果你不想要电子证书,这是免费的。 课程的第二部分是今年spring

C使用抽象机器来描述C代码的function。 所以没有指定它的工作原理。 例如,有一些C编译器实际上将C编译为脚本语言。

但是,在大多数C实现中,小于机器整数大小的两个整数之间的+将被转换为汇编指令(在多个步骤之后)。 汇编指令将被翻译成机器代码并embedded到你的可执行文件中。 程序集是从机器代码中“一步删除”的语言,旨在比一堆打包的二进制文件更容易阅读。

那个机器码(经过多个步骤)然后被目标硬件平台解释,在那里它被CPU上的指令解码器解释。 这个指令解码器接受指令,并将其转换成信号沿“控制线”发送。 这些信号通过CPU将来自寄存器和存储器的数据路由到数据中,这些值通常以算术逻辑单元相加。

算术逻辑单元可能具有单独的加法器和乘法器,或者可能将它们混合在一起。

算术逻辑单元有一堆执行加法操作的晶体pipe,然后产生输出。 所述输出通过从指令解码器产生的信号被路由,并被存储在存储器或寄存器中。

算术逻辑单元和指令解码器(以及我已经掩盖的部分)中的所述晶体pipe的布局被蚀刻到工厂的芯片中。 刻蚀模式通常是通过编译硬件描述语言来产生的,硬件描述语言抽象了与什么相关的以及如何操作以及如何生成晶体pipe和互连线。

硬件描述语言可以包含移位和循环,这些移位和循环不能描述事件发生的时间 (如一个接一个),而是在空间中 – 它描述硬件不同部分之间的连接。 所述代码可能看起来非常模糊,就像您上面发布的代码。

上面的许多部分和层次的光彩,并包含不准确的地方。 这既是我自己的无能(我写过硬件和编译器,也不是专家),因为完整的细节需要一两个职位,而不是SO职位。

这里是关于一个8位加法器的SOpost。 这里是一个非SOpost,你会注意到一些加法器只是在HDL中使用operator+ ! (HDL本身理解+并为您生成较低级别的加法器代码)。

几乎所有可以运行编译C代码的现代处理器都将支持整数加法。 你发布的代码是一个聪明的方式来执行整数加法,而不执行一个整数加操作码,但它不是如何整数加法通常执行。 实际上,函数链接可能使用某种forms的整数加法来调整堆栈指针。

你发布的代码依赖于这样一个观察:当添加x和y时,可以将它分解成它们共有的位和x或y中唯一的位。

expression式x & y (按位AND)给出x & y共有的位。 expression式x ^ y (按位异或)给出对x或y之一唯一的位。

总和x + y可以被重写为它们共有的位的两倍的总和(因为x和y贡献那些位)加上对于x或y唯一的位。

(x & y) << 1是它们共有的位的两倍(左移1乘以2)。

x ^ y是x或y中唯一的位。

所以,如果我们用第一个值代替x,第二个代替y,那么总和应该不变。 您可以将第一个值作为逐位加法的进位,将第二个值作为逐位加法的低位进行设置。

这个过程继续下去,直到x为零,在这个点上y保持和。

您find的代码试图解释非常原始的计算机硬件可能实现“添加”指令。 我说“可能”,因为我可以保证这个方法不被任何 CPU使用,我会解释为什么。

在正常的生活中,你使用十进制数字,你已经学会了如何添加它们:要添加两个数字,添加最低的两位数字。 如果结果小于10,则记下结果并进入下一个数字位置。 如果结果为10或更多,则记下结果减10,进入下一个数字,买你记得加1。 例如:23 + 37,你加3 + 7 = 10,你写下0,记得在下一个位置加1。 在十几岁的位置,你加(2 + 3)+ 1 = 6并写下来。 结果是60。

你可以用二进制数做完全相同的事情。 不同之处在于唯一的数字是0和1,所以唯一可能的总和是0,1,2。对于一个32位的数字,你可以处理一个数字位置。 这就是真正的原始计算机硬件如何做到这一点。

此代码工作不同。 你知道如果两个数字都是1,那么两个二进制数字的和就是2.所以如果两个数字都是1,那么你在下一个二进制位置加上1就可以了。这就是t的计算:它find所有的地方其中二进制数字是1(即&),并将它们移动到下一个数字位置(<< 1)。 然后它进行加法运算:0 + 0 = 0,0 + 1 = 1,1 + 1 = 1,1 + 1 = 2,但是我们写下0,这就是排除或操作符所做的事情。

但是,你必须在下一个数字位置处理的所有1都没有处理。 他们仍然需要添加。 这就是为什么代码做一个循环:在下一个迭代中,所有额外的1被添加。

为什么没有处理器这样做呢? 因为它是一个循环,处理器不喜欢循环,而且速度很慢。 这很慢,因为在最坏的情况下,需要32次迭代:如果将数字1加到0xffffffff(32个1位),则第一次迭代清除y的第0位,并将x设置为2.第二次迭代清零位1 y并将x设置为4.依此类推。 需要32次迭代来获得结果。 但是,每次迭代必须处理x和y的所有位,这需要很多硬件。

一个原始的处理器会像从小数点到最高点一样快速地进行小数运算。 它也需要32个步骤,但是每一步只处理两位,加上前一位的一个值,所以实现起来要容易得多。 即使是在一台原始的计算机上,也可以做到这一点,而不必执行循环。

现代,快速和复杂的CPU将使用“条件和加法器”。 特别是如果位数很高,例如一个64位的加法器,就可以节省很多时间。

一个64位的加法器由两部分组成:首先是32位的最低32位加法器。 这个32位加法器产生一个和,和一个“进位”(一个指示符1必须加到下一个位的位置)。 其次,对于更高的32位,两个32位加法器:一个加上x + y,另一个加上x + y + 1。所有三个加法器并行工作。 那么当第一个加法器产生了它的进位时,CPU只是select两个结果x + y或x + y + 1中哪一个是正确的,并且你有完整的结果。 所以一个64位加法器只比32位加法器长一点点,而不是两倍长。

32位加法器部分再次实现为条件和加法器,使用多个16位加法器,16位加法器是条件和加法器,等等。

我的问题是:是+运营商实施的代码张贴在MOST实现?

我们来回答一下实际的问题。 所有的操作符都被编译器实现为一些内部的数据结构,最终在一些转换之后被转换成代码。 你不能说一个单一的添加会产生什么代码,因为几乎没有真实的编译器为单个语句生成代码。

编译器可以自由地生成任何代码,只要它的行为就像按照标准执行实际操作一样。 但实际发生的事情可能完全不同。

一个简单的例子:

 static int foo(int a, int b) { return a + b; } [...] int a = foo(1, 17); int b = foo(x, x); some_other_function(a, b); 

这里没有必要生成任何补充说明。 编译器将其转换成以下代码是完全合法的:

 some_other_function(18, x * 2); 

或者,也许编译器注意到你连续几次调用函数foo ,这是一个简单的算术,它会为它生成向量指令。 或者加法的结果稍后用于数组索引,并且将使用lea指令。

你根本不能谈论运营商是如何实施的,因为它几乎从来没有单独使用。

如果代码细分可以帮助其他人,例如x=2, y=6


x不是零,所以开始添加到y

 while(2) { 

x & y = 2因为

  x: 0 0 1 0 //2 y: 0 1 1 0 //6 x&y: 0 0 1 0 //2 

2 <<1 = 4因为<< 1将所有位左移:

  x&y: 0 0 1 0 //2 (x&y) <<1: 0 1 0 0 //4 

总之,把这个结果隐藏起来, 4

 int t = (x & y) <<1; 

现在应用按位XOR y^=x

  x: 0 0 1 0 //2 y: 0 1 1 0 //6 y^=x: 0 1 0 0 //4 

所以x=2, y=4 。 最后,通过重置x=t并返回到while循环的开始来总结t+y

 x = t; 

t=0 (或者在循环的开始处, x=0 )时,用

 return y; 

出于兴趣,在Atmega328P处理器上,使用avr-g ++编译器,以下代码实现了通过减去-1来添加一个:

 volatile char x; int main () { x = x + 1; } 

生成的代码:

 00000090 <main>: volatile char x; int main () { x = x + 1; 90: 80 91 00 01 lds r24, 0x0100 94: 8f 5f subi r24, 0xFF ; 255 96: 80 93 00 01 sts 0x0100, r24 } 9a: 80 e0 ldi r24, 0x00 ; 0 9c: 90 e0 ldi r25, 0x00 ; 0 9e: 08 95 ret 

特别要注意的是,在这种情况下,通过subi指令(从寄存器中减去常量)来完成添加,其中0xFF实际上是-1。

另外值得关注的是,这个特定的处理器没有addi指令,这意味着devise者认为执行补码的减法操作将被编译器编写者充分处理。

这是否利用二进制补码或其他实现相关的function?

说编译器编写者试图以最有效的方式实现想要的效果(向另一个编号添加一个数字)可能是公平的。 如果这需要减去补充,那就这样吧。