Rust: 整数转换的浮点可能会导致未定义的行为

创建于 2013-10-31  ·  234评论  ·  资料来源: rust-lang/rust

截至2020-04-18的状态

我们打算稳定as的Saturating-float-casts行为,并稳定处理先前行为的不安全库函数。 有关该稳定过程的最新讨论,请参见#71269。

状态截至2018-11-05

一个标志已在编译器中实现, -Zsaturating-float-casts ,这将导致所有浮点数到整数类型转换都具有“饱和”行为,如果超出范围,则将其限制在最近的范围内。 不久前就呼吁对此变化某些项目中却是

下一步是弄清楚如何在这些情况下恢复性能:

  • 一种选择是采取今天的as强制转换行为(在某些情况下为UB),并为相关类型等添加unsafe函数。
  • 另一个是等待LLVM添加freeze概念,这意味着我们得到了垃圾位模式,但至少不是UB
  • 另一个方法是通过LLVM IR中的内联汇编来实现强制转换,因为当前的代码生成器尚未得到优化。

旧状态

更新(由@nikomatsakis提供):经过大量讨论,我们对如何解决此问题有了初步的计划。 但是,在实际调查性能影响并确定最终细节时,我们需要一些帮助!


原始问题如下:

如果该值不能适合ty2,则结果不确定。

1.04E+17 as u8
A-LLVM C-bug I-unsound 💥 P-medium T-lang

最有用的评论

我已经开始进行一些工作以实现将LLVM中的float转换为int强制转换的内在函数: https :

如果任何地方都能实现,它将提供一种开销较低的方式来获取饱和的语义。

所有234条评论

提名

接受P高电平,与#10183相同

我不认为这在语言级别上是向后不兼容的。 这不会导致正常工作的代码停止工作。 提名。

更改为P高,与#10183相同

我们建议如何解决此问题和#10185? 由于是否定义行为取决于要转换的数字的动态值,因此似乎唯一的解决方案是插入动态检查。 我们似乎同意,我们不想为算术溢出而这么做,我们是否愿意为强制转换溢出而这么做?

我们可以向LLVM添加执行“安全转换”的内在函数。 @zwarich可能还有其他想法。

AFAIK目前唯一的解决方案是使用特定于目标的内在函数。 至少根据我所问的人,这就是JavaScriptCore所做的。

哦,那很容易。

ping @pnkfelix这是新的溢出检查内容所涵盖的吗?

rustc不会通过调试断言检查这些强制类型转换。

我很高兴解决这个问题,但是我需要一个具体的解决方案。 我个人认为应该与溢出整数运算一起检查,因为这是一个非常相似的问题。 我真的不介意我们做什么。

请注意,当在某些常量表达式中使用此问题时,当前会导致ICE。

这会违反安全锈蚀中的内存安全性,例如此论坛帖子中的示例:

Undefs,是吗? Undef很有趣。 它们倾向于传播。 经过几分钟的争吵..

#[inline(never)]
pub fn f(ary: &[u8; 5]) -> &[u8] {
    let idx = 1e100f64 as usize;
    &ary[idx..]
}

fn main() {
    println!("{}", f(&[1; 5])[0xdeadbeef]);
}

在我的系统上使用-O进行段错误(最近夜间)。

标记I-unsound会破坏安全防锈中的内存安全性。

@bluss ,对我来说这不是正确的,只是给出一个断言错误。 取消标记,因为我是添加它的人

igh,我忘了-O,重新标记。

重新提名为P高。 显然,这在某个时候为P高,但随着时间的推移逐渐降低。 对于正确性来说,这似乎非常重要。

编辑:对分类检阅没有反应,手动添加标签。

似乎来自溢出内容(例如,转移)的先例是只是确定一些行为。 Java似乎会以范围为模的结果产生结果,这似乎并非不合理。 我不确定要处理哪种LLVM代码。

根据https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls -5.1.3 Java还保证将NaN值映射到0和无穷大到最小/最大可表示整数。 此外,用于转换的Java规则比仅包装要复杂得多,它可以是饱和(用于转换为intlong )和包装(用于转换为较小的整数类型)的组合, 如果需要的话)。 从Java复制整个转换算法当然是可能的,但是每次转换都需要大量的操作。 特别是,为了确保LLVM中fpto[us]i操作的结果不会表现出未定义的行为,需要进行范围检查。

或者,我建议仅在原始值的截断可以表示为目标类型的值(或可能表示为[iu]size ?)时,才保证float-> int强制转换有效。对调试版本具有断言,当未如实表示该值时会触发恐慌。

Java方法的主要优点是转换功能是完整的,但这也意味着可能会出现意想不到的行为:它将防止未定义的行为,但是很容易被诱使不检查强制转换是否真正有意义(不幸的是,对于其他演员:worried:也是如此。)

另一种方法与当前用于算术运算的方法相匹配:在发行版中简单高效地实现,在调试中通过范围检查触发恐慌。 不幸的是,与其他as强制转换不同,这将检查这种转换,这对于用户来说可能是令人惊讶的(尽管也许算术运算的类比在此可以有所帮助)。 这也将破坏一些代码,但是AFAICT仅应在当前依赖于未定义行为的代码中发生(即它将替换未定义行为“让我们返回任何整数,您显然不在乎哪个”)。

问题不在于“让我们返回任何整数,您显然不在乎哪一个”,而是它导致了一个undef,它不是一个随机值,而是一个鼻恶魔值,并且允许LLVM假定该undef永远不会发生启用可做可怕的错误事情的优化。 如果它是一个随机值,但关键不是undef,那么就足以解决稳健性问题。 我们不需要定义如何表示无法表示的值,我们只需要防止undef。

在@ rust-lang / compiler会议上讨论。 最一致的行动方针仍然是:

  1. 启用溢出检查后,请检查是否有非法的强制转换和紧急情况。
  2. 否则,我们需要一个后备行为,对于有效值,它应该具有最小的运行时开销(理想情况下为零),但是精确的行为并不那么重要,只要它不是LLVM undef。

主要问题是我们需要对选项2提出具体建议。

分流:P-中

@nikomatsakis as当前是否在调试版本中出现恐慌? 如果不是这样,则为了保持一致性和可预测性,最好保持这种方式。 (我认为它应该有,就像算术一样,但这是一个单独的且过去的辩论。)

否则,我们需要一个后备行为,对于有效值,它应该具有最小的运行时开销(理想情况下为零),但是精确的行为并不那么重要,只要它不是LLVM undef。

具体建议:提取数字和指数为u64并按指数提取位移数字。

fn f64_as_u64(f: f64) -> u64 {
    let (mantissa, exponent, _sign) = f.integer_decode();
    mantissa >> ((-exponent) & 63)
}

是的,它不是零成本,但是它是可以优化的(如果我们将integer_decode标记为inline会更好),并且至少是确定性的。 将来的扩展了float-> int强制转换的MIR传递可能会分析是否可以保证可以浮动并跳过此繁重的转换。

LLVM是否没有转换函数的平台内在函数?

编辑@zwarich说(很久以前):

AFAIK目前唯一的解决方案是使用特定于目标的内在函数。 至少根据我所问的人,这就是JavaScriptCore所做的。

为什么还要慌张呢? AFAIK, @ glaebhoerl是正确的, as应该截断/扩展,_not_检查操作数。

GáborLehel在2016年3月5日(星期六)03:47:55 AM -0800上写道:

@nikomatsakis as当前是否在调试版本中出现恐慌? 如果不是这样,则为了保持一致性和可预测性,最好保持这种方式。 (我认为它应该有,就像算术一样,但这是一个单独的且过去的辩论。)

真正。 我觉得很有说服力。

2016年3月9日,星期三,00-08:02:31:05,Eduard-Mihai Burtescu写道:

LLVM是否没有转换函数的平台内在函数?

编辑

AFAIK目前唯一的解决方案是使用特定于目标的内在函数。 至少根据我所问的人,这就是JavaScriptCore所做的。

为什么还要慌张呢? AFAIK, @ glaebhoerl是正确的, as应该截断/扩展,_not_检查操作数。

是的,我想我以前弄错了。 as是“未经检查的截断”
运算符,无论是好是坏,似乎最好保持一致
那种哲学。 使用特定于目标的内在函数可能是一个完美的选择
好办法了吗?

@nikomatsakis :似乎尚未定义行为? 您能提供有关该计划的最新信息吗?

只是遇到了小得多的数字

    let x: f64 = -1.0;
    x as u8

结果为0、16等,具体取决于优化,我希望将其定义为255,因此不必编写x as i16 as u8

@gmorenz您尝试过!0u8吗?

在没有意义的情况下,我正在通过对通过网络发送的数据进行转换来获取f64,范围为[-255,255]。 我希望它能很好地包裹(以<i32> as u8包裹的确切方式)。

这是最近的LLVM建议,以“杀死undef” http://lists.llvm.org/pipermail/llvm-dev/2016-October/106182.html ,尽管我几乎不了解该知识是否会自动解决这个问题。

他们用毒药代替了undef,其语义略有不同。 它不会使int-> float强制转换定义的行为。

我们可能应该提供一些明确的方法来进行饱和投射? 我刚刚想要那种确切的行为。

给定https://github.com/rust-lang/rust/issues/10184#issuecomment -139858153,似乎应该将此类事件标记为I-crash。

我们今天在#rust-beginners对此有疑问,有人在野外遇到了它。

我正在使用@jimblandy _Programming Rust_编写的书中提到了此错误。

允许使用多种类型的转换。

  • 数字可以从任何内置数字类型转换为其他类型。

    (...)

    但是,在撰写本文时,将大浮点值转换为太小而无法表示它的整数类型可能会导致不确定的行为。 即使在安全的Rust中也可能导致崩溃。 这是编译器github.com/rust-lang/rust/issues/10184中的错误。

我们本章的最后期限是5月19日。我想删除最后一段,但是我觉得我们至少应该首先在这里制定某种计划。

显然,当前的JavaScriptCore在x86上

老实说,我认为我们应该使用as弃用数字类型转换,并使用FromTryFrom或类似conv crate的东西代替。

也许是这样,但这似乎与我正交。

好的,我只是重新阅读了整个对话。 我认为已经达成共识,该操作不应惊慌(与as保持一致)。 对于行为应该有两个主要的竞争者:

  • 某种定义的结果

    • 赞成:我认为这与到目前为止的总体哲学思想是最大的一致

    • 缺点:在这种情况下,似乎没有一种真正可移植的方式来产生任何特定的定义结果。 这意味着我们将使用特定于平台的内部函数,并对超出范围的值进行某种回退(例如,回退到饱和度@ oli-obk提出的该函数, Java的定义任何“有毛的C ++” JSC使用

    • 最糟糕的是,我们只能为“超出范围”的情况插入一些if。

  • 未定义的值(不是未定义的行为)

    • Pro:这让我们仅使用每个平台上可用的特定于平台的内在函数。

    • 缺点:这是可移植性危害。 总的来说,我觉得我们并没有经常使用未定义的结果,至少在语言方面(我确信我们在不同地方的lib中都使用过)。

我不清楚在第一种情况下应该有什么样的明确结果的先例吗?

在写完这些之后,我倾向于保持确定性的结果。 我觉得我们能够坚持到底的每个地方都是胜利。 我不太确定结果应该是什么

我喜欢饱和度,因为我可以理解它,它似乎很有用,但是它似乎在某种程度上与u64 as u32截断的方式不一致。 因此,也许某种基于截断的结果是有意义的,我想这可能是@ oli-obk提出的内容-我不完全了解该代码的意图。 =)

我的代码为0..2 ^ 64范围内的事物提供正确的值,并为其他所有事物提供确定性但虚假的值。

浮点数由尾数^指数表示,例如1.0(2 << 52) ^ -52 ,由于位移位和指数在二进制中是相同的,所以我们可以反转移位(因此对指数和右数取负转移)。

+1为确定性。

