在 MIPS 中使用 addi 与 addiu 的具体情况

Specific situations to use addi vs addiu in MIPS

我读过几本关于 addiu 的 post 文章,我认为如果我知道一些特定 situations/cases 的示例,那么在什么情况下使用 addiu 优于 addi 和反之亦然(以及为什么更好)。

这个post表示地址处理和地址加法使用addiu来防止溢出很重要"trap."但是似乎说陷阱甚至没有真正的好处,所以为什么我应该使用 addi 而不是 addiu?

我的教授似乎只是交替使用它们来提醒我们 addiu 可能存在???但它有时会让我感到困惑 "Do I really need to use addiu in this case or are they just using it for 'fun'?"

so why should I use addi at all instead of addiu?

通常你不应该这样做,除非 assert() 加法中没有 2 的补码有符号溢出。 在某些情况下你可能需要它在您希望不会溢出的地方签名了值,尤其是在手动编写 asm 时。 (编译器从不使用 add,总是使用 addu。)

除此之外,指令在字面上完全相同,包括 addiu 的 16 位立即数的符号扩展。 (与 ori 和其他零扩展的布尔值不同。MIPS 确实需要对某些指令进行零扩展,但是 addiu 的符号扩展意味着它不需要 subiu 操作码来减去小整数。)

在您自己使用的程序中,让您的程序因输入错误而出错可能比让不应该的东西换行要好。如果您正在为 Linux MIPS 而不是 MARS 编写代码,您可以为 SIGFPE(算术异常)安装一个信号处理程序,这样您至少可以打印一条人性化的错误消息,例如 "signed overflow detected, aborting"。在 MARS 中,我假设您只是进入调试器。


存在 addaddi 的唯一原因(而不只是正常的 adduaddiu,它们做大多数 ISA像 x86 和 ARM 调用 add) 是整数溢出检测。

整数溢出检测是个难题。 (另请参阅 https://en.wikipedia.org/wiki/Integer_overflow)。 ISA 已经做出各种尝试来为它提供硬件支持,让软件(尤其是高级编译语言)经常使用它。 MIPS 的 add/sub 指令就是这样一种尝试,但不包括左移等情况。因此,想要为每个操作提供溢出检查的高级语言将需要一个单独的机制。或者他们可能只是使用其他机制而不是add/sub

作为与 MIPS 的对比,考虑像 x86 或 ARM 这样的 ISA:它们有一个带有符号溢出位的标志或状态寄存器,以及如果最后一条指令设置该位则执行条件分支的方法。 x86 甚至有一个 into (如果在 FLAGS 中设置了 OF 则陷阱)指令。

但是 MIPS 根本没有标志寄存器,只有整数寄存器。所以没有地方可以记录加法溢出的事实,除非通过像 add 这样的加法和分支指令。在不花费任何位来为溢出情况编码分支目标的情况下,唯一真正的选择是陷阱/异常。

(好吧,另一个更容易使用的选择是一个你可以设置的特殊寄存器,但是上下文切换必须 save/restore 它。它必须像 JAL 一样工作以添加错误的记录。这可能需要 MIPS 避免的微码。或者至少对该特殊情况进行更复杂的处理,因为它就像一个异常但不是异常,并且不能尽早检测到 beq 因为它确实需要一个完整的 32 位加法结果。)


为什么 MIPS 的架构师将普通加法指令命名为 addu

我不知道,也许他们是硬件人员,而不是软件人员,并且过于乐观地认为他们会迎来一个检查算术的新时代,并使许多整数溢出错误成为过去。

事后看来,为编译器始终使用的标准非陷阱版本提供 add 助记符是最有意义的。然后,您需要为诱捕版本起另一个名字。可能 addt(陷印)或 addc(添加检查)。但是 addc 很糟糕,因为它与其他 ISA 上的 add-with-carry 混淆,通常是 adcadds(添加签名)是一种可能性。取名好难!


基础知识:在一些明显的情况下,您需要使用addu:

对于(可能)大的无符号整数,使用addu显然很重要。 0x7fffffff + 1 对于无符号来说没什么特别的。但它是从 INT_MAX 到 INT_MIN 的 2 的补码溢出,所以 add 会陷入困境。

对于指针,使用addu可能很重要。除非您使用 "high half kernel" 内存模型,其中一个数组不可能从 0x80000000 以下开始并在 0x80000000 以上结束。


以下是我的第一版回答。部分内容与上述内容多余。但我认为并非全部。 (TODO:完成编辑;我现在没有时间,但我认为现在 post 比坐等到以后更有用。)

when it's better to use addiu over addi and vice-versa (and why it's better).

