【问题标题】:Can I disable checking for zero division every time the division happens?我可以在每次除法时禁用检查零除法吗?
【发布时间】:2017-03-02 00:05:00
【问题描述】:

为了更好地理解 Rust 的恐慌/异常机制,我编写了以下代码:

#![feature(libc)]

extern crate libc;

fn main() {
    let mut x: i32;
    unsafe {
      x = libc::getchar();
    }

    let y = x - 65;
    println!("{}", x);

    let z = 1 / y;
    println!("{}", z);
}

我想检查一下 Rust 如何处理除以零的情况。最初我认为它要么将一个未处理的 SIGFPE 带到脸上并死去,要么它实现了一个处理程序并将其重新路由到恐慌(现在可以处理了吗?)。

代码很冗长,因为我想确保当 Rust 在编译时知道某些东西为零时,它不会做任何“聪明”的事情,因此是用户输入。只要给它一个“A”,它就可以解决问题。

我发现 Rust 实际上生成的代码会在每次除法发生之前检查零除法。我什至看了一次组装。 :-)

长话短说:我可以禁用此行为吗?我想对于较大的数据集,这可能会对性能产生相当大的影响。为什么不使用我们的 CPU 能力为我们检测这些东西呢?我可以设置自己的信号处理程序并改为处理 SIGFPE 吗?

根据an issue on Github的说法,前一段时间的情况肯定有所不同。

我认为事先检查每个部门离“零成本”还很远。你怎么看?我错过了什么明显的东西吗?

【问题讨论】:

  • 我很困惑。如果除数为零,您要使用检查除法还是禁用检查?
  • “为什么不使用我们的 CPU 能力为我们检测这些东西?”并非所有 CPU 都具有这种能力。 x86 会引发除以零的异常,但其他则不会;例如ARM 静默返回结果 0。

标签: performance exception rust divide-by-zero


【解决方案1】:

我认为事先检查每个部门离“零成本”还很远。你怎么看?

你测量了什么?

执行的指令数量是性能的一个非常差的代理;矢量化代码通常更冗长,但速度更快。

所以真正的问题是:这个分支的成本是多少?

由于故意除以 0 的可能性很小,而且偶然做的可能性稍大一些,因此当发生除以 0 时,分支总是会被正确预测除了。但是,考虑到恐慌的代价,错误预测的分支是你最不担心的。

因此,成本为:

  • 稍微胖一点的程序集,
  • 分支预测器中的一个占用槽。

确切的影响很难确定,对于数学繁重的代码,它可能会产生影响。虽然我会提醒你,整数除法是 ~100 个周期1 开始,所以数学繁重的代码会尽可能避开它(这可能是你的最耗时的指令CPU)。

1参见Agner Fog's Instruction Table:例如,在 Intel Nehalem DIV 和 IDIV 上,64 位积分的延迟分别为 28 到 90 个周期和 37 到 100 个周期。


除此之外,rustc 是在 LLVM 之上实现的,它将实际代码生成委托给 LLVM。因此,在许多情况下,rustc 都受 LLVM 支配,这就是其中之一。

LLVM 有两个整数除法指令:udiv and sdiv

两者都具有除数为 0 的未定义行为。

Rust 旨在消除未定义的行为,因此 必须 防止发生除以 0,以免优化器将发出的代码弄得无法修复。

按照 LLVM 手册中的建议,它使用检查。

【讨论】:

  • 整数除法约为 100 个周期
  • 我用 C/C++ 和类似的场景做了一些测试,但都没有检查任何东西。也用 Clang(++) 编译。考虑到 Clang 的“接近”程度,Clang 不尊重 LLVM 的这一建议,这让我感到困惑。
  • @Lazarus535:啊,但是在 C 和 C++ 中整数除法具有未定义的行为(根据规范),因此 Clang 调用 udivsdiv 而不事先检查是“正常的”。
  • @Lazarus535 C 标准明确允许 Clang 在除以零时产生未定义的行为。另一方面,Rust 避免了安全代码中未定义的行为。如果在整数类型上提供 unsafe fn divide_unchecked(divisor: T) 方法,Rust 将保持其承诺,但它不存在,可能是由于答案中解释的原因。
  • 此成本分析遗漏了最大的成本:此检查使您的函数变得更大,因此它们不太可能被内联,由于其他丢失优化的机会成本,这可能是任意昂贵的。分支预测表中的条目数量也是有限的,如果系统地程序中的每个部门都在生成这个额外的代码,那么它更有可能产生重大影响。 Rust 已经使用 SIGSEGV 处理程序进行零成本堆栈溢出检测,为什么不使用 SIGFPE 进行零成本除以零检测?
【解决方案2】:

长话短说:我可以禁用此行为吗?

是的,您可以:std::intrinsics::unchecked_div(a, b)。 您的问题也适用于余数(这就是 Rust 调用 modulo 的方式):std::intrinsics::unchecked_rem(a, b)。 我检查了程序集输出 here 以将其与 C++ 进行比较。

在文档中指出:

这是一个仅限夜间的实验性 API。 (core_intrinsics)

内在函数不可能永远稳定,相反,它们应该通过标准库其余部分中的稳定接口使用

因此您必须使用每晚构建,并且由于Matthieu M. 已经指出的原因,它不太可能以稳定的形式进入标准库。

【讨论】:

  • 感谢您提供这个非常有趣的答案!艰难的决定,但我现在会接受。有异议吗?
  • @Lazarus535 我想你问了两个问题:“长话短说:我可以禁用此行为吗?”和“我认为事先检查每个部门还很遥远从“零成本”。你怎么看?我错过了一些明显的东西吗?“。我回答了第一个问题,Matthieu M. 回答了第二个问题。不过我没有反对意见:)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-29
  • 2014-04-07
  • 1970-01-01
  • 2020-05-22
相关资源
最近更新 更多