我看到了两种对人类有意义的语义,并且我认为当编译器无法优化任何计算时,对于范围内的值,我们应该选择速度更快的一种。 (当编译器知道某个值在范围内时,这两个选项将给出相同的结果,因此它们同样可以优化。)

  • 饱和度(超出范围的值变成IntType::max_value() / min_value()
  • 模数(超出范围的值被视为先转换为bigint然后截断)

下表旨在完全指定这两个选项。 T是任何机器整数类型。 Tmin和Tmax是T::min_value()T::max_value() 。 RTZ(v)表示取v的数学值和向零舍入取整数。

v | v as T (饱和度)| v as T (取模)
---- | ---- | ----
在范围内(Tmin <= v <= Tmax)| RTZ(v)| RTZ(v)
负零| 0 | 0
NaN | 0 | 0
无限最高温度 0
-无穷大| Tmin | 0
v> Tmax | 最高温度 RTZ(v)截短以适合T
v <Tmin | Tmin | RTZ(v)截短以适合T

ECMAScript标准指定操作ToInt32 ,ToUint32,ToInt16,ToUint16,ToInt8,ToUint8,我的意图是使用上面的“ modulo”选项在每种情况下都匹配这些操作。

ECMAScript还指定了与以上两种情况都不匹配的ToInt8Clamp :对分数值进行“四舍五入到偶数”取整,而不是“四舍五入”。

@ oli-obk的建议是第三种方法,对于范围内的值,值得考虑是否计算速度更快。

@ oli-obk有符号整数类型呢?

向组合中提出另一个建议:将u128标为不安全的浮标,迫使人们明确选择一种处理方式。 u128目前非常罕见。

@Manishearth我希望有类似的语义整数→浮点数,就像浮点数→整数一样。 因为两者都是UB-ful,并且我们不能再使float→integer不安全,所以我们也应该避免使integer→float不安全。

对于float→integer,饱和将更快地实现AFAICT(导致and的序列, test + jump float比较和跳转,在现代弓上均为0.66或0.5 2-3个周期)。 只要范围内的值尽可能快,我个人就不会在乎我们决定的确切行为。

使其表现得像溢出一样有意义吗? 因此,在调试版本中,如果您使用未定义的行为进行强制转换,则会感到恐慌。 然后,您可以使用一些方法来指定强制转换行为,例如1.04E+17.saturating_cast::<u8>()unsafe { 1.04E+17.unsafe_cast::<u8>() }以及其他可能的方法。

哦,我认为问题只在于u128,我们可以通过两种方式使这种情况不安全。

@cryze UB甚至在释放模式下也不应以安全代码存在。 溢出的东西仍然是定义的行为。

也就是说,对调试感到恐慌,并且发布会很棒。

这会影响:

  • f32 -> u8, u16, u32, u64, u128, usize-1f32 as _所有, f32::MAX as _为所有,但U128)
  • f32 -> i8, i16, i32, i64, i128, isize (全部f32::MAX as _
  • f64 ->所有整数(全部f64::MAX as _

f32::INFINITY as u128也是UB

@CryZe

使其表现得像溢出一样有意义吗? 因此,在调试版本中,如果您使用未定义的行为进行强制转换,则会感到恐慌。

这是我最初的想法,但是提醒我, as转换目前从不慌张(无论好坏,我们都不会对as进行溢出检查)。 因此,最类似的事情是“执行定义的操作”。

FWIW实际上是“ kill undef”,它提供了一种解决内存不安全的方法,但结果却不确定。 关键组件之一是:

3)创建一条新指令'%y = Frozen%x',该指令将停止传播
毒。 如果输入是有毒的,则返回任意但固定的,
值。 (就像旧的undef,但每次使用都具有相同的值),否则
只返回其输入值。

今天,可以使用undef违反内存安全性的原因是,它们可以在使用之间神奇地更改值:特别是在边界检查和后续指针算术之间。 如果rustc在每次危险的转换后添加了冻结,您将获得未知的但行为良好的值。 在性能方面,冻结基本上是免费的,因为与转换对应的机器指令当然会产生一个单一的值,而不是一个波动的值。 即使优化器出于某种原因想要复制强制转换指令,这样做也应该是安全的,因为超出范围的输入的结果通常在给定的体系结构上是确定的。

...但是,如果有人想知道的话,就无法确定整个体系结构的确定性。 对于所有错误的输入,x86返回0x80000000; 对于超出范围的输入,ARM会饱和,并且(如果我正在正确阅读此伪代码)对于NaN返回0。 因此,如果目标是产生确定性且与平台无关的结果,仅使用平台的fp-to-int内部函数是不够的; 至少在ARM上,您还需要检查状态寄存器是否存在异常。 这本身可能会有一些开销,并且在万一尚未使用内部函数的情况下,肯定会阻止自动矢量化。 或者,我猜您可以使用常规的比较操作显式测试范围内的值,然后使用常规的float-to-int。 在优化器上听起来好多了……

as转化目前从未出现过恐慌

在某些时候,我们将+更改为panic(在调试模式下)。 在以前是UB的情况下,看到as恐慌我不会感到震惊。

如果我们关心检查(应该这样做),那么我们应该弃用as (是否有唯一的好选择的用例?)或至少建议不要使用它,并将人们转移到类似取而代之的是TryFromTryInto ,这是我们说的,当我们决定按原样保留as时,我们打算这样做。 我不认为讨论的案件性质不同,在抽象,从那里的案件as已定义没有做任何检查。 所不同的只是,在实践中,这些案例的实施目前尚不完整,并且具有UB。 一个不能依赖as进行检查的世界(因为对于大多数类型,它不是),并且您不能依赖它不惊慌(由于某些类型,它会恐慌),并且这并不一致,而且我们仍未弃用,这对我来说似乎是最糟糕的。

因此,我认为在这一点上, @ jorendorff基本上列举了我认为是最好的计划

  • as将具有确定性行为;
  • 我们将根据行为的明智程度和效率来选择行为

他列举了三种可能性。 我认为剩下的工作是研究这些可能性-或至少研究其中一种。 也就是说,实际实施它,并尝试对它的“缓慢”或“快速”感觉有所了解。

有没有人愿意为此采取行动? 我将其标记为E-help-wanted ,希望吸引一些人。 (@ oli-obk?)

呃,我宁愿不为跨平台的一致性付出代价:/这是垃圾回收,我不在乎什么垃圾会消散(但是调试断言会非常有帮助)。

当前,Rust中的所有舍入/截断函数都非常慢(函数调用使用了非常精确的实现),因此as是我最后使用的快速float舍入方法。

如果您要使as比裸cvttss2si ,还请添加一个稳定的替代方法。

@pornel这不只是理论上的UB,如果您忽略它是ub,那么一切还可以,它具有现实意义。 我从一个真实的代码示例中提取了#41799。

@ est31我同意将其保留为UB是错误的,但是我已经看到建议将freeze作为UB的解决方案。 AFAIK使其成为已定义的确定性值,您只是不必说出哪个。 这种行为对我来说很好。

所以,我会被罚款,如果如u128::MAX as f32确定性产生17.5在x86和999.0上的x86-64和-555上ARM。

freeze不会产生定义的,确定性的,未指定的值。 其结果仍然是“编译器喜欢的任何位模式”,并且仅在相同操作的使用之间保持一致。 这可能会避开人们在上面收集的产生UB的示例,但不会给出以下信息:

u128 :: MAX as f32确定在x86上产生17.5,在x86-64上产生999.0,在ARM上产生-555。

例如,如果LLVM注意到u128::MAX as f32溢出并用freeze poison替换,则在x86_64上有效降低fn foo() -> f32 { u128::MAX as f32 }可能是这样的:

foo:
  ret

(也就是说,只返回最后存储在返回寄存器中的内容)

我懂了。 对于我的用途,这仍然可以接受(对于我期望超出范围的值,我会事先进行钳位。如果我期望在范围内的值,但它们不是,那么无论如何我都不会得到正确的结果) 。

我对超出范围的float强制转换返回任意值没有问题,只要这些值被冻结即可,这样就不会导致进一步的未定义行为。

LLVM上是否有类似freeze东西? 我认为这纯粹是理论上的建构。

@nikomatsakis我从未见过像这样使用过(不同于poison )-这是计划中的毒药/ undef改造。

今天,LLVM中根本不存在freeze 。 仅提出了建议(此PLDI论文是独立的版本,但在邮件列表中也进行了很多讨论)。 该提议似乎有很大的支持,但当然不能保证一定会被采纳,更不用说及时采纳了。 (从指针类型中删除pointee类型已被接受了多年,但仍然没有完成。)

我们是否要开放RFC,以便就此处提出的更改进行更广泛的讨论? IMO,任何可能影响as性能的事物都会引起争议,但如果我们不给人们提供机会表达自己的声音,这将是双重争议。

我是Julia开发人员,并且一直关注此问题已有一段时间,因为我们共享相同的LLVM后端,因此也存在类似的问题。 如果有兴趣的话,这就是我们已经确定的内容(带有我机器上单个功能的大概计时):

  • unsafe_trunc(Int64, x)直接映射到相应的LLVM内部fptosi (1.5 ns)
  • trunc(Int64, x)引发超出范围值的异常(3 ns)
  • convert(Int64, x)引发范围外或非整数值(6 ns)的异常

另外,我在邮件列表中询问有关使未定义行为更加定义的问题,但是没有收到非常有希望的答复。

@bstrie我对RFC表示满意,但我认为拥有数据绝对有用! 但是,@ simonbyrne的评论在这方面很有帮助。

我玩弄了JS语义(提到了@jorendorff模)和Java语义,它们似乎是“饱和”列。 如果这些链接过期了,那就是JSJava

我还想出了一个快速实现Rust的饱和度的方法,我认为该方法是正确的。 并获得一些基准数字。 有趣的是,我看到饱和的实现比内部实现慢2-3倍,这与@simonbyrne发现的慢只有2倍。

我不确定如何在Rust中实现“ mod”语义。

但是,对我而言,似乎很显然,我们需要大量的f32::as_u32_unchecked()方法,而对于那些需要性能的人来说,则是这样。

似乎很明显,我们将需要大量的f32::as_u32_unchecked()方法,例如那些需要性能的人。

那真是令人um目结舌-还是您说的是安全的但实现定义的变体?

定义为快速默认的实现没有选项吗?

@eddyb我在想,我们在f32 unsafe fn as_u32_unchecked(self) -> u32上只有as是直接类似的。

我当然不会断言我编写的Rust实现是最佳的,但是我的印象是,在大多数情况下,在阅读此线程时,确定性和安全性比速度更重要。 unsafe逃生舱口适用于栅栏另一侧的人。

因此,没有廉价的平台相关变体吗? 我想要的是快速的东西,超出界限时给出未指定的值并且安全。 我不希望UB提供一些投入,如果我们可以做得更好,

据我所知,大多数如果不是所有的平台来实现这种转换的正规途径做一些事情,以超出范围的输入的不是UB。 但是LLVM似乎没有任何方法可以在UB上选择该选项(无论它可能是什么)。 如果我们可以说服LLVM开发人员引入一个在超出范围的输入上产生“未指定但不是undef / poison ”结果的内在函数,则可以使用该内在函数。

但是我估计该线程中的某个人将不得不写一个令人信服的RFC(在llvm-dev列表上),买入并实现它(在我们关心的后端,以及对其他实现的后备实现)目标)。 比说服llvm-dev使现有的强制转换不是UB容易得多(因为它回避了诸如“这会使所有C和C ++程序变慢”之类的问题)的麻烦,但仍然不是很容易。

以防万一,您可以在以下选项中进行选择:

饱和度(超出范围的值变为IntType :: max_value()/ min_value())
模数(超出范围的值被视为先转换为bigint然后截断)

IMO仅饱和在这里才有意义,因为浮点的绝对精度会随着值变大而迅速下降,因此在某些点上,模将是无用的,就像所有零一样。

我将其标记为E-needs-mentor ,并用WG-compiler-middle标记了它,因为在隐含期间可能是进一步研究此问题的好时机! 我在记录计划上的现有记录,那就太好了!

@nikomatsakis

IIRC LLVM计划最终实现freeze ,这应该允许我们通过执行freeze来处理UB。

到目前为止我的结果: https :

_array变量运行1024个值的循环。
_cast: x as i32
_clip:x.min(MAX).max(MIN)as i32
_panic:如果x超出范围,则会发生恐慌
_zero:如果超出范围,则将结果设置为零

test bench_array_cast       ... bench:       1,840 ns/iter (+/- 37)
test bench_array_cast_clip  ... bench:       2,657 ns/iter (+/- 13)
test bench_array_cast_panic ... bench:       2,397 ns/iter (+/- 20)
test bench_array_cast_zero  ... bench:       2,671 ns/iter (+/- 19)
test bench_cast             ... bench:           2 ns/iter (+/- 0)
test bench_cast_clip        ... bench:           2 ns/iter (+/- 0)
test bench_cast_panic       ... bench:           2 ns/iter (+/- 0)
test bench_cast_zero        ... bench:           2 ns/iter (+/- 0)

也许您不需要为单个操作将结果四舍五入为整数。 显然,这2 ns / iter后面必须有一些区别。 还是真的像这样,所有4个变体_exactly_ 2 ns?

@ sp-1234我想知道它是否已部分优化。

@ sp-1234测量太快了。 非数组基准基本上没有用。
如果通过#[inline(never)]强制单值函数成为函数,则得到2ns vs 3ns。

@ arielb1
我对freeze有所保留。 如果我正确理解,冻结的undef仍然可以包含任意值,只是在使用之间不会改变。 实际上,编译器可能会重用寄存器或堆栈插槽。

但是,这意味着我们现在可以从安全代码中读取未初始化的内存。 这可能会导致泄露秘密数据,有点像Heartbleed。 从Rust的角度来看,是否真的将其视为UB尚有待商but,但这显然是不可取的。

我在本地运行@ s3bk的基准测试。 我可以确认标量版本已完全优化,并且数组变量的asm看起来也可疑地进行了优化:例如,循环是矢量化的,这很好,但是很难将性能推算到标量代码上。

不幸的是,向black_box发送垃圾邮件似乎无济于事。 我确实看到asm做了有用的工作,但是运行基准仍然始终为标量基准提供0ns( cast_zero除外,后者显示1ns)。 我看到@alexcrichton在基准测试中执行了100次比较,因此我采用了相同的技巧。 我现在看到这些数字(源代码):

test bench_cast             ... bench:          53 ns/iter (+/- 0)
test bench_cast_clip        ... bench:         164 ns/iter (+/- 1)
test bench_cast_panic       ... bench:         172 ns/iter (+/- 2)
test bench_cast_zero        ... bench:         100 ns/iter (+/- 0)

阵列基准测试差异太大,我无法相信它们。 好吧,说实话,我还是对test基准测试基础设施表示怀疑,尤其是在看到上述数字与我之前获得的平坦0ns相比之后。 此外,即使black_box(x); 100次迭代(作为基准)也需要34ns,这使得更难于可靠地解释这些数字。

有两点值得注意:

  • 尽管没有专门处理NaN(返回-inf而不是0?),但cast_clip实现似乎比@alexcrichton的饱和as具有大致相同的时间)
  • @ s3bk的数组结果不同,我看到cast_panic比其他选中的强制转换慢。 我还看到阵列基准测试的速度甚至更大。 也许这些事情高度依赖于微体系结构的细节和/或优化者的心情?

记录下来,我在轻负载下在i7-6700K上以rustc 1.21.0-nightly(d692a91fa 2017-08-04), -C opt-level=3进行了测量。


总而言之,我得出的结论是,到目前为止还没有可靠的数据,而且很难获得更可靠的数据。 此外,我强烈怀疑任何实际应用程序都将其挂钟时间的1%花费在此操作上。 因此,我建议通过在rustc中实现饱和的as强制类型转换,在-Z标志的后面,然后运行带有和不带有此标志的一些非人工基准以确定对现实的影响来向前迈进。应用程序。

编辑:如果可能的话,我还建议在各种体系结构(例如,包括ARM)和微体系结构上运行此类基准测试。

我承认我对锈不太熟悉,但是我认为这行是不正确的: std::i32::MAX (2 ^ 31-1)不能完全表示为Float32,因此std::i32::MAX as f32将是四舍五入到最接近的可表示值(2 ^ 31)。 如果将此值用作参数x ,则结果在技术上是不确定的。 用严格的不等式代替应该可以解决这种情况。

