那么我为什么要使用 addi 而不是 addiu?
通常您不应该这样做,除非像 assert() 那样在加法中没有 2 的补码有符号溢出。如果您有签名的值,您可能希望这样做期望不会溢出,尤其是在手工编写 asm 时。 (编译器从不使用add,总是使用addu。)
除此之外,这些指令实际上是相同的,包括 addiu 的 16 位立即数的符号扩展。 (与ori 和其他零扩展的布尔值不同。MIPS 确实需要对某些指令进行零扩展,但addiu 的符号扩展意味着它也不需要subiu 操作码来减去小整数。)
在您自己使用的程序中,让您的程序因错误的输入而出错可能比让不应该的东西换行要好。如果您正在为 Linux MIPS 而不是 MARS 编写代码,则可以为 SIGFPE(算术异常)安装信号处理程序,这样您至少可以打印一条人性化的错误消息,例如“检测到有符号溢出,正在中止”。在 MARS 中,我假设您只是进入调试器。
add 和 addi 存在的唯一原因(而不是普通的 addu 和 addiu,它们执行大多数 ISA (如 x86 和 ARM)调用 add 的操作)是整数溢出检测。
整数溢出检测是一个难题。 (另见https://en.wikipedia.org/wiki/Integer_overflow)。 ISA 已经进行了各种尝试来为它提供硬件支持,让软件(尤其是高级编译语言)经常使用它。 MIPS 的add/sub 指令就是这样一种尝试,但不包括左移等情况。因此,想要对每个操作提供溢出检查的高级语言将需要一个单独的机制。或者他们可能只是只使用其他机制而不使用add/sub。
与 MIPS 相比,考虑像 x86 或 ARM 这样的 ISA:它们有一个带有符号溢出位的 FLAGS 或状态寄存器,并且如果最后一条指令设置了该位,则可以进行条件分支。 x86 甚至有一个into(如果在 FLAGS 中设置了 OF 则陷阱)指令。
但是 MIPS 根本没有标志寄存器,只有整数寄存器。所以除了通过像add 这样的add-and-branch 指令外,没有地方可以记录加法溢出的事实。在不花费任何位对溢出情况的分支目标进行编码的情况下,唯一真正的选择是陷阱/异常。
(好吧,另一个更易于使用的选择是您可以设置的特殊寄存器,但随后上下文切换必须保存/恢复它。它必须像 JAL 一样工作来记录哪个添加错误. 这可能需要 MIPS 避免的微码. 或者至少对这种特殊情况进行更复杂的处理, 因为它就像一个异常但不是异常, 并且不能像beq 这样的条件尽早检测到, 因为它确实需要一个完整的 32 位相加结果。)
为什么 MIPS 的架构师将普通加法指令命名为addu?
我不知道,也许他们是硬件人员,而不是软件人员,并且过于乐观地认为他们将迎来一个检查算术的新时代,并使许多整数溢出错误成为过去。
事后看来,将add 助记符赋予编译器始终使用的标准非陷阱版本最有意义。然后,您需要为陷印版本取另一个名称。也许addt(陷阱)或addc(添加检查)。但是addc 很糟糕,因为它与其他 ISA 上的 add-with-carry 混淆,通常是adc。 adds(添加签名)是一种可能性。命名很难!
基本知识:在一些明显的情况下,您需要使用addu:
对于(可能)大的无符号整数,使用addu 显然很重要。 0x7fffffff + 1 对于未签名的人来说没什么特别的。但它是从 INT_MAX 到 INT_MIN 的 2 的补码溢出,所以 add 会陷入陷阱。
对于指针,使用addu 可能很重要。除非您使用“高半内核”内存模型,否则一个数组不可能从 0x80000000 以下开始并在其以上结束。
下面是我回答这个问题的第一个版本。上面的部分是多余的。但我认为不是全部。 (TODO:完成编辑;我现在没有时间,但我认为现在发帖比等到以后再写更有用。)
何时使用 addiu 优于 addi,反之亦然(以及为什么更好)。
使用add / addi 仅如果您特别希望机器捕获(又名错误,引发异常)2 的补码溢出(例如,像 INT_MAX + 1 变成 INT_MIN 的环绕)。
大多数时候没有人想要这个,但也许如果你在 asm 中手写并想防御一些整数溢出错误,你可能会使用add,如果引发异常比继续处理错误更好价值。当然这仅适用于add,不能左移超过1 或其他指令。所以如果你真的想在一些防御性代码中进行全面的整数溢出检查,你无论如何都需要另一种机制。
为了这是一个好主意,您可能需要为该硬件异常安装一个中断处理程序。 (或者在操作系统下的用户空间中,SIGFPE 的信号处理程序,算术异常的 POSIX 信号。)
或者在开发过程中,也许您希望整数溢出在您期望不溢出的添加指令上突然停止。即像assert。编译器不这样做是因为有时只进行检查而不检查所有内容的整数运算会很奇怪。但手工可能总比没有好。
整数溢出错误经常发生的一个地方是内存分配大小计算。 (例如,导致分配小而程序结束可能是 DOS 错误,或者取决于分配器的内存覆盖错误)。但是那些经常使用无符号整数;使用 unsigned 执行 0x7fffffff + 1 = 0x80000000 不是错误。
否则使用addu / addiu
addu 是 MIPS 的普通加法指令。
正如Difference between add and addu 指出的那样,这些名称具有误导性。 u 可能对应于有符号溢出是 C 中的未定义行为,但无符号溢出被明确定义为环绕。但如您所知,2 的补码加法与普通无符号二进制加法相同。 addi 仅检查签名溢出。
(并不是说 C 编译器会使用 add 或 addi:他们不想编写永远出错的代码。签名溢出是 UB,但这并不意味着他们必须 做任何特别的事情。此外,在优化之后,使 asm 具有在 C 抽象机中不存在的临时值的情况并不罕见。即使 (w+x)+(y+z) 执行 w+x+y+z 也可能有符号溢出987654370@ 没有;无论有符号溢出如何,二进制加法都是真正的关联。)
MIPS32r6 甚至删除了addi,只留下了无故障的addiu。(所以如果你想捕获有符号溢出,你仍然可以将操作数放在寄存器中并使用@ 987654373@。)这在项目符号列表on Wikipedia 中列出,作为16 位立即数的整数溢出捕获指令,这应该告诉你在现实世界中有多少使用@ 987654374@永远得到。
我的教授似乎只是将它们互换使用以提醒我们addiu可能存在???
很难判断这是否真的是真的,或者他们是否有你没有注意到的微妙原因。例如在绝对安全的情况下使用addi?
或者当他们认为一个整数是有符号的时,即使代码中只包含非负值?例如就 C 抽象机而言,for(int i=0 ; i<100; i++) 是 signed int。
在已知不可能出现带符号溢出的情况下(例如,在lui 之后),这并不重要。但是 IMO,除非你愿意,否则仍然总是使用 addiu。对我来说,作为人类阅读代码,看到 add / addi 理想情况下表明我们有意进行有符号溢出检查。但是在 SO 问题中,更多的初学者只是使用add,因为他们没有考虑甚至不知道addu。因此,它迫使您寻找“误报”陷阱错误的风险:add 在您不希望出现的情况下可能会出错。
我认为没有任何真正的 MIPS CPU 的 add/addi 比 addu / addiu 慢。可能不是;任何lw 或sw 都可能因寄存器输入而出错,因此MIPS 流水线需要能够有效地处理可能出错的指令。
在一般情况下,他们从不出错;您为此优化了管道,实际上承担故障的效率有多低几乎无关紧要。 (或者,在具有软件 TLB 未命中处理的 MIPS 上,这可能确实很重要;这可能会相当频繁地发生,比页面错误更严重)。