很多人(我所包括)是反对变革的变化,即积极利用未定义的行为,但是 TL; DR:C'如果INT!=寄存器宽度,但(对于向后Compat)int是32位 主要的64位目标,这导致一些代码生成和优化的毛茸茸的问题 相当常见的病例。 签名的溢出UB利用是尝试解决此问题的尝试。 更精确:编译器真的想要摆脱的#1转换是替代int32 尽可能在64位代码中使用INT64循环计数器的循环计数器,因为INT32S是坏消息 movsxd(标记*)表示问题:We'在一个64位机器上尝试使用32位索引
请参阅阵列,因为地址为64位(我们可能绝对可以具有大于4GB的数组,但 在少于20亿个条目),我们需要在我们完成寻址之前将该索引扩展到64位 该符号扩展是在此代码段的32位版本中不存在的内部循环中的额外工作。 它也会导致其他问题。 例如,上面的代码的32位等效于始终访问内存 和ebx是什么' s称为"诱导变量" - 它在每个循环迭代中的常量值更改 (在这种情况下递增1)。 32位编译器可以实现此功能并用指针算术替换它 (每次递增4,递增4个)。 " esi" 和#34; ebx" 两个32位值是否受到32位环绕,
符号在中间延伸(那个" sext32to64(ebx)"是" movsxd rbx,edx" do)。 和在 剧烈地,(4 - 2 ^(32 + 2))。 在32位模式(所有地址模2 ^ 32),这只是增加4 照常; "环绕声" 部分是2 ^ 32的倍数,因此看不见。 在64位模式下,它是' t; 如果是 选项a)在这个特殊的例子中相对简单,但它可以得到毛茸茸的,我的观点是 32位编译器永远不需要担心任何一个开始(并且都没有执行64位编译器 如果编译器*' t *能够做这些证明,它可以变得非常糟糕。 例如,上面的循环 基本上是全部循环开销,大多数编译器都会结束展开(或矢量化,但我' ll
忽略它。 现在我们更喜欢看到一个内循环,如:(我在此显示设置或尾循环): (仅在这里展示地址零件,在实践中'其他改变我们'喜欢看,但是 如果编译器可以' t(无论出于何种原因),证明索引表达式不会溢出 这是一个愚蠢的例子,但你需要做的就是看一些装配列表 " movsxd&#34 ;; 通常你' ll能够找到实际的寻址代码,这些代码是这样的东西 无论如何,如上所述,编译器使用签名overflow UB作为自由票证来推广Int32 当" int" 是机器寄存器的宽度和漂亮
当它'没有。 较新的语言(例如,Go和Rust)通过制作解决这个问题 " int" 在64位平台上是64位,从而消除了这类的大部分动机 在冷码上延伸(并导致额外的工作)延伸(并导致额外的工作)不是*糟糕的话 在那里有一个方法,编译器告诉程序员真正的热环 通常与额外指令一样多(或更多)。 在x64上,它在 或类似的。 前32位" rbx" 下次一个32位ALU将获得零清除 op写入ebx,但是'很好。 至少我们没有使用额外的寄存器。
例如 显示溢出何时可以' t发生在循环中,例如示例循环(在此 案例:如果我们可以证明"伯爵和#34; 保持固定在整个循环中,我们只输入循环 如果" 0< 数&#34 ;,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 随着我的增加 每次迭代的速度为1,这是在我溢出之前发生的。 这是 程序员,并且真的很难理解编译器可以或不能 在任何给定的时间点证明。 所以它'棘手。 更好的分析捕获更多和 如果有疑问,它几乎总是优选的是以这种方式定义了语义
许多算术运算,无论如何都冒险。 你真的很想 在大多数机器上你'重新使用," size_t" 对于循环计数器是个好主意