是的,我们之前在Servo中确实遇到了这个问题。 最终的解决方案是将铸模铸造到f64,然后夹紧。

还有其他解决方案,但是它们非常棘手,并且rust并没有公开很好的API来处理这些问题。

使用0x7FFF_FF80i32作为上限和-0x8000_0000i32应该解决此问题而不转换为f64。
编辑:使用正确的值。

我认为您的意思是0x7fff_ff80 ,但是仅使用严格的不等式可能会使代码的意图更清晰。

x < 0x8000_0000u32 as f32 ? 那可能是个好主意。

我认为所有建议的确定性选择中,夹紧通常是最有用的一种,因为我认为它经常会执行。 如果实际上将转换类型记录为饱和,则无需手动夹紧。

我只是有点担心建议的实现,因为它无法正确转换为机器指令,并且严重依赖于分支。 分支使性能取决于特定的数据模式。 在上面给出的测试用例中,由于总是采用相同的分支,并且处理器具有来自许多先前循环迭代的良好分支预测数据,因此一切看起来(比较)起来都很快。 现实世界可能看起来不会像那样。 另外,分支会损害编译器矢量化代码的能力。 我不同意@rkruppe的意见,即不应同时将操作与矢量化结合进行测试。 向量化在高性能代码中很重要,并且能够向量化常见体系结构上的简单强制转换应该是至关重要的要求。

出于上述原因,我试用了@alexcrichton的带有饱和语义和@simonbyrne的修订的无分支和面向数据流的替代版本。 我为u16i16i32实现了它,因为它们都必须涵盖略有不同的情况,从而导致性能变化。

结果:

test i16_bench_array_cast       ... bench:          99 ns/iter (+/- 2)
test i16_bench_array_cast_clip  ... bench:         197 ns/iter (+/- 3)
test i16_bench_array_cast_clip2 ... bench:         113 ns/iter (+/- 3)
test i16_bench_cast             ... bench:          76 ns/iter (+/- 1)
test i16_bench_cast_clip        ... bench:         218 ns/iter (+/- 25)
test i16_bench_cast_clip2       ... bench:         148 ns/iter (+/- 4)
test i16_bench_rng_cast         ... bench:       1,181 ns/iter (+/- 17)
test i16_bench_rng_cast_clip    ... bench:       1,952 ns/iter (+/- 27)
test i16_bench_rng_cast_clip2   ... bench:       1,287 ns/iter (+/- 19)

test i32_bench_array_cast       ... bench:         114 ns/iter (+/- 1)
test i32_bench_array_cast_clip  ... bench:         200 ns/iter (+/- 3)
test i32_bench_array_cast_clip2 ... bench:         128 ns/iter (+/- 3)
test i32_bench_cast             ... bench:          74 ns/iter (+/- 1)
test i32_bench_cast_clip        ... bench:         168 ns/iter (+/- 3)
test i32_bench_cast_clip2       ... bench:         189 ns/iter (+/- 3)
test i32_bench_rng_cast         ... bench:       1,184 ns/iter (+/- 13)
test i32_bench_rng_cast_clip    ... bench:       2,398 ns/iter (+/- 41)
test i32_bench_rng_cast_clip2   ... bench:       1,349 ns/iter (+/- 19)

test u16_bench_array_cast       ... bench:          99 ns/iter (+/- 1)
test u16_bench_array_cast_clip  ... bench:         136 ns/iter (+/- 3)
test u16_bench_array_cast_clip2 ... bench:         105 ns/iter (+/- 3)
test u16_bench_cast             ... bench:          76 ns/iter (+/- 2)
test u16_bench_cast_clip        ... bench:         184 ns/iter (+/- 7)
test u16_bench_cast_clip2       ... bench:         110 ns/iter (+/- 0)
test u16_bench_rng_cast         ... bench:       1,178 ns/iter (+/- 22)
test u16_bench_rng_cast_clip    ... bench:       1,336 ns/iter (+/- 26)
test u16_bench_rng_cast_clip2   ... bench:       1,207 ns/iter (+/- 21)

该测试在每晚的Intel Haswell i5-4570 CPU和Rust 1.22.0上运行。
clip2是新的无分支实现。 它在所有2 ^ 32个可能的f32输入值上都同意clip

对于rng基准,使用的随机输入值经常会遇到不同的情况。 这将揭示分支预测失败时发生的_extreme_性能成本(大约是正常成本的10倍!!!)。 我认为考虑这一点非常重要。 这也不是现实世界中的平均表现,但这仍然是可能的,一些应用程序将达到这一目标。 人们期望f32演员表具有一致的性能。

在x86上进行明显比较: https ://godbolt.org/g/AhdF71
无分支版本很好地映射到minss / maxss指令。

不幸的是,我无法让Rustbol从Godbolt生成ARM程序集,但是这是ARM与Clang的方法的比较: https ://godbolt.org/g/s7ronw
在无法测试代码并且不了解ARM的情况下:代码大小似乎也较小,并且LLVM主要生成vmax / vmin,这看起来很有希望。 也许最终可以教LLVM将大多数代码折叠成一条指令?

@ActuallyaDeviloper asm和基准测试结果看起来非常好! 此外,像您这样的无分支代码可能比其他解决方案的嵌套条件更容易在rustc生成(为记录起见,我假设我们要生成内联IR而不是调用lang item函数)。 非常感谢您撰写本文。

我对u16_cast_clip2有疑问:它似乎无法处理NaN ?! 有一条关于NaN的评论,但我相信该函数将通过未修改的NaN传递并尝试将其强制转换为f32 (即使没有,它也会产生一个边界值而不是0 )。

PS:明确地说,我并不是要暗示是否可以对演员表进行矢量化并不重要。 如果周围的代码可以向量化,则显然很重要。 但是标量性能很重要,因为矢量化通常不适用,我评论的基准并没有对标量性能做任何陈述。 出于兴趣,您是否检查了*array*基准测试的组件,以查看它们是否仍随您的实现进行矢量化?

@rkruppe你是对的,我不小心换了if的侧面,忘了这一点。 f32 as u16恰好通过截去较高的0x8000来做正确的事情,因此测试也没有抓住它。 我现在通过再次交换分支并这次用if (y.is_nan()) { panic!("NaN"); }测试所有方法来解决问题。

我更新了我以前的帖子。 x86代码根本没有明显改变,但是不幸的是,由于某种原因,该改变阻止了LLVM在u16 ARM情况下生成vmax 。 我认为这与有关该ARM指令的NaN处理的一些细节有关,或者可能是LLVM的限制。

对于它为什么起作用,请注意,无符号值的下边界值实际上为0。 因此可以同时捕获NaN和下限。

数组版本已向量化。
Godbolt: https ://godbolt.org/g/HnmsSV

回复: ARM组件,我相信不再使用vmax的原因是,如果任一操作数为NaNvmovgt ,指的是较早的vcmp为0)。

对于它为什么起作用,请注意,无符号值的下边界值实际上为0。 因此可以同时捕获NaN和下限。

哦,对。 真好

我建议通过在-c标志后面的rustc中实现饱和转换来向前推进

我已经实现了这一点,并且在我也修复了#41799并进行了更多测试之后,将提交PR。

45134指出了我错过的代码路径(LLVM常量表达式的生成–这与rustc自己的常量评估是分开的)。 我将针对同一PR修复此问题,但需要花费一些时间。

@rkruppe您应该与@ oli-obk协调,以便miri可以进行相同的更改。

拉取请求已启动:#45205

45205已被合并,因此任何人现在(从第二天晚上开始)可以通过RUSTFLAGS传递-Z saturating-float-casts衡量饱和对性能的影响。 [1]这样的测量对于决定如何进行此问题非常有价值。

[1]严格来说,这不会影响标准库的非泛型,非#[inline]部分,因此,要100%准确,您要在本地使用Xargo构建std。 但是,我不认为会有很多代码受此影响(例如,各种转换特征显示为#[inline] )。

@rkruppe我建议以与https://internals.rust-lang.org/t/help-us-benchmark-incremental-compilation/6153/相同的方式启动一个内部/用户页面来收集数据。将人们链接到该链接,而不是在我们的问题跟踪器中添加一些随机评论)

@rkruppe,您应该创建一个跟踪问题。 该讨论已经分为两个问题。 这不好!

@Gankro是的,我同意,但是可能要等几天后才能找到适当的时间写这篇文章,因此我认为我希望在此期间征集对此问题的人们的反馈。

