即使在有限范围的数字情况下,是否建议使用`u32`/`i32`?

10
我们在处理范围有限的数字时,比如“一个月的天数”(1-30)或“科目分数”(0-100),应该使用u32/ i32还是其低版本(u8/i8, u16/i16)?如果使用低版本,是否有任何优化或好处(例如更节省内存)?

如果没有充分的理由,我更喜欢最小化尺寸,但如果它们只是本地变量,差别不会太大。 - Veedrac
1
(离题:pH 应该是 f32/f64 类型,且其值可以小于 0 或大于 14。) - kennytm
@kennytm 你说得对!我会编辑它。但是这也适用于 f 哦 :D - Abdillah
3个回答

10

总结

正确性应该优先于性能,并且针对像1-100这样的范围,在正确性方面,所有解决方案(u8u32等)都同样不好。最好的解决方案是创建一个新类型以受益于强类型

我的回答的其余部分试图证明这一主张并讨论创建新类型的不同方式。

更多说明

让我们看一下“学科成绩”的例子:唯一合法的值为0-100。我认为,在正确性方面,使用u8u32同样糟糕:在这两种情况下,您的变量可以包含语义上合法的值;那是不好的!

而且,认为u8更好,因为非法值更少,就像认为与熊搏斗比步行穿过纽约更好,因为你只有一种死亡可能性(被熊攻击导致失血),而不是在纽约有很多种死亡可能性(汽车事故,刀刃攻击,溺水等)。

所以,我们想要的是一个类型,保证只包含合法值。我们想要创建一种新类型,正好可以做到这一点。然而,有多种方法可供选择;每种方法都有不同的优缺点。


(A)公开内部值

struct ScoreOfSubject(pub u8);
优点:至少API更容易理解,因为参数已经被类型解释。哪个更容易理解:

  • add_record("peter", 75, 47) 还是
  • add_record("peter", StudentId(75), ScoreOfSubject(47))

我会说后者更好理解。

缺点:我们实际上并没有进行任何范围检查,非法值仍然可能发生;不好!


(B) 使内部值私有并提供一个范围检查的构造函数

struct ScoreOfSubject(pub u8);

impl ScoreOfSubject {
    pub fn new(value: u8) -> Self {
        assert!(value <= 100);
        ScoreOfSubject(value)
    }
    pub fn get(&self) -> u8 { self.0 }
}
优点: 我们通过很少的代码来实施合法值,是的 :)

缺点: 和类型打交道可能很烦人。几乎每个操作都需要程序员对值进行打包和解包。


(C) 添加大量实现 (除了(B)之外)

(代码会 impl Add<_>, impl Display等等)

优点: 程序员可以直接使用类型并执行所有有用的操作 -- 带有范围检查! 这是相当优化的。

请看 Matthieu M. 的评论:

[...] 通常将分数相乘或相除不会产生分数! 强类型不仅强制有效值,而且还强制执行有效操作,这样您就不会真正地将两个分数相除以获得另一个分数。

我认为这是我之前未能清楚表达的非常重要的一点。强类型防止程序员在值上执行非法操作(不合理的操作)。一个很好的例子是箱子cgmath,它区分点和方向向量,因为两者都支持不同的操作。您可以在这里找到更多的解释。

缺点: 很多代码 :(

幸运的是,Rust 宏/编译器插件系统可以减少缺点。有像newtype_derivebounded_integer这样的箱子为您生成此类代码 (免责声明: 我从未使用过它们)。


但现在你说: "你不会认真的吧? 我应该花时间写新类型吗?"。

不一定,但如果您正在处理生产代码(== 至少有些重要),那么我的答案是: 是的,你应该


4
引入新类型加1分。至于套话,我想指出一般情况下将分数相乘或相除并不会产生评分!强类型不仅强制使用有效值,还强制使用有效的操作,这样您实际上不会将两个得分除以另一个得分来获得另一个得分。 - Matthieu M.
@MatthieuM。非常好的观点!我编辑了我的答案,将其包括进去。 - Lukas Kalbertodt

4
一个无声的答案: 我怀疑你在基准测试中不会看到任何差异,除非你进行了大量算术运算或处理海量数字数组。
你应该选择更有意义的类型(没有理由在一个月的某一天使用负数或上限为百万),并提供你需要的方法(例如,你无法直接对无符号整数执行abs())。

1

使用较小的类型可能会带来重大好处,但您需要在目标平台上对应用程序进行基准测试以确保。

较小的内存占用量带来的第一个且最容易实现的好处是更好的缓存。不仅您的数据更有可能适合缓存,而且还不太可能丢弃缓存中的其他数据,从而可能改善应用程序的完全不同部分。这是否触发取决于应用程序接触的内存及其顺序。做基准测试!

网络数据传输从使用较小的类型中获得明显优势。

较小的数据允许“更大”的指令。128位SIMD单元可以处理4个32位数据或16个8位数据,使某些操作快4倍。在我的基准测试中,这些指令确实执行得快4倍,但整个应用程序的性能提高不到1%,而且代码变得更混乱。塑造程序以更好地利用SIMD可能很棘手。

就有符号/无符号讨论而言,无符号具有略微更好的属性,编译器可能会或可能不会利用它们。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接