Rust的数组边界检查会影响性能吗?

Jou*_*aen 13 arrays performance benchmarking rust

我来自C,我想知道Rust的边界检查会影响性能.每次访问可能需要一些额外的汇编指令,这在处理大量数据时可能会受到影响.

另一方面,处理器性能代价高昂的是内存,因此更多的算术汇编指令可能不会受到影响,但是在加载缓存行之后,顺序访问应该非常快.

有人对此进行了基准测试吗?

Vee*_*rac 31

不幸的是,边界检查的成本并不是一个简单的估计.它肯定不是"每次检查一个周期",或任何这样容易猜测的成本.它产生非零影响,但可能无关紧要.

从理论上讲,可以测量基本类型边界检查的成本,例如Vec修改Rust以禁用它们并运行大规模生态系统测试.这会给出一些经验法则,但如果不这样做,很难知道这是否会接近百分之十或十分之一的开销.

但是,有些方法可以比计时和猜测做得更好.这些经验法则主要适用于桌面级硬件; 低端硬件或针对不同利基的东西将具有不同的特征.

如果您的索引是从容器大小派生的,那么编译器很可能完全消除边界检查.此时,发布版本中边界检查的唯一成本是它间歇性地干扰优化,这可能(但通常不会)妨碍其他优化.

如果您的代码是分支的,内存访问量很大或者很难优化,并且检查的边界很容易访问,那么很有可能边界检查将主要发生在CPU的备用带宽中,并且分支预测特别有助于在这种情况下,总体成本将特别小,特别是与其余代码的成本相比.

如果您需要检查的界限是多层指针的背后,那么您将遇到内存延迟问题似乎合理的,并且会相应地受到影响.然而,CPU中的推测和预测机制也会设法掩盖这一点,这也是合理的.这非常依赖于上下文.如果您正在引用内部数据,而不是在边界检查的同时取消引用它,则此风险会放大.

如果您的边界检查是在一个不会使核心饱和的紧密算术循环中,除了阻碍其他编译器优化之外,您不可能直接损害吞吐量.但是,阻碍其他编译器优化可能是任意不好的,从无差别到防止SIMD并导致因子10减速.

如果你的边界检查是在一个确实使核心饱和的紧密算术循环中,你就会承担上述风险并且每次边界检查会有大约半个周期的直接执行惩罚.

如果您的代码足够大以强调指令缓存,那么您需要担心对代码大小的影响.这通常是适度的,但特别难以衡量运行时的影响.

Peter Cordes在评论中补充了一些观点.首先,边界检查意味着加载和存储,因此您将运行混合负载,这最有可能成为问题/重命名的瓶颈.其次,即使是并行执行的预测分支也会从预测变量中获取资源,这会导致其他分支预测更差.

这可能看起来很吓人,而且确实如此.这就是为什么在与您和您的代码相关的级别上衡量和理解您的表现非常重要的原因.

还有一种情况是,由于Rust已经"生成"并进行了边界检查,因此它已经产生了降低成本的方法,例如普遍的零成本引用,迭代器(它们吸收,但实际上不会删除,限制检查),以及一组不寻常的实用功能.如果您发现自己遇到病态病例,Rust还提供不安全的逃生舱口.

  • 更多分支(即使总是以相同方式分支)也可能对分支预测率产生很小的负面影响.他们稀释了有趣分支的分支历史.无论如何,+1很好的答案.与往常一样,性能问题的答案是"它取决于上下文":P(和目标硬件.现代x86非常适合犁沟,特别是关键路径,但低功耗CPU管道更窄.甚至Xeon Phi/Silvermont可以从边界检查中看到更多的影响.) (3认同)
  • “使核心的功能单元饱和”:通常你会限制前端解码指令并将它们提供给乱序核心的能力:“发出/重命名”吞吐量通常是管道中最窄的部分,也是瓶颈您最有可能使用混合加载、存储和 ALU 的代码。(如果您需要边界检查,则必须有一些负载和/或存储)。我猜你可能会在代码中的加载端口吞吐量上遇到瓶颈,其中有很多负载也需要边界检查,但在 L1D 缓存中命中。 (2认同)
  • 哦,这是一个有趣的观点.如果你编译过时的32位x86,那么来自数组边界的额外寄存器压力是有问题的(7 GP regs +堆栈指针).幸运的是,x86具有快速的L1D缓存(大核CPU上的每个时钟加载2个(不是Atom/Xeon Phi),因此将寄存器与内存进行比较几乎与reg,reg一样便宜(您可以将绑定复制到堆栈外部)紧密循环所以你不需要在循环内的寄存器中指向容器控制块的指针.但是在x86-64(15 GP regs + RSP)和ARM32(14 GP regs + stack)上注册压力并不重要+ PC). (2认同)