@ est31嗯。 尽管-Z标志涵盖了两个投射方向(回想起来,这可能是一个错误),但我们似乎不太可能同时翻转两个投射方向,而且在必须(例如,此问题取决于饱和度的性能,而在#41799中已商定了正确的解决方案)。
一个有点愚蠢的基准测试主要针对这个问题也可能是在测试修复至41799#的影响,但最多可导致虚报业绩回归的,所以我有点处之泰然。 (但是,如果有人愿意将-Z标志一分为二,请继续。)

我已经考虑过删除标记后不再使用它的任务的跟踪问题,但是我认为没有必要合并此处和#41799中进行的讨论。

我起草了一份内部文章: https :

随意复制它,或只给我笔记以便我可以发布它。 (请注意,我对const fn行为有些困惑)

另外一个小窍门是float-> int转换的成本是特定于当前实现的,而不是基本的。 在x86上, cvtss2si cvttss2si在过低,过高和nan情况下返回0x80000000,因此可以使用cvtss2si cvttss2si实现-Zsaturating-float-casts cvttss2si后跟0x80000000情况下的特殊代码,因此在常见情况下它可能只是一个比较和可预测分支。 在ARM上, vcvt.s32.f32已经具有-Zsaturating-float-casts语义。 无论哪种情况,LLVM当前都不会优化额外的检查。

@Gankro

太好了,非常感谢! 我在要点上留下了一些笔记。 读完这篇文章后,我想刺痛地将u128-> f32强制转换与-Z标志分开。 只是为了摆脱关于覆盖两个正交特征的标志的分散注意事项。

(我已提交#45900来重新定位-Z标志,以便它仅涵盖float-> int问题)

如果在要求进行大规模基准测试之前,我们可以获取平台特定的实现la @sunfishcode (至少对于x86),那将是很好的。 这应该不是很难。

问题是,据我所知,LLVM当前不提供执行此操作的方法,除了可能带有内联汇编,我不一定推荐发布。

我已对草案进行了更新,以反映讨论情况(基本上将对u128-> f32的任何内联提及都剔除到最后一个额外的部分)。

@sunfishcode你确定吗? 您要找的不是llvm.x86.sse.cvttss2si内在的吗?

这是一个使用它的游乐场链接:

https://play.rust-lang.org/?gist=33cf9e0871df2eb2475b845af4f1b574&version=每晚

在释放模式下, float_to_int_with_intrinsicfloat_to_int_with_as都编译成一条指令。 (在调试模式下, float_to_int_with_intrinsic浪费了一些指令,将零置为高电平,但这还不错。)

它甚至似乎可以正确地不断折叠。 例如,

float_to_int_with_intrinsic(42.0)

变成

movl    $42, %eax

但是值超出范围,

float_to_int_with_intrinsic(42.0e33)

不会折叠:

cvttss2si   .LCPI2_0(%rip), %eax

(理想情况下,它会折叠为常数0x80000000,但这没什么大不了的。重要的是它不会产生undef。)

哦,酷。 看起来那样行得通!

知道我们确实有办法在cvttss2si上建立是很酷的。 但是,我不同意在要求基准测试之前更改实现以使用它显然更好:

多数人会以x86为基准,因此,如果我们以x86为特例,则获得的有关通用实现的数据将少得多,这些数据仍将用于大多数其他目标。 诚然,已经很难推断出有关其他体系结构的任何信息,但是完全不同的实现方式使其完全不可能。

其次,如果我们现在使用“简单”解决方案收集基准,并且发现实际代码中没有性能下降(这就是我所期望的tbh),那么我们甚至不需要经历尝试进一步优化此代码路径。

最后,我什至不确定在cvttss2si上构建的速度是否会比现在快(尽管在ARM上,仅使用适当的指令显然会更好):

  • 您需要进行比较以注意转换将返回0x80000000,如果是这种情况,您仍然需要另一个比较(输入值)来知道您应该返回int :: MIN还是int :: MAX。 而且如果是带符号的整数类型,我看不到如何避免使用第三个比较来区分NaN。 因此,在最坏的情况下:

    • 您没有保存比较/选择的数量

    • 您正在将浮点数比较换成int比较,这可能适合OoO内核(如果您遇到可以进行比较的FU瓶颈,如果看起来比较大的话),但是该比较也取决于浮点数-> int比较,尽管当前实现中的比较都是独立的,所以这显然不是胜利。

  • 向量化可能变得更加困难或不可能。 我不希望循环矢量化程序完全处理此内在函数。
  • 还值得注意的是(AFAIK)此策略仅适用于某些整数类型。 例如,f32-> u8将需要对结果进行其他修复,这显然会使该策略无利可图。 我不太确定哪种类型会受此影响(例如,我不知道是否有f32-> u32的说明),但是仅使用这些类型的应用程序将完全无法受益。
  • 您可以在幸福的道路上只进行一个比较(而不是像以前的解决方案那样进行两个或三个比较,从而进行分支)来完成分支解决方案。 但是,正如@ActuallyaDeviloper先前指出的那样,分支可能不是理想的:现在,性能变得更加依赖工作负载和分支预测。

是否可以安全地假设无论基准测试显示什么,我们都需要unsafe fn as_u32_unchecked(self) -> u32和朋友们? 会有人有什么其他潜在追索权,如果他们最终观察放缓?

@bstrie我认为在这种情况下,将语法扩展到as <type> [unchecked]并要求unchecked仅出现在unsafe会更有意义。上下文。

如我所见,就直观而言, _unchecked的林用作as强制转换的变体,就直觉而言,以及在生成干净,可用的文档方面,都是如此。

@ssokolow添加语法应该永远是万不得已的方法,特别是如果仅用十个死记硬背就能解决所有这些问题的话。 即使是通用的foo.as_unchecked::<u32>()也比语法更改(以及随之而来的无休止的自行车棚)更可取,尤其是因为我们应该减少而不是增加unsafe解锁的事物数量。

点。 考虑到选择方案时,涡轮增压器让我无所适从,事后看来,我也不是今天晚上也没有在所有汽缸上开火,所以我在评论设计决策时应该更加谨慎。

就是说,将目标类型烘焙到函数名称中感觉不对。 turbo鱼是一个更好的选择。

通用方法可以由一组新的UncheckedFrom / UncheckedInto特征与unsafe fn方法来支持,将From / IntoTryFrom / TryInto集合。

@bstrie对于代码变慢的人来说,另一种解决方案是使用内部函数(例如,通过stdsimd)访问底层硬件指令。 早些时候我曾说过,这对优化器有缺点-自动向量化可能会遭受损失,并且LLVM无法利用它在超出范围的输入中返回undef的情况-但它确实提供了一种无需进行转换的方法在运行时进行任何额外的工作。 我不能确定这是否足够好,但似乎至少是合理的。

有关x86指令集中的转换的一些说明:

实际上,SSE2在提供给您的转换操作方面相对有限。 你有:

  • 具有32位寄存器的CVTTSS2SI系列:将单个浮点数转换为i32
  • 具有64位寄存器的CVTTSS2SI系列:将单个浮点数转换为i64 (仅x86-64)
  • CVTTPS2PI系列:将两个浮点数转换为两个i32s

每个变量都有f32f64变量(以及舍入而不是截断的变量,但这在这里没有用)。

但是,无符号整数没有任何内容,小于32的大小也没有任何内容,如果您使用的是32位x86,则对于64位则没有任何内容。 后来的指令集扩展添加了更多功能,但似乎几乎没有人为此进行编译。

结果,现有的(“不安全”)行为:

  • 要转换为u32,编译器将转换为i64并截断所得的整数。 (这会导致超出范围的值产生奇怪的行为,但这是UB,所以谁在乎。)
  • 要转换为16位或8位的任何内容,编译器会转换为i64或i32并截断所得的整数。
  • 要转换为u64,编译器会生成大量指令。 对于从f32到u64的GCC和LLVM生成等效项:
fn f32_to_u64(f: f32) -> u64 {
    const CUTOFF: f32 = 0x8000000000000000 as f32; // 2^63 exactly
    if !(f >= CUTOFF) { // less, or NaN
        // just use the signed conversion
        f as i64 as u64
    } else {
        0x8000000000000000u64 + ((f - CUTOFF) as i64 as u64)
    }
}

无关的事实:“转换而不是截断”代码的生成是引起Super Mario 64中“并行宇宙”故障的原因。 因此,适合i16但不适合i32“换行”的坐标,例如转到坐标65536.0,则可以将碰撞检测为0.0。

无论如何,结论:

  • “测试0x80000000并具有特殊的处理程序”仅适用于转换为i32和i64。
  • 但是,对于转换为u32,u / i16和u / i8而言,“测试截断/符号扩展的输出是否与原始输出不同”是等效的。 (这将获取两个原始转换范围内但最终类型超出范围的整数,以及0x8000000000000000(指示浮点数为NaN或原始转换范围外的指示符)。
  • 但是,在这种情况下,分支的成本和大量额外的代码可能是过大的。 如果可以避免分支,那就可以了。
  • @ActuallyaDeviloper基于minss / maxss的方法还不错! 最小形式
minss %xmm2, %xmm1
maxss %xmm3, %xmm1
cvttss2si %rax, %xmm1

只有三个指令(具有适当的代码大小和吞吐量/延迟),并且没有分支。

然而:

  • 纯Rust版本需要对NaN进行额外测试。 对于转换为32位或更小的值,可以使用内在函数来避免,方法是使用64位cvttss2si并截断结果。 如果输入不是NaN,则最小值/最大值可确保整数通过截断保持不变。 如果输入为NaN,则整数为0x8000000000000000,该整数将截断为0。
  • 我不包括将2147483647.0和-2148473648.0加载到寄存器中的成本,通常每一个都从内存中移动一个。
  • 对于f32,无法完全表示2147483647.0,因此实际上不起作用:您需要再次检查。 这使情况变得更糟。 f64到u / i64的同上,但f64到u / i32的也没有此问题。

我建议在两种方法之间进行折衷:

  • 对于f32 / f64到u / i16和u / i8,以及f64到u / i32,请使用最小/最大+截断,如上所述,例如:
    let f = if f > 32767.0 { 32767.0 } else { f };
    let f = if f < -32768.0 { -32768.0 } else { f };
    cvttss2si(f) as i16

(对于u / i16和u / i8,原始转换可以是到i32;对于从f64到u / i32的转换,必须是到i64。)

  • 对于f32 / 64至u32,
    let r = cvttss2si64(f) as u32;
    if f >= 4294967296.0 { 4294967295 } else { r }

只是一些说明,没有分支:

    cvttss2si   %xmm0, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movl    $-1, %eax
    cmovbl  %ecx, %eax
  • 对于f32 / 64到i64,也许
    let r = cvttss2si64(f);
    if f >= 9223372036854775808. {
        9223372036854775807 
    } else if f != f {
        0
    } else {
        r
    }

这会产生更长的序列(仍然是无分支的):

    cvttss2si   %xmm0, %rax
    xorl    %ecx, %ecx
    ucomiss %xmm0, %xmm0
    cmovnpq %rax, %rcx
    ucomiss .LCPI0_0(%rip), %xmm0
    movabsq $9223372036854775807, %rax
    cmovbq  %rcx, %rax

…但是至少与天真的方法相比,我们保存了一个比较,好像f太小了,0x8000000000000000已经是正确的答案(即i64 :: MIN)。

  • 对于从f32到i32的服务器,不确定是否最好像以前一样做,还是先转换为f64然后再做更短的min / max。

  • u64简直是一团糟,我不想在想。 :p

基准测试的呼声很高: https

https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14中,有人报告了使用图像包装箱进行JPEG编码的可测量且显着的降低。 我已对该程序进行了最小化,因此它是独立的,并且主要专注于与减速相关的部分: https :

请注意,强制类型转换是f32-> u8( rgb_to_ycbcr )和f32-> i32( encode_rgb ,“量化”循环)的比例相等。 看起来输入也都在范围内,即饱和度实际上不会发生,但是在f32-> u8的情况下,只能通过计算多项式的最小值和最大值并考虑取整误差来验证这一点。有很多问题要问。 f32-> i32强制转换在i32的范围内更明显,但这仅是因为self.tables元素非零,这(显然吗?)对于优化器来说显示起来并不那么容易,尤其是在原始程序中。 tl; dr:饱和度检查一直存在,唯一的希望就是使其更快。

我还对LLVM IR进行了一些评论-从字面上看,唯一的区别是比较和从饱和转换中进行选择。 快速浏览一下,就可以看到该指令集具有相应的说明,当然还有更多实时值(这会导致更多溢出)。

@comex您认为使用CVTTSS2SI可以使f32-> u8和f32-> i32转换速度明显提高吗?

较小的更新,从rustc 1.28.0-nightly (952f344cd 2018-05-18)-Zsaturating-float-casts标志仍导致https://github.com/rust-lang/rust/issues/10184#issuecomment -345479698中的代码为〜20在x86_64上慢%。 这意味着LLVM 6并未进行任何更改。

| 标志计时|
| ------- | -------:|
| -Copt-level = 3 -Ctarget-cpu = native | 325,699 ns / iter(+/- 7,607)|
| -Copt-level = 3 -Ctarget-cpu = native -Zsaturating-float-casts | 386,962 ns / iter(+/- 11,601)
(慢19%)|
| -Copt-level = 3 | 331521 ns / iter(+/- 14,096)|
| -Copt-level = 3 -Zsaturating-float-casts | 413,572 ns / iter(+/- 19,183)
(慢25%)|

@kennytm我们期望LLVM 6有所改变吗? 他们是否正在讨论一种有益于该用例的特定增强功能? 如果是这样,票号是多少?

@insanitybit它...似乎仍然处于打开状态...?

image

抱歉,不知道我在看什么。 谢谢!

@rkruppe不能确保LLVM中的float至int强制转换不再是UB
(通过更改文档)?

在2018年7月20日上午4:31,“ Colin” [email protected]写道:

抱歉,不知道我在看什么。

-
您收到此消息是因为您已订阅此线程。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/10184#issuecomment-406462053
或静音
线程
https://github.com/notifications/unsubscribe-auth/AApc0v3rJHhZMD7Kv7RC8xkGOiIhkGB1ks5uITMHgaJpZM4BJ45C

@nagisa也许您正在考虑f32::from_bits(v: u32) -> f32 (以及类似的f64 )? 它曾经对NaN进行过一些标准化,但现在只是transmute

这个问题是关于as转换,它们试图近似数值。

啊,是的,那是漂浮的。

2018年7月20日星期五,12:24罗宾·克鲁普(Robin Kruppe) [email protected]写道:

@nagisa https://github.com/nagisa您可能会想到float- > float
演员表,请参阅#15536 https://github.com/rust-lang/rust/issues/15536
rust-lang-nursery / nomicon#65
https://github.com/rust-lang-nursery/nomicon/pull/65

-
您收到此邮件是因为有人提到您。
直接回复此电子邮件,在GitHub上查看
https://github.com/rust-lang/rust/issues/10184#issuecomment-406542903
或使线程静音
https://github.com/notifications/unsubscribe-auth/AApc0gA24Hz8ndnYhRXCyacd3HdUSZjYks5uIaHegaJpZM4BJ45C

LLVM 7发行说明提到了一些内容:

浮点转换的优化得到改善。 对于依赖于溢出强制转换的未定义行为的代码,这可能会导致令人惊讶的结果。 可以通过指定函数属性来禁用优化:“ strict-float-cast-overflow” =“ false”。 该属性可以通过clang选项-fno-strict-float-cast-overflow创建。 代码清理程序可用于检测受影响的模式。 仅用于检测此问题的clang选项是-fsanitize = float-cast-overflow:

这与这个问题有关系吗?

只要它不是不安全的未定义行为,我们就不必关心LLVM对溢出的强制转换做了什么。 只要不引起不良行为,结果就可能是垃圾。

这与这个问题有关系吗?

并不是的。 UB并没有改变,LLVM在利用它方面变得更加积极,这使得它在实践中更容易受到它的影响,但是稳健性问题没有改变。 特别是,新的属性不会删除UB或影响LLVM 7之前就已经存在的任何优化。

@rkruppe出于好奇,是否已将其摔倒? 似乎https://internals.rust-lang.org/t/help-us-benchmark-saturating-float-casts/6231/14进行得很好,并且实现没有太多错误。 似乎总是会出现轻微的性能下降,但是正确编译似乎是一个值得权衡的问题。

这只是在等待被推到终点吗? 还是还有其他已知的阻滞剂?

通常,我已经分心/忙于其他事情,但是RBG JPEG编码中的x0.82回归似乎不仅仅“轻微”,还可以吞咽一些苦味(尽管这可以确保其他种类的工作负载似乎没有受到影响) 。 还不够严厉,我不愿意默认打开饱和度,但是足以让我犹豫自己亲自尝试一下,然后再尝试“还提供了一个比饱和度更快的转换函数,但可能会产生(安全的)垃圾” ”选项之前讨论过。 我还没有做到这一点,而且显然也没有其他人,所以这已经被抛在一边了。

好的,非常感谢@rkruppe的更新! 我很好奇,但实际上是否有安全垃圾选项的实现? 我可以想象我们很容易提供诸如unsafe fn i32::unchecked_from_f32(...)类的东西,但是听起来您在想这应该是一个安全的功能。 今天使用LLVM可以做到吗?

还没有freeze ,但是可以使用内联汇编来访问目标体系结构的指令,以将浮点数转换为整数(回退到例如使as饱和)。 虽然这可能会抑制某些优化,但可能足以在某些基准中修复回归。

一个unsafe函数可以保留此问题所涉及的UB(并且与今天的as一样进行代码生成)是另一种选择,但是吸引力不大得多,我是d如果可以完成工作,则更喜欢安全功能。

安全饱和的float-int序列也有很大的

     cvttsd2si %xmm0, %eax   # x86's cvttsd2si returns 0x80000000 on overflow and invalid cases
     cmp $1, %eax            # a compact way to test whether %eax is equal to 0x80000000
     jno ok
     ...  # slow path: check for and handle overflow and invalid cases
ok:

这应该比rustc当前的速度快得多。

好的,我只是想确保澄清一下,谢谢! 我发现内联asm解决方案不能作为默认值使用,因为它会过多地抑制其他优化,但是我还没有尝试过。 我个人更希望我们通过定义一些合理的行为(就像今天的饱和演员表一样)来关闭这个不完善的漏洞。 如果有必要,我们可以始终将当今的快速/不完善实现保留为不安全的函数,并且在给定无限资源的时间内,我们甚至可以极大地改进默认值和/或添加其他专门的转换函数(例如安全转换,即超出范围的情况不是UB,只是垃圾位模式)

其他人会反对这种策略吗? 我们是否认为这还不足以同时解决?

我认为cvttsd2si (或类似指令)的内联汇编应该是可以容忍的,特别是因为该内联汇编不会访问内存或具有副作用,因此它只是一个不透明的黑匣子,如果不使用它,则可以将其删除。 LLVM很大程度上抑制它的优化,LLVM只是无法推断内联汇编的内部和结果值。 最后一点就是为什么我会怀疑例如对代码序列使用内联asm @sunfishcode表示饱和的原因:为饱和引入的检查如果有多余的话今天可以偶尔删除,但是内联asm块中的分支可以简化一下。

其他人会反对这种策略吗? 我们是否认为这还不足以同时解决?

我不反对立即饱和并可能在以后添加替代方案,我只是不想成为必须鼓吹共识并将其证明给代码速度较慢的用户的人just

我已经开始进行一些工作以实现将LLVM中的float转换为int强制转换的内在函数: https :

如果任何地方都能实现,它将提供一种开销较低的方式来获取饱和的语义。

如何重现这种未定义的行为? 我在注释中尝试了该示例,但结果是255 ,对我来说似乎不错:

println!("{}", 1.04E+17 as u8);

无法以这种方式可靠地观察到未定义的行为,有时它可以提供您期望的结果,但在更复杂的情况下会崩溃。

简而言之,我们使用的代码生成引擎(LLVM)可以假定不会发生这种情况,因此,如果依赖于这种假设,它可能会生成错误的代码。

@ AaronM04今天在reddit

fn main() {
    let a = 360.0f32;
    println!("{}", a as u8);

    let a = 360.0f32 as u8;
    println!("{}", a);

    println!("{}", 360.0f32 as u8);
}

(见游乐场

我认为最后的评论是针对@ AaronM04的,参考了他们先前的评论

“哦,那很容易。”

  • @pcwalton ,2014

抱歉,我已经认真阅读了这6年的所有良好历史。 但是,很严重的是,十分之六的十年! 如果这是一个政客论坛,那么人们会预料到这里会遭到破坏。

因此,请问任何人都可以简单地解释一下,这使寻找解决方案的过程比解决方案本身更有趣吗?

因为它比最初看起来要难,并且需要更改LLVM。

好的,但这不是上帝在第二周制作了此LLVM,并且朝着相同的方向前进可能要再花费15年才能解决这个基本问题。

确实,我没有伤害别人的事,Rust基础架构的新成员突然为我提供帮助,但是当我了解到这种情况时,我感到非常震惊。

该问题跟踪工具用于讨论如何解决此问题,并指出明显的问题在该方向上没有任何进展。 因此,如果您想帮助解决问题或提供一些新信息,请这样做,但是否则您的评论将不会神奇地使修复程序出现。 :)

我认为这种要求更改LLVM的假设为时过早。

我认为我们可以使用性能成本最低的语言来做到这一点。 难道是一个重大更改* *是啊,但它可以做,而且应该做的。

我的解决方案是将float到int强制转换定义为unsafe然后在标准库中提供一些帮助器函数,以提供绑定在Result Types中的结果。

这是一个不安全的修补程序,并且是一个突破性的更改,但最终,这是每个开发人员都必须编写自己的代码才能解决现有UB的问题。 这是正确的防锈方法。

谢谢@RalfJung ,让我明白了。 我无意侮辱任何人或蔑视干预生产性头脑风暴过程。 诚然,锈病新手,我无能为力。 但是,它可以帮助我和其他可能尝试生锈的人,更多地了解其未解决的缺陷并提供相关的输出:是值得进行更深入的挖掘还是现在选择其他更好的选择。 但是我已经很高兴删除“我的无用评论”会容易得多。

正如该线程前面所提到的那样,正如相关团队早就同意的那样,通过修复llvm以支持必要的语义,这是缓慢而可靠的解决方法。

真的没有什么可以添加到此讨论中了。

https://reviews.llvm.org/D54749

@nikic好像LLVM方面的进展停滞了,如果可以的话,您能否简要介绍一下? 谢谢。

如果他们愿意采取一些偏好的回归以获得声音,可以将饱和转换作为用户可以选择的库函数来实现吗? 我正在阅读编译器的实现,但似乎很微妙:

https://github.com/rust-lang/rust/blob/625451e376bb2e5283fc4741caa0a3e8a2ca4d54/src/librustc_codegen_ssa/mir/rvalue.rs#L774 -L901

我们可以公开一个独立于-Z标志的内部函数,该内部函数生成LLVM IR进行饱和(无论是当前的开放编码IR还是将来的llvm.fpto[su]i.sat )。 这一点都不困难。

但是,我担心这是否是最佳做法。 当(if?)饱和度成为as强制转换的默认语义时,这样的API将变得多余。 告诉用户应该选择自己想要的声音还是性能,这似乎是不合适的,即使这只是暂时的。

同时,目前的情况显然更加糟糕。 如果我们正在考虑添加库API,我会越来越多地警告您默认情况下仅启用饱和,并提供一个unsafe内部函数,该函数的UB在NaN上并且超出范围(并且降低到普通的fpto[su]i )。 那仍然会提供基本相同的选择,但默认情况下是健全的,并且新的API将来可能不会变得多余。

默认情况下切换到声音听起来不错。 我认为我们可以根据要求而不是一开始就懒惰地提供内在函数。 另外,在这种情况下,const eval是否也会饱和? (抄送@RalfJung @eddyb @ oli-obk)

确认我们已经做饱和并且已经做了很久了,我想甚至在miri之前(我显然还记得在旧的基于llvm::Constant的评估器中进行了更改)。

@rkruppe太棒了! 既然您熟悉所讨论的代码,您是否想带头切换默认设置?

@rkruppe

我们可以公开一个内部函数,该内部函数会生成LLVM IR进行饱和

对于源和目标类型的每种组合,这可能需要10或12个独立的内在函数。

@Centril

默认情况下切换到声音听起来不错。 我认为我们可以根据要求而不是一开始就懒惰地提供内在函数。

我认为与其他注释不同,您的注释中的“内在”意味着当as饱和时,其偏好回归将更少。

我认为这不是处理已知重大回归的好方法。 对于某些用户而言,性能损失可能是一个真正的问题,而他们的算法确保输入始终在范围内。 如果他们没有订阅该线程,他们可能只会意识到当更改到达稳定通道时,它们会受到影响。 到那时,即使我们应要求立即提供不安全的API,它们也可能会停留6到12周。

我宁愿我们遵循已经为弃用警告建立的模式:仅在Stable可用了一段时间后,才在Nightly中进行切换。

对于源和目标类型的每种组合,这可能需要10或12个独立的内在函数。

很好,您找到了我,但是我看不到这有什么关系? 假设它是30个内在函数,添加它们仍然是微不足道的。 但是实际上,由N个瘦包装器使用单个通用内在函数甚至更容易。 如果我们选择“发出as声音并引入unsafe cast API”选项,则数字也不会更改。

我认为这不是处理已知的重大回归的好方法。 对于某些用户而言,性能损失可能是一个真正的问题,而他们的算法确保输入始终在范围内。 如果他们没有订阅该线程,他们可能只会意识到当更改到达稳定通道时,它们会受到影响。 到那时,即使我们应要求立即提供不安全的API,它们也可能会停留6到12周。

+1

我不确定是否需要弃用警告的程序(仅在替换稳定后每晚才弃用),因为在所有发行渠道上保持无性能下降的重要性似乎比在所有发行渠道上保持无警告的重要性低,但是再说一遍,再等待12周基本上就是解决此问题已持续多长时间的舍入错误。

我们还可以保留-Zsaturating-float-casts左右(只是更改默认值),这意味着任何夜间用户仍然可以选择退出Cange一段时间。

(是的,内在函数的数量仅是实现细节,并不表示支持或反对任何东西。)

@rkruppe我不能声称自己在这里已经消化了所有注释,但是我的印象是LLVM现在确实有一个冻结指令,这是阻止在此处消除UB的“最短路径”的项目,对吗?

虽然我猜freeze太新了,以至于在我们自己的LLVM版本中可能不可用,对吗? 不过,似乎我们应该在2020年上半年探索发展之路?

提名在T编译器会议上进行讨论,以试图就我们目前所希望的道路达成粗略的共识。

尽管由于此处引用的所有原因,使用freeze仍然存在问题。 我不确定对于这些演员表使用冻结会产生多大的现实顾虑,但原则上它们适用。 基本上,期望freeze可以返回随机垃圾或您的秘密密钥,无论何者更糟。 (我在某处在线阅读此书,真的很喜欢它的摘要。:D)

无论如何,对于as强制转换,即使返回随机垃圾似乎也很糟糕。 与unchecked_add类似,在需要的地方进行更快的速度操作是有道理的,但是使默认设置似乎非常违反Rust的精神。

@SimonSapin,您首先提出了相反的方法(默认为不健全/“怪异”的语义,并提供了明确的健全方法); 我无法从您以后的评论中看出,您认为默认(健全的过渡期后)是否也合理/更好?

@pnkfelix

我的印象是LLVM现在确实有一个冻结指令,这是阻止此处消除UB的“最短路径”的项目,对吗?

有一些警告。 最重要的是,即使我们只关心摆脱UB,并且我们将捆绑的LLVM更新为包括freeze (我们可以随时执行此操作),我们也支持多个较旧的版本(回到LLVM 6时刻),我们需要一些后备实现以使这些用户实际上摆脱所有用户的UB。

其次,当然是这样一个问题:我们是否只在乎“ UB”? 特别是,我想再次强调一下, freeze(fptosi %x)行为非常违反直觉:它是不确定的,并且每次执行时都可以返回不同的结果(即使从@RalfJung所说的敏感内存中freeze -using)转换非默认选项。

@RalfJung我的立场是,无论此问题如何,都最好完全避免as ,因为根据输入和输出类型的不同,语义可能截然不同(截断,饱和,舍入等),但并非总是如此阅读代码时显而易见。 (甚至可以用foo as _推断出后者。)因此,我有一份RFC草案,用于提议各种显式命名的转换方法,这些方法涵盖了as如今(甚至可能更多)的情况。 。

我认为as绝对不应该包含UB,因为它可以在unsafe 。 返回垃圾也不是一件好事。 但是对于已知的由饱和转换导致的性能下降的案例,我们可能应该采取某种缓解/过渡/替代方法。 我只问过饱和转换的库实现,以便在此过渡时不阻止RFC草案。

@SimonSapin

我的立场是,无论出现什么问题,都应完全避免,因为它可能具有截然不同的语义(截断,饱和,舍入等)。

同意但这并不能真正帮助我们解决这个问题。

(此外,我很高兴您正在努力使不需要as 。期待着。:D)

我认为绝对不应该使用UB,因为它可以在不安全的外部使用。 返回垃圾也不是一件好事。 但是对于已知的由饱和转换导致的性能下降的案例,我们可能应该采取某种缓解/过渡/替代方法。 我只问过饱和转换的库实现,以便在此过渡时不阻止RFC草案。

因此,我们似乎同意最终状态应该是浮点到整数as饱和? 我对任何过渡计划都感到满意,只要这是我们朝着最终目标迈进。

最终目标对我来说听起来不错。

我认为这不是处理已知的重大回归的好方法。 对于某些用户而言,性能损失可能是一个真正的问题,而他们的算法确保输入始终在范围内。 如果他们没有订阅该线程,他们可能只会意识到当更改到达稳定通道时,它们会受到影响。 到那时,即使我们应要求立即提供不安全的API,它们也可能会停留6到12周。

在我看来,如果这些用户在6-12周内等待升级rustc,这不会是世界末日-在任何情况下,他们可能都不需要即将发布的版本中的任何内容,或者他们的库可能具有MSRV约束来坚持。

同时,还没有订阅该线程的用户可能会得到错误的编译,就像他们可能会损失性能一样。 我们应该优先考虑哪个? 我们提供稳定性方面的保证,也提供安全方面的保证-但是据我所知,没有关于性能的此类保证(例如RFC 1122根本没有提到perf)。

我宁愿我们遵循已经为弃用警告建立的模式:仅在Stable可用了一段时间后,才在Nightly中进行切换。

就弃用警告而言,至少在我所知的范围内,等待弃用直到有稳定的替代方案的结果在等待期间不会造成健全的漏洞。 (此外,尽管可以在此处提供内在函数,但通常情况下,在修复健全性漏洞时,我们可能无法合理地提供替代方案。因此,我认为在稳定状态上拥有替代方案并不是一个硬性要求。)

很好,您找到了我,但是我看不到这有什么关系? 假设它是30个内在函数,添加它们仍然是微不足道的。 但是实际上,由N个瘦包装器使用单个通用内在函数甚至更容易。 如果我们选择“发出as声音并引入unsafe cast API”选项,则数字也不会更改。

对于那些12/30特定的单态实例化,单个通用内在函数是否需要在编译器中进行单独实现?

向编译器添加内在函数可能并不容易,因为LLVM已经完成了大部分工作,但这还远未达到全部成本。 此外,在Miri,Cranelift中也有实现,以及规范中所需的最终工作。 因此,我认为我们不应该在有人需要它们的机会之外添加内部函数。

但是,我不反对公开更多的内在函数,但是如果有人需要它们,他们应该提出建议(例如,作为带有详细说明的PR),并用一些基准数字或类似基准来证明添加的合理性。

我们还可以保留-Zsaturating-float-casts左右(只是更改默认值),这意味着任何夜间用户仍然可以选择退出Cange一段时间。

这对我来说似乎不错,但我建议将标志重命名为-Zunsaturating-float-casts以避免对已经使用此标志的用户改变语义,使其不健全。

@Centril

对于那些12/30特定的单态实例化,单个通用内在函数是否需要在编译器中进行单独实现?

不,通过参数化源位和目标位的宽度可以共享大多数实现。 只有几位需要区分大小写。 这同样适用于miri中的实现,最可能也适用于其他实现和规范。

(编辑:要清楚,即使存在N个不同的内在函数,也可能发生这种共享,但是单个内在函数会减少每个内在函数所需要的样板。)

因此,我认为我们不应该在有人需要它们的机会之外添加内部函数。

但是,我不反对公开更多的内在函数,但是如果有人需要它们,他们应该提出建议(例如,作为带有详细说明的PR),并用一些基准数字或类似基准来证明添加的合理性。 同时,我不认为这应该阻止修复声音漏洞。

我们已经有一些基准数据。 我们很早就从对基准的呼吁中知道,在具有饱和转换的情况下,x86_64上的JPEG编码会明显变慢。 有人可以重新运行这些代码,但我有信心预测它不会改变(尽管具体数字不会完全相同),并且看不出任何将来改变实现方式的理由(例如切换为嵌入式asm或LLVM内部函数@nikic进行了工作)将从根本上改变这一点。 虽然很难确定未来,但我的有根据的猜测是,要获得这种性能,唯一可行的方法是使用无需范围检查即可生成代码的东西,例如unsafe转换或使用freeze东西。

好的,因此从现有的基准测试数字看来,人们似乎对上述内在函数有积极的渴望。 如果是这样,我将提出以下行动计划:

  1. 同时:

    • 通过#[unstable(...)]函数介绍每晚公开的内部函数。

    • 删除-Zsaturating-float-casts并引入-Zunsaturating-float-casts

    • 将默认设置切换为-Zsaturating-float-casts功能。

  2. 我们会在一段时间后稳定内在函数; 我们可以快速跟踪一下。
  3. 稍后删除-Zunsaturating-float-casts

听起来不错。 除了内部函数是某些公共API的实现细节外,可能是f32f64 。 它们可以是:

  • 通用特征的方法(带有用于转换的整数返回类型的参数),可以选择在前奏中使用
  • 具有支持特征的固有方法(类似于str::parseFromStr )以支持不同的返回类型
  • 名称中具有目标类型的多个非泛型固有方法

是的,我的意思是通过方法或类似方法公开内在函数。

名称中具有目标类型的多个非泛型固有方法

感觉就像我们平常所做的一样-是否对此选项有异议?

是吗我觉得当方法名称中包含(签名的)类型名称时,它是临时的“一种类型”转换(例如Vec::as_slice[T]::to_vec ) ,或一系列差异不是类型的转化(例如to_ne_bytesto_be_bytesto_le_bytes )。 但是std::convert特性的部分动机是避免使用数十种单独的方法,例如u8::to_u16u8::to_u32u8::to_u64等。

我想知道的是,鉴于方法需要为unsafe fn这是否自然可以推广为特征。 如果我们确实添加了固有方法,那么您始终可以委托给trait实现中的方法或其他方法。

为不安全的转换添加特征对我来说似乎很奇怪,但是我想西蒙可能正在考虑这样一个事实,即对于浮点数和整数类型的每种组合,我们可能需要一种不同的方法(例如f32::to_u8_unsaturatedf32::to_u16_unsaturated等)。

不要在我还没有完全了解的长线程上占一席之地,但这是所希望的还是拥有例如f32::to_integer_unsaturated转换为u32东西就足够了? 对于不安全转换,目标类型是否有明显的选择?

仅提供不安全的转换(例如,仅对i32 / u32进行转换)会完全排除其值范围并非严格较小的所有整数类型,并且有时确实需要这样做。 通常也需要变小(减小到u8,如JPEG编码一样),但可以通过转换为更宽的整数类型并用as截断来模拟(虽然便宜,但通常不是免费的)。

但是我们不能很好地仅提供到最大整数大小的转换。 并非总是本地支持的(因此,速度很慢),优化无法解决此问题:优化“转换为大int,然后截断”为“直接转换为小int”是不明智的,因为后者具有UB(在LLVM IR中) /在截断时原始转换结果会环绕的情况下(在机器代码级别,在大多数体系结构上)会有不同的结果。

请注意,即使务实地排除128位整数并专注于64位整数对于常见的32位目标也仍然不利。

我是这次对话的新手,但不是编程人员。 我很好奇为什么人们认为饱和转换并将NaN转换为零是合理的默认行为。 我知道Java可以做到这一点(尽管环绕似乎更常见),但实际上NaN不能说是正确的转换,没有整数值。 同样,将1000000.0转换为65535(u16)似乎是错误的。 根本没有u16显然是正确的答案。 至少,我不认为它比将其转换为16960的当前行为更好,它至少是与C / C ++,C#,go和其他对象共享的行为,因此至少有些不足为奇。

许多人都对溢出检查的相似性发表了评论,我也同意。 它也类似于整数除以零。 我认为无效转换应该像无效算术一样惊慌。 依赖NaN-> 0和1000000.0-> 65535(或16960)似乎容易出错,就像依赖整数溢出或假设的n / 0 == 0一样。这种情况默认情况下会产生错误。 (在发行版中,rust可以避免错误检查,就像使用整数算术一样。)在极少数情况下,如果您想将NaN转换为零或具有浮点饱和度,则必须选择使用它,就像您必须选择整数溢出。

至于性能,似乎最高的一般性能将来自进行纯转换并依靠硬件故障。 例如,当无法正确表示浮点数到整数的转换时(包括NaN和超出范围的情况),x86和ARM都会引发硬件异常。 除无效转换外,此解决方案的成本为零,除了在调试版本中将浮点数直接转换为小整数类型时(这种情况很少见),在这种情况下它仍应相对便宜。 (在不支持这些异常的理论硬件上,可以在软件中对其进行仿真,但只能在调试版本中进行模拟。)我想,硬件异常正是今天实现检测零除整数的方式。 我看到了很多有关LLVM的话题,所以也许您在这里受到了限制,但是不幸的是,即使在发行版本中,也要在每个浮点转换中都进行软件仿真,以便为固有的无效转换提供可疑的替代行为。

@admilazz我们受限于LLVM的功能,目前LLVM尚未提供一种有效地将浮点数转换为整数而没有未定义行为风险的方法。

饱和是因为该语言将as强制转换定义为始终成功,因此我们不能将运算符更改为紧急状态。

同样,将1000000.0转换为65535(u16)似乎是错误的。 根本没有u16显然是正确的答案。 至少,我不认为它比将其转换为16960的当前行为更好,

这对我来说并不明显,所以我认为值得指出:16960是将1000000.0转换为足够宽的整数,然后截断以保留16个低位的结果。

这不是该线程之前建议的选项,而是〜(编辑:我在这里错了,很抱歉,我没有找到它)也不是当前行为。 Rust中的当前行为是超出范围的浮点数到整数转换是未定义行为。 在实践中,这通常会导致垃圾值,从原则上讲,它可能会导致错误编译和漏洞。 这个线程就是要解决这个问题。 当我在Rust 1.39.0中运行以下程序时,每次都会得到一个不同的值:

fn main() {
    dbg!(1000000.0 as u16);
}

操场。 输出示例:

[src/main.rs:2] 1000000.0 as u16 = 49072

我个人认为像整数这样的截断并不比饱和好或差,它们在数值上都超出范围是错误的。 只要是确定性的,而不是UB,无误的转换就有它的位置。 您可能已经从算法中知道值在范围内,或者可能不在乎这种情况。

我认为我们应该添加返回Result错转换API,但我仍然需要完成该草稿(RFC之前的编写):)

“转换为整数,那么截断为目标宽度”或“回绕”语义被在此线程(https://github.com/rust-lang/rust/issues/10184#issuecomment-299229143)之前建议。 我不是特别喜欢它:

  • 我认为这比饱和稍不明智。 饱和度一般不给出了数字有意义的结果远远超出了范围,但是:

    • 当数字稍微超出范围时(例如,由于累积舍入误差),它的表现比回绕更明智。 相反,环绕的转换可以将浮点计算中的轻微舍入误差放大为整数域中的最大可能误差。

    • 它通常在数字信号处理中使用,因此至少在某些实际需要的应用中。 相比之下,我不知道从环绕语义中受益的单一算法。

  • AFAIK首选环绕式语义的唯一原因是软件仿真的效率,但这对我来说似乎是未经证实的假设。 我很乐意被证明是错误的,但是粗略地看一下,环绕似乎需要这么长的ALU指令链(加上分别处理无穷和NaN的分支),我不觉得很明显会有更好的选择性能比其他。
  • 尽管对于任何转换为​​整数的问题, NaN的问题是一个丑陋的问题,但饱和度至少不需要任何特殊的大小写(无论在语义上还是在大多数实现中)都无穷大。 但是,对于环绕,等于+/-无限的整数等效值是多少? JavaScript表示它为0,我想如果我们对NaN进行as恐慌,那么它也可能对无穷大表示恐慌,但是无论哪种方式,这似乎都比通常的和不正常的数字更难使折回更快一个人会建议。

我怀疑使用饱和度语义进行转换的大多数代码在使用SIMD时会更好。 因此,尽管不幸的是,此更改不会阻止编写高性能代码(特别是如果提供了具有不同语义的内在函数),甚至可能会使某些项目趋向于更快(如果移植性更差)。

如果是这样,则不应将一些轻微的性能下降作为辩解,以避免关闭健全性漏洞。

https://github.com/rust-lang/rust/pull/66841添加unsafe fn使用LLVM的fptouifptosi进行转换的unsafe fn方法,对于那些已知值的情况处于范围和饱和状态是可测量的性能回归。

在那之后,我认为可以将默认值更改为as (也许再添加一个-Z标志以选择退出?),尽管这可能是Lang团队的正式决定。

在那之后,我认为最好将默认值更改为as (也许再添加一个-Z标志退出),尽管这可能是Lang团队的正式决定。

所以我们(语言团队,至少是在那里的人)在https://github.com/rust-lang/lang-team/blob/master/minutes/2019-11-21.md中对此进行了讨论,我们认为添加新的内在函数+添加-Zunsaturated-float-casts将是很好的第一步。

我认为最好将默认值设置为此设置的一部分,也可以在此之后不久,如有必要,可以使用FCP。

我认为,通过新的内在函数,您的意思是类似https://github.com/rust-lang/rust/pull/66841

在不更改默认值的情况下添加-Z unsaturated-float-casts是什么意思? 接受它为无操作,而不是发出“错误:未知的调试选项”?

我认为通过新的内在函数,您的意思是像#66841

是的-感谢您的带头行动。

在不更改默认值的情况下添加-Z unsaturated-float-casts是什么意思? 接受它为无操作,而不是发出“错误:未知的调试选项”?

是的,基本上。 另外,我们删除-Z saturated-float-casts赞成-Z unsaturated-float-casts和直接切换默认的,但它应该导致了更少的永久居民相同的结果。

我真的不明白“不饱和”的建议。 如果目标只是提供一个选择退出新默认值的旋钮,则只需更改现有标志的默认值而仅执行其他操作会更容易。 如果目标是选择一个更清晰地权衡(不健全)的新名称,那么“不饱和”是很糟糕的—我建议使用包含“不安全”或“ UB”或类似名称的名称可怕的词,例如-Z fix-float-cast-ub

unchecked是在API名称中具有一些先例的术语。

@admilazz我们受限于LLVM的功能,目前LLVM尚未提供一种有效地将浮点数转换为整数而没有未定义行为风险的方法。

但是大概您只能像在整数溢出中那样在调试版本中添加运行时检查。

AFAIK首选环绕式语义的唯一原因是软件仿真的效率

我认为我们不应该选择环绕或饱和,因为两者都是错误的,但是环绕至少具有作为许多类似于rust的语言所使用的方法的好处:C / C ++,C#,go,可能是D,当然以及锈病的当前行为(至少有时)。 就是说,我认为“对无效转换感到恐慌(可能仅在调试版本中)”是理想的,就像我们对整数溢出和无效算术(例如被零除)所做的那样。

(有趣的是,我确实在操场上得到了16960。但从其他示例中我看到,有时锈的作用有所不同...)

饱和是因为语言定义为始终成功的强制转换,因此我们不能将运算符更改为紧急状态。

就我们关心已经执行此操作的人员的结果而言,更改操作评估的结果已经是一项重大更改。 这种无恐慌的行为也可能改变。

我想,如果我们对NaN感到恐慌,那么它也可能对无穷大感到恐慌,但是无论哪种方式,这似乎都会使绕回变得越来越困难

如果仅在调试版本中进行检查(就像整数溢出一样),那么我认为我们可以两全其美:保证转换正确(在调试版本中),更有可能捕获用户错误,您可以选择加入如果您愿意的话,可以解决诸如环绕和/或饱和之类的怪异行为,并且性能也可以达到最好。

另外,通过命令行开关控制这些东西似乎很奇怪。 那是一个很大的锤子。 当然,超出范围的转换的期望行为取决于算法的细节,因此应该在每次转换的基础上进行控制。 我建议使用f.to_u16_sat()和f.to_u16_wrap()或类似选项,不要使用任何命令行选项来更改代码的语义。 这将使得难以混合和匹配不同的代码段,并且您无法通过阅读来理解某些功能……

并且,如果将“无效时紧急”设为默认行为确实是不可接受的,那么最好有一个内在的方法来实现它,但仅在调试版本中执行有效性检查,这样我们就可以确保转换在( (大多数?)情况下,我们希望在转换后会获得相同的数量,但不会在发布版本中支付任何罚款。

有趣的是,我确实在操场上得到了16960。

这就是未定义行为的工作方式:根据程序的确切公式,确切的编译器版本和确切的编译标志,您可能会得到确定性的行为,或者每次运行都会更改的乱码值,或者编译错误。 允许编译器执行任何操作。

环绕至少有一个好处,它是被许多类似于rust的语言所使用的方法:C / C ++,C#,go,可能是D,当然还有更多,

真的吗至少在C和C ++中不是,它们具有与Rust相同的未定义行为。 这不是巧合,我们使用的LLVM主要是为实现C和C ++的clang而构建的。 您确定要使用C#并继续吗?

C11标准https://port70.net/~nsz/c/c11/n1570.html#6.3.1.4

当实数浮点型的有限值转换为_Bool以外的整数类型时,小数部分将被丢弃(即,该值将被截断为零)。 如果整数部分的值不能用整数类型表示,则行为是不确定的。

当将实数浮点型的值转换为无符号类型时,无需执行将整数类型的值转换为无符号类型时执行的余数运算。 因此,可移植实际浮点值的范围是(-1,Utype_MAX + 1)。

C ++ 17标准http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf#section.7.10

浮点类型的prvalue可以转换为整数类型的prvalue。 转换将截断;即,小数部分将被丢弃。 如果无法在目标类型中表示截断的值,则该行为未定义。

C#参考https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/builtin-types/numeric-conversions

当您将double或float值转换为整数类型时,此值将四舍五入为最接近的整数值。 如果结果积分值超出目标类型的范围,则结果取决于溢出检查上下文。 在检查的上下文中,抛出OverflowException,而在未检查的上下文中,结果是目标类型的未指定值。

因此,它不是UB,而只是“未指定的值”。

@admilazz整数溢出与整数溢出之间存在巨大差异:整数溢出是不可取的,定义明确。 浮点强制转换是未定义的行为

您要的内容类似于在发布模式下关闭Vec边界检查,但这将是错误的,因为这将导致未定义的行为。

即使只在发布模式下发生,也不允许在安全代码中允许未定义的行为。 因此,任何修复都必须适用于发布和调试模式。

当然,在调试模式下可以有一个限制性更强的修复程序,但是对于发布模式的修复程序必须仍然定义良好。

@admilazz整数溢出与整数溢出之间存在巨大差异:整数溢出是不可取的,但定义明确。 浮点强制转换是未定义的行为。

可以,但是此线程与定义行为有关。 如果将其定义为产生“目标类型的未指定值”(如Amanieu在上面帮助引用的C#规范中那样),那么它将不再是未定义的(以任何危险的方式)。 在实际程序中,您不能轻易利用整数溢出的明确定义的性质,因为它在调试版本中仍然会感到恐慌。 同样,无效的强制转换版本生成所产生的值也不必是可预测的或特别有用的,因为如果在调试版本中出现恐慌,程序实际上将无法利用它。 实际上,这为编译器提供了最大的优化空间,而选择诸如饱和度的行为会限制编译器,并且在没有本机饱和转换指令的情况下,在硬件上的运行速度可能会大大降低。 (而且饱和度显然不是正确的。)

您要的内容类似于在发布模式下关闭Vec边界检查,但这是错误的,因为这将导致未定义的行为。 不允许以安全代码进行未定义的行为...

并非所有未定义的行为都是相似的。 未定义的行为仅表示由编译器实现者决定会发生什么。 只要没有办法通过将浮点数强制转换为int来违反生锈的安全保证,那么我认为这并不类似于允许人们写入任意内存位置。 但是,我当然同意,即使不是必然可预测的,也应该在保证安全的意义上进行定义。

真的吗至少在C和C ++中不这样,它们具有与Rust相同的未定义行为...您确定要使用C#并继续吗?

很公平。 我没有阅读他们所有的规格。 我刚刚测试了各种编译器。 没错,说“我尝试过的所有编译器都这样做”与说“语言规范将其定义为这样”不同。 但是无论如何,我并不主张支持溢出,只是指出它似乎是最常见的。 我实际上是在赞成采用以下转换:1)防止“错误”的结果,例如1000000.0变成65535或16960,其原因与我们防止整数溢出的原因相同-它很可能是一个错误,因此用户必须选择使用它和2)允许在发行版本中发挥最佳性能。

并非所有未定义的行为都是相似的。 未定义的行为仅表示由编译器实现者决定会发生什么。 只要没有办法通过将浮点数强制转换为int来违反生锈的安全保证,那么我认为这并不类似于允许人们写入任意内存位置。 尽管如此,我当然同意应该对其进行定义:已定义,但不一定是可预测的。

未定义的行为意味着优化器(由专注于C和C ++的LLVM开发人员提供)可以自由地假设它永远不会发生,并根据该假设对代码进行转换,包括删除只能通过未定义的强制转换才能到达的代码块或者,正如这个例子显示,假设一个任务必须已经调用,即使它实际上不是,因为在调用调用代码,而无需先调用它是未定义的行为。

即使合理的证明,构成不同的优化过程不会产生危险的紧急行为,LLVM的开发商不会做任何有意识的努力来保持这一点。

我认为所有未定义的行为在此基础上都是相似的。

即使有理由证明组成不同的优化遍历不会产生危险的紧急行为,LLVM开发人员也不会做出任何有意识的努力来保持这种状态。

好吧,不幸的是LLVM以此方式影响了rust的设计,但是我确实阅读了一些LLVM指令参考,并且提到了上面提到的“冻结”操作(“ ...另一种是等待LLVM添加冻结信息概念…”),以防止在LLVM级别发生未定义的行为。 锈是否与LLVM的旧版本有关? 如果没有,我们可以使用它。 但是,他们的文档尚不清楚确切的行为。

如果参数是undef或有毒,则“ freeze”返回一个任意但固定的类型“ ty”的值。 否则,此指令为空操作,并返回输入参数。 保证由同一“冻结”指令返回的所有值的使用始终观察到相同的值,而不同的“冻结”指令可能会产生不同的值。

我不知道“固定值”或“相同的“冻结”指令”的含义。 我认为理想情况下,它将编译为无操作并给出不可预测的整数,但是听起来它可能会做一些昂贵的事情。 有人尝试过这种冻结操作吗?

好吧,不幸的是,LLVM以此方式影响了锈的设计

LLVM开发人员不仅编写优化程序。 就是说,即使rustc开发人员编写了优化程序,但由于链接优化程序的新兴特性,未定义调情本质上是一个巨大的步枪。 当所讨论的舍入是通过链接优化过程建立的紧急行为时,人脑根本没有进化为“暗示舍入误差的潜在幅度”。

我不会在那里反对你的。 :-)我确实希望此LLVM“冻结”指令提供一种零成本的方法来避免这种未定义的行为。

上面已经讨论过了,结论是,虽然“先浇铸后冻结”是定义的行为,但它根本不是合理的行为。 在发布模式下,此类强制转换将为超出范围的输入返回任意结果(使用完全安全的代码)。 对于看起来像as这样无辜的东西,这不是一个好的语义。

IMO这样的语义将是我们宁愿避免的糟糕的语言设计。

我的立场是,无论此问题如何,都最好完全避免as ,因为根据输入和输出类型的不同,它的语义可能截然不同(截断,饱和,舍入等),而当输入和输出类型不同时,它们并不总是很明显阅读代码。 (甚至可以用foo as _推断出后者。)因此,我有一份RFC草案,用于提议各种显式命名的转换方法,这些方法涵盖了as如今(甚至可能更多)的情况。 。

我完成了那个草稿! https://internals.rust-lang.org/t/pre-rfc-add-explicitly-named-numeric-conversion-apis/11395

任何反馈都非常欢迎,但请在内部线程中而不是在此处提供反馈。

在发布模式下,此类强制转换将为超出范围的输入返回任意结果(使用完全安全的代码)。 对于看起来很无辜的东西来说,这不是一个好的语义。

很抱歉重复我自己,但我认为这个论点适用于整数溢出。 如果将一些数字相乘并且结果溢出,那么您将得到一个非常错误的结果,该结果几乎肯定会使您尝试执行的计算无效,但是在调试版本中会出现恐慌,因此很可能会捕获该错误。 我会说,给出错误结果的数字转换也应该惊慌,因为它很有可能代表用户代码中的错误。 (已经解决了典型的浮点错误的情况。如果计算产生65535.3,将其转换为u16已经有效。要进行越界转换,通常需要在代码中添加一个错误,如果我有一个错误,错误,我想得到通知,以便我进行修复。)

发布版本的功能可以为无效转换提供任意但已定义的结果,也可以实现最佳性能,我认为这对于数字转换等基本功能非常重要。 始终饱和会严重影响性能,隐藏错误,并且很少进行意外地得出正确结果的计算。

很抱歉重复我自己,但我认为这个论点适用于整数溢出。 如果将一些数字相乘并且结果溢出,那么您将得到一个非常错误的结果,几乎肯定会使您尝试执行的计算无效

但是,我们不是在谈论乘法,而是在强制转换。 是的,整数溢出也是如此:从整数到整数的转换即使在溢出时也不会惊慌。 这是因为as是设计使然,从不惊慌,甚至在调试版本中也是如此。 对于浮点强制转换,偏离这一点充其量是令人惊讶的,而在最坏的情况下则是危险的,因为不安全代码的正确性和安全性可能取决于某些操作而不是惊慌失措。

如果您要争辩as的设计有缺陷,因为它在类型之间提供了可靠的转换,而这种转换并不总是可能的,那么我想我们大多数人都会同意。 但这完全超出了此线程的范围,这是关于as casts现有框架内修复float-to-int转换。 这些必须是绝对可靠的,甚至在调试版本中也不要惊慌。 因此,请提出一些合理的(不涉及freeze ),浮点到整数强制转换的非恐慌语义,或者尝试重新开始有关重新设计as以允许恐慌的讨论当强制转换是有损的(并且对于int-to-int和float-to-int强制转换始终如此)时,但是后者在此问题中不合时宜,因此请打开一个新线程(RFC之前的样式)为了那个原因。

怎么样,我们仅通过实施启动freeze语义现在修复UB,然后我们可以在世界上所有的时间,以什么语义我们真正想要的同意,因为我们选择的任何语义将与向后兼容freeze语义。

我们如何通过仅实现freeze语义_now_来修复UB来开始,然后我们就可以一直在世界各地就我们真正想要的语义达成共识,因为我们选择的任何语义都将与freeze向后兼容

  1. 恐慌与冻结不向后兼容,因此我们至少需要拒绝所有涉及恐慌的提议。 从UB转移到恐慌似乎不兼容,尽管如上所述,还有其他一些原因不会使as陷入恐慌。
  2. 就像我以前写的
    >我们支持多个较旧的版本(目前回溯至LLVM 6),并且我们需要一些后备实现才能真正摆脱所有用户的UB。

我同意@RalfJung的观点,非常不希望让一些as恐慌,但是除了我不认为@admilazz提出的这一点显然是正确的:

(已经解决了典型的浮点错误的情况。如果计算产生65535.3,将其转换为u16已经有效。要进行越界转换,通常需要在代码中添加一个错误,如果我有一个错误,错误,我想得到通知,以便我进行修复。)

对于f32-> u16,可能确实确实需要非常大的舍入误差才能从舍入误差中退出u16范围,但是对于从f32到32位整数的转换,显然不是这样。 i32::MAX在f32中不能精确表示,最接近的可表示数字是i32::MAX 47位。 因此,如果您的计算应在数学上得出不超过i32::MAX ,则任何从零开始的误差> = 1 ULP都会使您超出范围。 一旦考虑到精度较低的浮点数(IEEE 754 binary16或非标准bfloat16),情况就会变得更加糟糕。

我们不是在谈论乘法,而是在强制转换

好吧,浮点数到整数的转换几乎只在与乘法相同的上下文中使用:数值计算,而且我确实认为整数溢出的行为与并行有用。

是的,这同样适用于整数溢出:int-to-int强制转换永远也不会惊慌,即使它们溢出...对于浮点强制转换,偏离此值充其量也是令人惊讶的,最坏的情况是危险的,因为不安全代码的正确性和安全性可能取决于某些操作而不会惊慌。

我认为这里的不一致是由通常的做法证明的,并不令人惊讶。 使用移位,掩码和强制转换截断和切分整数(有效地将强制转换用作按位AND加上大小变化的一种形式)非常普遍,并且在系统编程中具有悠久的历史。 至少我每周要做几次。 但是在过去的30多年中,我想不起来曾经期望将NaN,Infinity或超出范围的浮点值转换为整数而获得合理的结果。 (我记得的每个实例都是产生该值的计算中的错误。)因此,我认为整数->整数强制转换和浮点数->整数强制转换的情况必须相同。 也就是说,我可以理解,一些决定已经确定下来。

请…为float-to-int强制转换提出一些合理的(不涉及冻结的)非恐慌语义

好吧,我的建议是:

  1. 不要使用会影响语义的重大更改的全局编译开关。 (我假设-Zsaturating-float-casts是命令行参数或类似参数。)依赖于饱和行为的代码,例如,如果不进行编译,将被破坏。 可能在同一项目中无法将期望不同的代码混合在一起。 应该有某种本地计算方法来指定所需的语义,可能类似于此pre-RFC
  2. 默认情况下,使as强制转换具有最佳性能,这是强制转换所期望的。

    • 我认为应该通过冻结支持它的LLVM版本以及不支持的LLVM版本上的任何其他转换语义(例如,截断,饱和等)来完成此操作。 我预计“冻结可能会泄漏敏感内存中的值”的说法纯属假设。 (或者,如果y = freeze(fptosi(x))仅使y保持不变,从而泄漏了未初始化的内存,则可以通过先清除y 。)

    • 如果默认情况下as相对较慢(例如因为它饱和),请提供某种方式来获得最佳性能(例如,一种方法-必要时不安全-使用冻结)。

  1. 不要使用会影响语义的重大更改的全局编译开关。 (我假设-Zsaturating-float-casts是命令行参数或类似参数。)

明确地说,我认为没有人会不同意。 仅当将库更新为修复这些回归时,才建议将该标志作为一种短期工具来更轻松地衡量和解决性能回归。

对于f32-> u16,可能确实确实需要非常大的舍入误差才能从舍入误差中退出u16范围,但是对于从f32到32位整数的转换,显然不是这样。 i32 :: MAX在f32中无法精确表示,最接近的可表示数字是i32 :: MAX的47位。 因此,如果您的计算应在数学上得出最大为i32 :: MAX的数字,则任何误差> = 1 ULP(从零开始)都将超出范围

这有点偏离主题了,但假设您有一个假设的算法,该算法应该在数学上产生高达2 ^ 31-1的f32(但应不产生2 ^ 31或更高的值,除非可能由于舍入误差)。 它似乎已经存在缺陷。

  1. 我认为最接近的可表示i32实际上比i32 :: MAX低127,因此即使在没有浮点不精确度的完美世界中,您期望产生的值高达2 ^ 31-1的算法实际上也只能产生(合法)值,最高达2 ^ 31-128。 也许那已经是一个错误了。 当该数字无法表示时,我不确定谈论从2 ^ 31-1测得的错误是否有意义。 您必须与最接近的可表示数字相差64(考虑到四舍五入)才能越界。 当然,当您接近2 ^ 32时,这并不是百分比。
  2. 当最接近的可表示值相隔128个时,您不应期望区分相隔1的值(即2 ^ 31-1而不是2 ^ 31)。 而且,只有3.5%的i32可表示为f32(而<32%的u32)。 使用f32时,您无法获得这种范围的精度。 该算法听起来像是在使用错误的工具来完成工作。

我想任何执行您描述的实用算法都将以某种方式紧密地与整数绑定。 例如,如果将随机i32转换为f32然后又返回,则如果i32 :: MAX-64以上,它可能会失败。 但这会严重降低您的精度,我不知道您为什么会这样做。 几乎所有输出i32完整范围的i32-> f32-> i32计算都可以使用整数数学运算来更快更准确地表示,如果没有,则为f64。

无论如何,虽然我确定有可能发现某些情况下执行越界转换的算法会因饱和而固定,但我认为它们很少见-十分罕见,我们不应该放慢所有转换的速度以适应它们。 而且,我认为此类算法可能仍然存在缺陷,应予以修复。 而且,如果算法不能固定,它总是可以在可能的越界转换之前进行边界检查(或调用饱和转换函数)。 这样,仅在需要时才支付限制结果的费用。

PS祝大家感恩节快乐。

明确地说,我认为没有人会不同意。 该标志仅被建议作为一种短期工具。

我主要是指用-Zunsaturated-float-casts替换-Zsaturated-float-casts的建议。 即使饱和度成为默认值,诸如-Zunsaturated-float-casts之类的标志对于兼容性也似乎是不好的,但是如果它也打算是临时的,那么没关系。 :-)

无论如何,我敢肯定,每个人都希望我对这个问题已经说够了-包括我自己在内。 我知道Rust团队传统上一直试图提供多种处理方式,以便人们可以在性能和安全性之间做出自己的选择。 我已经分享了我的观点,并相信你们最终会提出一个好的解决方案。 照顾自己!

我假设-Zunsaturated-float-casts只会暂时存在,并会在某个时候被删除。 至少这是一个-Z选项(仅在Nightly可用),而不是-C

就其价值而言,饱和度和UB不是唯一的选择。 另一种可能性是更改LLVM以添加fptosi变体,该变体使用CPU的本机溢出行为–即,溢出行为在各个体系结构之间均不可移植,但是在任何给定体系结构上都可以很好地定义(例如在x86上返回0x80000000),它将永远不会返回带毒或未初始化的内存。 即使默认值变得饱和,最好将其作为选项。 毕竟,虽然饱和转换在不是默认行为的体系结构上具有固有的开销,但是“执行CPU的操作”只有在抑制某些特定的编译器优化时才具有开销。 我不确定,但是我怀疑通过将float-to-int溢出视为UB启用的任何优化都是利基的,不适用于大多数代码。

就是说,一个问题可能是架构是否具有多个浮点到整数指令,这些指令在溢出时返回不同的值。 在这种情况下,编译器选择一个或另一个将影响可观察到的行为,这本身不是问题,但是如果复制单个fptosi且两个副本的行为不同,则可能会成为一个问题。 但是我不确定这种流行是否确实存在于任何流行的架构上。 同样的问题也适用于其他浮点优化,包括浮点收缩...

const fn (miri)从Rust 1.26开始就已经选择了饱和广播行为(假设我们希望CTFE和RTFE结果保持一致)(在1.26之前,溢出的编译时强制返回0)

const fn g(a: f32) -> i32 {
    a as i32
}

const Q: i32 = g(1e+12);

fn main() {
    println!("{}", Q); // always 2147483647
    println!("{}", g(1e+12)); // unspecified value, but always 2147483647 in miri
}

Miri / CTFE使用apfloat的to_u128 / to_i128方法进行转换。 但是我不确定这是否是一个稳定的保证-特别是考虑到它以前似乎已经发生了变化(在Miri中实现这些功能时我们并不知道)。

我认为我们可以根据最终的代码生成来调整。 但是LLVM的apfloat(Rust版本是直接端口)使用饱和度这一事实可以很好地表明这是某种“合理的默认值”。

一种可观察到的行为的解决方案是在编译器或生成的二进制文件生成时随机选择一种可用方法。
然后为需要特定行为的用户提供诸如a.saturating_cast::<i32>()类的功能。

@ dns2utf8

“随机”一词将与获得可复制的版本的努力背道而驰,并且,如果在编译器版本中可以预测到这一点,您就会知道有人会决定不改变它。

IMO @comex所描述的(对于该线程IIRC来说不是新颖的,所有旧的东西又是新的),如果我们不希望饱和,则这是次佳选择。 注意,我们甚至不需要任何LLVM更改就可以测试出来,我们可以使用内联asm(在存在此类指令的体系结构上)。

就是说,一个问题可能是架构是否具有多个浮点到整数指令,这些指令在溢出时返回不同的值。 在这种情况下,选择一个或另一个编译器将影响可观察到的行为,这本身不是问题,但是如果复制单个fptosi并且两个副本的行为不同,则可能会成为一个问题。

freeze相比,IMO这样的不确定性将几乎放弃所有实际优势。 如果这样做,我们应该为确定性而为每个体系结构选择一条指令,并坚持使用它,以便确定性,以便程序在对它们有意义时可以实际依赖指令的行为。 如果在某些架构上无法做到这一点,那么我们可以退回到软件实现上(但是正如您所说的,这完全是假设的)。

如果我们不将此决定委托给LLVM而是使用内联asm来实现,这是最简单的。 顺便说一句,这比更改LLVM在每个后端添加新的内在函数并降低它们要容易得多。

@rkruppe

[...]与更改LLVM来添加新的内在函数并在每个后端降低它们相比,顺便说一下,这还容易得多。

此外,LLVM对具有依赖于目标的语义的内在函数并不十分满意:

但是,如果您希望强制转换定义正确,则应该定义其行为。 “做一些快速的事情”并不是一个真正的定义,我不认为我们应该给予与目标无关的构造与目标有关的行为。

https://groups.google.com/forum/m/#!msg/llvm -dev / cgDFaBmCnDQ / CZAIMj4IBAA

我打算将#10184重新标记为T-lang:我认为要解决的问题是float as int意味着什么的语义选择

(即我们是否愿意让它具有惊慌的语义,是否愿意让它具有基于freeze的规格不足,等等)

至少在最初的讨论中,这是针对T-lang团队而非T-compiler的问题

只是遇到了这个问题,即使在不重新编译的情况下,它们也可以在运行之间产生无法再现的结果。 在这种情况下, as运算符似乎从内存中获取了一些垃圾。

我建议完全禁止将as用作“ float as int”,而应使用特定的舍入方法。 推理: as对于其他类型没有损失。

推理:对其他类型而言也不是有损的。

是吗?

基于Rust Book,我可以假设它仅在某些情况下(即在为类型Y定义From<X>情况下)无损,即可以将u8u32使用From ,但不是相反。

所谓“非有损”,是指铸造足够小的值以适应需求。 示例: 1_u64 as u8不是有损的,因此u8 as u64 as u8不是有损的。 对于浮点数,没有简单的“拟合”定义,因为20000000000000000000000000000_u128 as f32没有损失,而20000001_u32 as f32没有损失,因此float as intint as float都是无损失的。

256u64 as u8虽然有损。

但是<anything>_u8 as u64 as u8不是。

我认为有损失是正常现象,并且对演员表是有期望的,这不是问题。 使用强制转换(例如u32 as u8 )截断整数是一种常见的操作,其含义众所周知,它在我所知道的所有类似C的语言中都是一致的(至少在使用二进制补码整数表示的体系结构上如此)这些天基本上都是这些)。 有效的浮点转换(即,整数部分适合目标位置)也具有易于理解和一致认可的语义。 1.6 as u32是有损的,但是我所知道的所有类似C的语言都同意结果应为1。这两种情况都超出了硬件制造商之间关于这些转换应如何工作以及C约定的共识。强制转换的类语言应该是高性能的“我知道我在做什么”类型的运算符。

