你必须在 MIPS 中初始化寄存器吗
Do you have to initialize registers in MIPS
我们在用汇编语言编写函数时需要将寄存器初始化为0吗?
只是为了确保这些寄存器中没有来自以前程序的值。
一般来说,可能是。
程序启动时的注册状态取决于操作系统。例如,如果您在 OS 上 运行 支持 ELF psABI for MIPS,则寄存器 $2、$29 和 $31 在程序启动时具有有意义的值,而其他寄存器包含未指定的值 - 请参阅"Process Initialization" 部分。
您听起来可能对 程序 和 函数 之间的区别感到困惑。 psABI 中也记录了 function 可以期待的内容 - 请参阅同一文档中的 "Function Calling Sequence" 部分 - 简短版本是 $4-$7、$25、$28、 $29、$31、$f12、$f13、$f14 和 $f15 可能包含有用的值,所有其他未指定,您有义务确保在 $16-$23、$28、$29、$30 中找到的值, 和 $f20-$f31 在进入时在退出时不变(即,如果你改变它们,你必须保存旧值并在退出前恢复它们;相反,如果你自己调用一个函数,你必须假设它已经覆盖了所有other 在返回前用未指定的值注册)。
如果您使用的 OS 不 支持 ELF psABI,那么您需要为您的 OS 找到等效的文档。某处会有一些规范。不过,可以想象,您将不得不对编译器进行逆向工程才能获得它。
不,您不需要将它们归零,但您确实需要假设每个寄存器在进入您的函数/程序时都保存随机垃圾,保存输入的寄存器除外(例如函数参数)或调用约定需要具有某种有用的值(例如堆栈指针)。
通常没问题;你不需要清除随机垃圾"early"。如果您第一次使用寄存器是向其写入内容,则无需先写入一个零,然后再写入您实际想要放在那里的任何内容。
但是,如果您打算将它用作计数器(即在循环中递增它),那么是的,您需要在循环之前将它归零。将寄存器用作源操作数的任何其他用途也是如此,而不仅仅是目标操作数。
请注意,这是汇编语言中为数不多的在每种 种汇编语言中对于每种体系结构都相同的东西之一。
一些架构(如x86) have instructions with implicit inputs (like DIV), but 仍然只是在读取寄存器之前将寄存器归零的情况。
换句话说:每个寄存器总是有一个值,所以第一次使用寄存器作为只写操作数没有什么特别之处。
您只需确保您的代码的正确性不依赖于不需要具有任何特定值的任何寄存器或内存的内容。
阅读奖励:
对于性能,此规则很少有例外:在某些微体系结构中,并非所有只写操作都是相同的。有些实际上对输出的旧值具有错误依赖性! 使用廉价的依赖性破坏方式来写入寄存器可以让乱序执行避免等待它不等待的 "input"不需要,以防您的代码在长依赖链末尾可能使用该寄存器的东西之后运行(例如,涉及缓存未命中)。
我不知道有任何 MIPS 示例(希望没有),所以我将使用 x86 作为示例:
对于将结果放入向量寄存器低位元素的 int->float 指令(例如 CVTSI2SS),Intel 采用了短视的方法将其设计为合并到目标向量中,而不是将目的地的其余部分归零。第一代具有 SSE 的 CPU 是 Intel Pentium III,它只有 64 位向量执行单元。我认为将向量的高 64 位置零会花费额外的 uop,或者至少在内部需要转换指令来生成向量的两半作为输出,而不仅仅是修改后的低半部分。
int->float 转换的绝大多数用例只希望将 float 作为标量,而不是插入到另一个向量中,因此对目标寄存器的依赖可能有害。 gcc 使用 避免它在 运行 CVTSI2SS 之前将目标 XMM 寄存器归零。即使在最新的 CPU 上,这个额外的归零指令也不是完全免费的:它占用代码大小和前端带宽。因此,当两个原本独立的依赖链通过同一寄存器上的输入依赖关系链接时,每次 int->float 转换通常会产生非常小的额外成本,以避免在不可预测的情况下出现罕见但可能很大的减速。
此外,POPCNT/LZCNT/TZCNT is architecturally write-only, but Intel's implementation has a false dependency on the output register 的目标寄存器。因此,如有必要,在 运行 POPCNT 之前将目标寄存器归零实际上是有意义的,以避免意外创建循环携带的依赖链。
上 gcc 在 popcnt 和 int->float 转换之前插入额外的异或归零指令的示例
我们在用汇编语言编写函数时需要将寄存器初始化为0吗?
只是为了确保这些寄存器中没有来自以前程序的值。
一般来说,可能是。
程序启动时的注册状态取决于操作系统。例如,如果您在 OS 上 运行 支持 ELF psABI for MIPS,则寄存器 $2、$29 和 $31 在程序启动时具有有意义的值,而其他寄存器包含未指定的值 - 请参阅"Process Initialization" 部分。
您听起来可能对 程序 和 函数 之间的区别感到困惑。 psABI 中也记录了 function 可以期待的内容 - 请参阅同一文档中的 "Function Calling Sequence" 部分 - 简短版本是 $4-$7、$25、$28、 $29、$31、$f12、$f13、$f14 和 $f15 可能包含有用的值,所有其他未指定,您有义务确保在 $16-$23、$28、$29、$30 中找到的值, 和 $f20-$f31 在进入时在退出时不变(即,如果你改变它们,你必须保存旧值并在退出前恢复它们;相反,如果你自己调用一个函数,你必须假设它已经覆盖了所有other 在返回前用未指定的值注册)。
如果您使用的 OS 不 支持 ELF psABI,那么您需要为您的 OS 找到等效的文档。某处会有一些规范。不过,可以想象,您将不得不对编译器进行逆向工程才能获得它。
不,您不需要将它们归零,但您确实需要假设每个寄存器在进入您的函数/程序时都保存随机垃圾,保存输入的寄存器除外(例如函数参数)或调用约定需要具有某种有用的值(例如堆栈指针)。
通常没问题;你不需要清除随机垃圾"early"。如果您第一次使用寄存器是向其写入内容,则无需先写入一个零,然后再写入您实际想要放在那里的任何内容。
但是,如果您打算将它用作计数器(即在循环中递增它),那么是的,您需要在循环之前将它归零。将寄存器用作源操作数的任何其他用途也是如此,而不仅仅是目标操作数。
请注意,这是汇编语言中为数不多的在每种 种汇编语言中对于每种体系结构都相同的东西之一。
一些架构(如x86) have instructions with implicit inputs (like DIV), but
换句话说:每个寄存器总是有一个值,所以第一次使用寄存器作为只写操作数没有什么特别之处。
您只需确保您的代码的正确性不依赖于不需要具有任何特定值的任何寄存器或内存的内容。
阅读奖励:
对于性能,此规则很少有例外:在某些微体系结构中,并非所有只写操作都是相同的。有些实际上对输出的旧值具有错误依赖性! 使用廉价的依赖性破坏方式来写入寄存器可以让乱序执行避免等待它不等待的 "input"不需要,以防您的代码在长依赖链末尾可能使用该寄存器的东西之后运行(例如,涉及缓存未命中)。
我不知道有任何 MIPS 示例(希望没有),所以我将使用 x86 作为示例:
对于将结果放入向量寄存器低位元素的 int->float 指令(例如 CVTSI2SS),Intel 采用了短视的方法将其设计为合并到目标向量中,而不是将目的地的其余部分归零。第一代具有 SSE 的 CPU 是 Intel Pentium III,它只有 64 位向量执行单元。我认为将向量的高 64 位置零会花费额外的 uop,或者至少在内部需要转换指令来生成向量的两半作为输出,而不仅仅是修改后的低半部分。
int->float 转换的绝大多数用例只希望将 float 作为标量,而不是插入到另一个向量中,因此对目标寄存器的依赖可能有害。 gcc 使用
此外,POPCNT/LZCNT/TZCNT is architecturally write-only, but Intel's implementation has a false dependency on the output register 的目标寄存器。因此,如有必要,在 运行 POPCNT 之前将目标寄存器归零实际上是有意义的,以避免意外创建循环携带的依赖链。
上 gcc 在 popcnt 和 int->float 转换之前插入额外的异或归零指令的示例