在理解如何在C中实现+
,-
,*
和/
等原始运算符时,我从an interesting answer找到了以下代码段。
// replaces the + operator
int add(int x, int y) {
while(x) {
int t = (x & y) <<1;
y ^= x;
x = t;
}
return y;
}
似乎此函数演示了+
在后台实际工作的方式。但是,让我理解它太困惑了。我相信这样的操作是使用编译器长时间生成的汇编指令完成的!
我的问题是:+
运算符是否已作为 MOST 实施中发布的代码实现?这是否利用了两个补充或其他依赖于实现的功能?如果有人能解释它是如何运作的话,我会非常感激。
嗯...也许这个问题在SO上有点偏离主题,但我认为通过这些运算符来看是有点好的。
答案 0 :(得分:184)
为了迂腐,C规范没有指定如何实现添加。
但实际上,小于或等于CPU字大小的整数类型的+
运算符会直接转换为CPU的加法指令,较大的整数类型会转换为多个加法指令用一些额外的位来处理溢出。
CPU内部使用逻辑电路来实现添加,并且不使用循环,位移或任何与C工作方式非常相似的东西。
答案 1 :(得分:77)
当你添加两位时,结果如下:(真值表)
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
因此,如果你按位xor,你可以得到没有携带的总和。 如果你按位进行,你就可以得到进位。
针对多位数a
和b
a+b = sum_without_carry(a, b) + carry_bits(a, b) shifted by 1 bit left
= a^b + ((a&b) << 1)
b
0
后:
a+0 = a
所以算法归结为:
Add(a, b)
if b == 0
return a;
else
carry_bits = a & b;
sum_bits = a ^ b;
return Add(sum_bits, carry_bits << 1);
如果你摆脱了递归并将其转换为循环
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;
考虑到上述算法,代码中的解释应该更简单:
int t = (x & y) << 1;
携带位。如果两个操作数中的右侧1位为1,则进位为1。
y ^= x; // x is used now
无携带添加(忽略进位)
x = t;
重复使用x将其设置为
while(x)
在有更多进位位的情况下重复
递归实现(更容易理解)将是:
int add(int x, int y) {
return (y == 0) ? x : add(x ^ y, (x&y) << 1);
}
似乎这个函数演示了+实际上是如何工作的 背景
没有。 通常(几乎总是)整数加法转换为机器指令添加。这只是演示了使用按位xor和and。
的替代实现答案 2 :(得分:25)
似乎此函数演示了+实际上如何在后台运行
没有。这被转换为add
中实际使用硬件加法器的本机ALU
机器指令。
如果您想知道计算机是如何添加的,这里有一个基本的加法器。
计算机中的所有东西都是使用逻辑门完成的,逻辑门主要由晶体管组成。全加器中有半加器。
有关逻辑门和加法器的基本教程,请参阅this。该视频非常有用,但很长。
在该视频中,显示了一个基本的半加器。如果你想要一个简短的描述,就是这样:
半加器加上给出的两位。可能的组合是:
- 添加0和0 = 0
- 添加1和0 = 1
- 添加1和1 = 10(二进制)
那么现在半加法器是如何工作的?好吧,它由三个逻辑门组成,and
,xor
和nand
。如果两个输入均为负,则nand
给出正电流,这意味着这解决了0和0的情况。xor
给出正输出,其中一个输入为正,另一个为负,这意味着它解决了1和0的问题。and
仅在两个输入均为正时才给出正输出,因此解决了1和1的问题。所以基本上,我们现在得到了半加器。但我们仍然只能添加位。
现在我们制作全加器。全加器包括一次又一次地调用半加器。现在这有一个进位。当我们加1和1时,我们得到一个进位1.那么全加器的作用是,它从半加器中取出,存储它,并将它作为另一个参数传递给半加器。
如果您对如何通过进位感到困惑,您基本上首先使用半加器添加位,然后添加总和和进位。所以现在你已经用两位添加了进位。所以你一次又一次地这样做,直到你必须添加的位结束,然后你得到你的结果。
惊讶?这就是它实际发生的方式。它看起来像一个漫长的过程,但计算机只需几分之一秒,或者更具体,在半个时钟周期内完成。有时它甚至在一个时钟周期内执行。基本上,计算机具有ALU
(CPU
的主要部分),内存,总线等。
如果你想学习计算机硬件,从逻辑门,内存和ALU,并模拟一台计算机,你可以看到这门课程,我从中学到了所有这些:Build a Modern Computer from First Principles
如果您不想要电子证书,它是免费的。该课程的第二部分将于今年春季上映
答案 3 :(得分:15)
C使用抽象机器来描述C代码的作用。所以它没有具体说明。有C&#34;编译器&#34;例如,实际上将C编译成脚本语言。
但是,在大多数C实现中,小于机器整数大小的两个整数之间的+
将被转换为汇编指令(在许多步骤之后)。汇编指令将被转换为机器代码并嵌入您的可执行文件中。汇编是一种语言&#34;删除了一步&#34;来自机器代码,比一堆打包的二进制文件更容易阅读。
然后由目标硬件平台解释该机器代码(在许多步骤之后),其由CPU上的指令解码器解释。该指令解码器接收指令,并将其转换为信号以沿着&#34;控制线发送。这些信号通过CPU从寄存器和存储器路由数据,其中值通常以算术逻辑单元的形式相加。
算术逻辑单元可能有单独的加法器和乘法器,或者可能将它们混合在一起。
算术逻辑单元具有一串执行加法运算的晶体管,然后产生输出。所述输出通过指令解码器产生的信号进行路由,并存储在存储器或寄存器中。
算术逻辑单元和指令解码器中的所述晶体管的布局(以及我已经掩盖的部分)被蚀刻到工厂的芯片中。蚀刻模式通常是通过编译硬件描述语言来生成的,该语言抽象了与操作内容和方式相关的内容,并生成晶体管和互连线。
硬件描述语言可以包含移动和循环,它们不会及时描述发生的事情(像一个接一个),而是 in space - 它描述了硬件不同部分之间的连接。所述代码可能看起来非常模糊,就像您上面发布的代码一样。
以上掩饰了许多部分和层次,并且包含不准确之处。这既是我自己的无能(我已经写了硬件和编译器,但我都不是专家),因为完整的细节需要一两个职业,而不是SO职位。
Here是关于8位加法器的SO帖子。 Here是非SO帖子,您可以在其中注意到某些加法器只在HDL中使用operator+
! (HDL本身理解+
并为您生成较低级别的加法器代码。)
答案 4 :(得分:14)
几乎所有可以运行已编译C代码的现代处理器都将内置支持整数加法。您发布的代码是一种在不执行整数添加操作码的情况下执行整数加法的聪明方法,但通常不执行整数加法的方式。实际上,函数链接可能使用某种形式的整数加法来调整堆栈指针。
您发布的代码依赖于以下观察:添加x和y时,您可以将其分解为它们共有的位以及x或y中唯一的位。
表达式x & y
(按位AND)给出x和y共有的位。表达式x ^ y
(按位异或)给出了x或y之一唯一的位。
总和x + y
可以重写为它们共有的两倍的总和(因为x和y都贡献这些位)加上x或y唯一的位。
(x & y) << 1
是他们共同位的两倍(左移1有效地乘以2)。
x ^ y
是x或y之一唯一的位。
因此,如果我们用第一个值替换x而用第二个值替换y,则总和应保持不变。您可以将第一个值视为按位加法的进位,将第二个值视为按位加法的低位。
此过程一直持续到x为零,此时y保持总和。
答案 5 :(得分:14)
您找到的代码试图解释非常原始的计算机硬件可能实现&#34;添加&#34;指令。我说&#34;可能&#34;因为我可以保证这个方法不被任何 CPU使用,我将解释原因。
在正常生活中,您使用十进制数字,并且您已学习如何添加它们:要添加两个数字,请添加最低两位数字。如果结果小于10,则记下结果并继续下一个数字位置。如果结果是10或更多,你写下结果减10,继续下一个数字,买你记得再加1。例如:23 + 37,你加3 + 7 = 10,你记下0并记得为下一个位置再加1。在10s位置,添加(2 + 3)+ 1 = 6并将其写下来。结果是60.
你可以用二进制数做同样的事情。不同之处在于,唯一的数字是0和1,因此唯一可能的总和是0,1,2。对于32位数字,您将在另一个数字位置之后处理一个数字位置。这就是真正原始的计算机硬件将如何做到这一点。
此代码的工作方式不同。你知道如果两个数字都是1,那么两个二进制数字的总和就是2.所以如果两个数字都是1,那么你将在下一个二进制位置再加1个并写下0。这就是t的计算结果:它找到两个二进制数字都是1的所有位置(&amp;#&amp;)并将它们移动到下一个数字位置(&lt;&lt; 1)。然后它执行加法:0 + 0 = 0,0 + 1 = 1,1 + 0 = 1,1 + 1是2,但我们写下0。这是排除或运算符的作用。
但是你必须在下一个数字位置处理的所有1个问题都没有得到处理。他们仍然需要添加。这就是代码执行循环的原因:在下一次迭代中,添加了所有额外的1。
为什么没有处理器这样做?因为它是一个循环,处理器不喜欢循环,而且速度很慢。它很慢,因为在最坏的情况下,需要32次迭代:如果你将数字加到0到0xffffffff(32位),那么第一次迭代将清除y的第0位并将x设置为2。迭代清除y的第1位并将x设置为4.依此类推。需要32次迭代才能得到结果。但是,每次迭代都必须处理x和y的所有位,这需要大量的硬件。
原始处理器可以像你进行十进制算术一样快速地从最低位置到最高位置。它还需要32个步骤,但每个步骤只处理两位加上前一位位置的一个值,因此实现起来要容易得多。即使在原始计算机中,也可以在不必实现循环的情况下完成此操作。
现代,快速和复杂的CPU将使用&#34;条件和加法器&#34;。特别是如果位数高,例如64位加法器,则节省了大量时间。
64位加法器由两部分组成:首先,32位加法器用于最低32位。那个32位加法器产生一个总和,而一个&#34;携带&#34; (指示1必须将1添加到下一位位置)。第二,高32位的两个32位加法器:一个加x + y,另一个加x + y + 1.所有三个加法器并行工作。然后当第一个加法器产生进位时,CPU只选择两个结果中的哪一个x + y或x + y + 1是正确的,并且你得到了完整的结果。因此,64位加法器只需要比32位加法器长一点点,而不是两倍长。
32位加法器部分再次实现为条件求和加法器,使用多个16位加法器,16位加法器是条件求和加法器,依此类推。
答案 6 :(得分:13)
我的问题是:+运算符是否作为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
指令。
您根本无法讨论如何实施运算符,因为它几乎从不单独使用。
答案 7 :(得分:11)
如果代码细分可以帮助其他人,请举例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
隐藏在t
中
int t = (x & y) <<1;
现在应用bitwise 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
。最后,通过重置t+y
并返回x=t
循环的开头来加总while
:
x = t;
t=0
时(或者,在循环开始时,x=0
),以
return y;
答案 8 :(得分:11)
出于兴趣,在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
特别注意,add是由subi
指令完成的(从寄存器中减去常量),其中0xFF实际上是-1。
同样令人感兴趣的是,这个特定的处理器没有addi
指令,这意味着设计人员认为编译器编写者可以充分地处理补码的减法。
这是否利用了两个补码或其他依赖于实现的功能?
可能公平地说,编译器编写者会尝试以特别架构的最有效方式实现想要的效果(向另一个添加一个数字)。如果这需要减去补数,那就这样吧。