因此,我不认为我们应该以无效的浮点转换相同的方式来考虑这些问题,因为在类似C的语言或硬件中,它们没有任何约定的语义(但它们通常会导致错误状态)或硬件异常),并且(根据我的经验)几乎总是指示错误,因此通常在正确的代码中不存在。

只是遇到了这个问题,即使没有重新编译,其结果在两次运行之间也是无法再现的。 在这种情况下, as运算符似乎从内存中获取了一些垃圾。

我个人认为这很好,只要它仅在转换无效时发生,并且除了产生垃圾值之外没有任何副作用。 如果您确实需要一段代码中的其他无效转换,则可以使用您认为应该具有的任何语义自己处理无效情况。

而且除了产生垃圾值外没有任何副作用

副作用是,垃圾值起源于内存中的某个位置,并且揭示了一些(可能是敏感的)数据。 仅从float本身返回“随机”值是可以的,但当前的行为不是。

有效的浮点转换(即,整数部分适合目标位置)也具有易于理解和一致认可的语义。

是否有浮点到整数转换的用例没有显式trunc()round()floor()ceil()附带? 当前的as舍入策略是“未定义的”,这使得as几乎不能用于非舍入的数字。 我相信,在大多数情况下,写x as u32实际上想要x.round() as u32

我认为有损失是正常现象,并且对演员表是有期望的,这不是问题。