使用 add / addi ONLY 如果你特别希望机器捕获 (又名错误,引发异常)2 的补码有符号溢出(例如环绕 INT_MAX + 1 变成 INT_MIN)。

大多数时候没有人想要这个,但也许如果你在 asm 中手工编写并且想要防御一些整数溢出错误,你可能会使用 add,如果引发异常比继续使用错误的值。当然这只适用于add,不能左移多于1或其他指令。所以如果你真的想要在一些防御性代码中进行全面的整数溢出检查,你无论如何都需要另一种机制。

如果这是一个好主意,您可能需要为该硬件异常安装一个中断处理程序。 (或者在 OS 下的 user-space 中,SIGFPE 的信号处理程序,用于算术异常的 POSIX 信号。)

或者在开发过程中,您可能希望整数溢出突然停止在您期望 而不是 溢出的添加指令上。即像 assert。编译器不这样做,因为有时只检查而不检查所有整数算法会很奇怪。但手工可能总比没有好。

经常发生整数溢出错误的一个地方是内存分配大小计算。 (例如,导致小的分配和程序结束它可能是 DOS 错误,或内存覆盖错误,具体取决于分配器)。但是那些经常使用无符号整数;用 unsigned.

0x7fffffff + 1 = 0x80000000 不是错误

否则请使用 addu / addiu

addu是MIPS的普通加法指令。

正如 Difference between add and addu 指出的那样,名称具有误导性。 u 可能对应于 C 中未定义行为的有符号溢出,但无符号溢出被明确定义为环绕。但如您所知,2 的补码加法与普通无符号二进制加法是相同的二进制运算。 addi 仅检查签名溢出。

(并不是说 C 编译器将永远使用 addaddi:他们不想编写永远出错的代码。有符号溢出是 UB,但这并不意味着他们 必须 做任何特别的事情。此外,在优化之后,制作具有 C 抽象机中不存在的临时值的 asm 并不少见。将 w+x+y+z 作为 (w+x)+(y+z) 可能有符号溢出,即使 (((w+x)+y)+z) 没有;二进制加法是真正结合的,不管有符号溢出。)


MIPS32r6 甚至删除了 addi,只留下无故障的 addiu.(所以如果你想捕获有符号溢出,你仍然可以将操作数放在寄存器中并使用 add。)这在删除的不常用指令的项目符号列表 on Wikipedia 中列为 具有 16 位立即数的整数溢出捕获指令 这应该告诉您一些关于实际使用量 addi 的一些信息。


My professor just seems to use them interchangeably in an effort to remind us addiu exists maybe???

很难从中判断这是否属实,或者他们是否有您没有注意到的微妙原因。例如在绝对安全的情况下使用 addi

或者每当他们考虑一个带符号的整数时,即使代码中只包含非负值?例如就 C 抽象机而言,for(int i=0 ; i<100; i++) 是一个 signed int

在已知不可能发生有符号溢出的情况下(例如在 lui 之后),这并不重要。但是 IMO,除非你愿意,否则仍然总是使用 addiu 。对我来说,作为人类阅读代码,看到 add / addi 理想情况下是我们有意进行签名溢出检查的标志。但是在SO题中,初学者更多的时候只是用add,因为他们没有想过甚至不知道addu。因此,它迫使您寻找 "false positive" 陷阱错误的风险:add 可能在您不希望出现错误时出现错误。


我认为没有任何真正的 MIPS CPU add/addiaddu / addiu 慢。可能不会;任何 lwsw 都可能出错,具体取决于寄存器输入,因此 MIPS 流水线需要能够有效地处理可能出错的指令。

在一般情况下,他们从不犯错;您为此优化了管道,实际上承担错误的效率有多低几乎无关紧要。 (或者在 MIPS 上,软件 TLB 未命中处理可能确实很重要;这可能发生得相当频繁,比页面错误要多得多)。