我们应该使用u32
/ i32
或它的低级变体(u8
/ i8
,u16
/ i16
)在处理有限范围的数字时,例如"月内的情况"其范围为1-30或"主题得分"范围从0到100?或者为什么我们不应该?
较低的变体是否有任何优化或好处(即内存效率)?
答案 0 :(得分:6)
正确性应该优先于性能和正确性(对于像1-100这样的范围),所有解决方案(u8
,u32
,......)都同样糟糕。最好的解决方案是创建一个新类型,以便从强类型中受益。
我的答案的其余部分试图证明这种说法是正确的,并讨论了创建新类型的不同方法。
让我们来看看“主题得分”的例子:唯一的合法值是0-100。我认为正确性,使用u8
和u32
同样糟糕:在这两种情况下,您的变量都可以在您的语义上下文中保存不合法的值;那很糟!
并且认为u8
更好,因为有更少的非法价值观,就像争辩说摔跤熊比走过纽约更好,因为你只有一种可能性死亡(熊的失血)与纽约的死亡(车祸,刀袭,溺水......)的许多可能性相反。
所以我们想要的是一种保证只保留合法价值的类型。我们想要创建一个完全符合这一要求的新类型。但是,有多种方法可以继续;各有利弊。
struct ScoreOfSubject(pub u8);
优势:至少API更容易理解,因为参数已经由类型解释。更容易理解的是:
add_record("peter", 75, 47)
或 add_record("peter", StudentId(75), ScoreOfSubject(47))
?我会说后者; - )
缺点:我们实际上没有进行任何范围检查,仍然可能发生非法值;糟糕!
struct ScoreOfSubject(pub u8);
impl ScoreOfSubject {
pub fn new(value: u8) -> Self {
assert!(value <= 100);
ScoreOfSubject(value)
}
pub fn get(&self) -> u8 { self.0 }
}
优势:我们使用非常少的代码强制执行合法值,是的:)
缺点:使用该类型可能很烦人。几乎每个操作都需要程序员打包和安装。解包价值。
(代码会impl Add<_>
,impl Display
等等)
优势:程序员可以直接使用该类型并对其执行所有有用的操作 - 进行范围检查!这非常理想。
请看看Matthieu M.的评论:
[...]通常将分数相乘或除以它们不会产生分数!强类型不仅强制执行有效值,还强制执行有效操作,因此您实际上不会将两个分数分开以获得另一个分数。
我认为这是一个非常重要的一点,我以前没有说清楚。强类型可以防止程序员对值执行非法操作(没有任何意义的操作)。一个很好的例子是crate cgmath
,它区分了point和direction向量,因为它们都支持不同的操作。您可以找到其他解释here。
缺点:很多代码:(
幸运的是,Rust宏/编译器插件系统可以减少这个缺点。有像newtype_derive
或bounded_integer
这样的包装箱为你做这种代码生成(免责声明:我从来没有使用它们)。
但是现在你说:“你不能认真吗?我应该花时间写新类型吗?”。
不一定,但是如果你正在处理生产代码(==至少有些重要),那么我的回答是:是的,你应该。
答案 1 :(得分:3)
无答案答案:我怀疑你会发现基准测试有任何不同,除非你很多算术或过程巨大数组。
你可能应该选择更有意义的类型(没有理由使用底片或者每月有数百万的上限)并提供你需要的方法(例如你不能执行{ {1}}直接在无符号整数上。)
答案 2 :(得分:1)
使用较小的类型可能会有很大的好处,但您必须在目标平台上对应用程序进行基准测试才能确定。
较低内存占用的第一个也是最容易实现的好处是更好的缓存。您的数据不仅更有可能适合缓存,而且还不太可能丢弃缓存中的其他数据,从而可能改善应用程序的完全不同的部分。是否触发此操作取决于应用程序接触的内存和顺序。做基准吧!
使用较小的类型,网络数据传输有明显的好处。
较小的数据允许“更大”的指令。 128位SIMD单元可以处理4个32位数据或16个8位数据,使某些操作速度提高4倍。在基准测试中,我使这些指令的执行速度确实提高了4倍,但整个应用程序的改进提高了不到1%,并且代码变得更加混乱。将程序简化为更好地使用SIMD可能会非常棘手。
截至签名/未签名讨论时,unsigned的属性稍好一些,编译器可能会或可能不会利用这些属性。