我同意,但前提是损失很容易预测。 对于整数,有损转换的条件显而易见。 对于浮子,它们是晦涩的。 即使它们是圆形的,它们对于一些非常大的数也是无损的,但是对于一些较小的数字却是有损的。 我的个人偏爱是针对有损和无损转换使用两个不同的运算符,以避免错误地引入有损转换,但是我也可以只用一个运算符,只要我能知道它是否有损即可。

副作用是,垃圾值起源于内存中的某个位置,并且揭示了一些(可能是敏感的)数据。

我希望它只会使目的地保持不变或其他任何方式,但是如果确实存在问题,可以先将其清零。

是否有没有用显式trunc(),round(),floor()或ceil()伴随的浮点到整数转换的用例? 当前的as舍入策略是“未定义”的,几乎不能用于非舍入的数字。

如果舍入策略确实未定义,那会让我感到惊讶,并且我同意除非您已经给它一个整数,否则运算符几乎没有用。 我希望它截断为零。

我认为在大多数情况下,写x as u32实际上想要x.round() as u32

我想这取决于域,但我希望x.trunc() as u32也很普遍。

我同意,但前提是损失很容易预测。

我绝对同意。 例如,不应该将1.6 as u32变成1还是2。

https://doc.rust-lang.org/nightly/reference/expressions/operator-expr.html#type -cast-expressions

从浮点数转换为整数将使浮点数趋近于零
注意:如果四舍五入的值不能由目标整数类型表示,当前这将导致未定义的行为。 这包括Inf和NaN。 这是一个错误,将得到修复。

注释链接在这里。

“适合”的值的舍入是明确定义的,这与此问题无关。 该线程已经很长了,最好不要将其用于推测与已建立和记录的事实相切的地方。 谢谢。

剩下要决定的是在以下情况下如何定义f as $Int

  • f.trunc() > $Int::MAX (包括正无穷大)
  • f.trunc() < $Int::MIN (包括负无穷大)
  • f.is_nan()

在Nightly中已经实现并可以使用-Z saturating-casts编译器标志的一个选项是定义它们分别返回: $Int::MAX$Int::MIN和零。 但是仍然有可能选择其他行为。

我的观点是,该行为绝对应该是确定性的,并返回一些整数值(例如,而不是恐慌),但是确切的值并不重要,关心这些情况的用户应该使用我单独建议的转换方法添加: https :

我想这取决于域,但是我希望x.trunc() as u32也很普遍。

正确。 通常, x.anything() as u32 ,最有可能是round() ,但也可能是trunc()floor()ceil() 。 仅x as u32而不指定具体的舍入过程很可能是一个错误。

我的观点是,该行为绝对应该是确定性的,并返回一些整数值(例如,而不是恐慌),但确切的值并不是太重要

即使使用“未定义”值,我个人也可以,只要它不依赖于任何东西,而是浮点数即可,而且最重要的是,不会暴露任何不相关的寄存器和内存内容。

在Nightly中已经实现并可以使用-Z saturating-casts编译器标志的一个选项是定义它们分别返回: $Int::MAX$Int::MIN和零。 但是仍然有可能选择其他行为。

我希望获得f.trunc() > $Int::MAXf.trunc() < $Int::MIN的行为与将浮点数虚数转换为无限大小的整数然后返回其最低有效位相同(如整数类型的转换)。 从技术上讲,这将是根据指数的有效位数向左移动的一些位(对于正数,负数需要根据两者的补数求逆)。

因此,例如,我希望很大的数字可以转换为0

定义无限和NaN转换成什么似乎更难/更随意。

@CryZe所以如果我没看错的话,那可以匹配-Z saturating-casts (以及Miri已经实现的)?

@RalfJung是的。

太好了,我将https://github.com/WebAssembly/testsuite/blob/master/conversions.wast (用指定的结果替换陷阱)复制到Miri的测试套件中。 :)

@RalfJung请更新到conversions.wast的最新版本,该版本刚刚更新为包括对新的饱和转换运算符的测试。 新的运算符名称中带有“ _sat”,并且它们没有陷阱,因此您无需替换任何内容。

@sunfishcode感谢您的更新! 无论如何,我都必须将测试转换为Rust,所以我仍然需要替换很多东西。 ;)

_sat测试的测试值是否有所不同? (编辑:这里有一条评论说这些值是相同的。)对于Rust饱和的转换,我采用了许多这些值并将它们添加到https://github.com/rust-lang/miri/pull/1321中。 我懒得为所有人做这件事...但是我认为这意味着更新的文件现在没有任何更改。

对于UB内在函数,我认为在wasi中,wasm侧的陷阱应成为编译失败测试。

输入值都相同,唯一的区别是_sat运算符在输入上具有预期的输出值,而陷阱运算符具有预期的陷阱。

https://github.com/rust-lang/miri/pull/1321中添加了对Miri(以及Rust CTFE引擎)的测试rustc -Zmir-opt-level=0 -Zsaturating-float-casts也通过了该文件中的测试。
我现在还在Miri中实现了未经检查的内在函数,请参见https://github.com/rust-lang/miri/pull/1325。

我已经发布了https://github.com/rust-lang/rust/pull/71269#issuecomment -615537137,据我所知,该文档记录了当前状态,并且PR还可以稳定-Z标志的行为。

考虑到这个话题的长度,我想如果人们觉得我在评论中没有任何内容,我可以直接向PR进行评论,或者,如果它很小,可以随意在Zulip或Discord(模拟)上对我进行ping操作,我可以进行修复以避免PR线程上不必要的干扰。

我希望语言团队中的某人可能会很快在该PR上启动FCP提案,并将其合并将自动解决此问题:)

有检查转换的计划吗? 像fn i32::checked_from(f64) -> Result<i32, DoesntFit>

您需要考虑i32::checked_from(4.5)返回什么。

此页面是否有帮助?
0 / 5 - 0 等级