为了确定我是否可以/应该使用 Rust 而不是默认的 C/C++,我正在研究各种边缘情况,主要是考虑以下问题:在 0.1% 的情况下,当它确实很重要时,我是否总是能够获得与 gcc 相同的编译器输出(使用适当的优化标志)?答案很可能是否定的,但让我们看看...
Reddit 上有一个相当奇特的例子,研究了无分支排序算法的子程序的编译器输出。
这是基准 C 代码:
这是基准 C 代码:
#include <stdint.h>
#include <stdlib.h>
int32_t* foo(int32_t* elements, int32_t* buffer, int32_t pivot)
{
size_t buffer_index = 0;
for (size_t i = 0; i < 64; ++i) {
buffer[buffer_index] = (int32_t)i;
buffer_index += (size_t)(elements[i] < pivot);
}
}
以下是与编译器输出相关的godbolt链接。
Rust的第一次尝试如下所示:
pub fn foo0(elements: &Vec<i32>, mut buffer: [i32; 64], pivot: i32) -> () {
let mut buffer_index: usize = 0;
for i in 0..buffer.len() {
buffer[buffer_index] = i as i32;
buffer_index += (elements[i] < pivot) as usize;
}
}
这里有很多边界检查,详见godbolt。
下一步尝试消除第一个边界检查:
pub unsafe fn foo1(elements: &Vec<i32>, mut buffer: [i32; 64], pivot: i32) -> () {
let mut buffer_index: usize = 0;
for i in 0..buffer.len() {
unsafe {
buffer[buffer_index] = i as i32;
buffer_index += (elements.get_unchecked(i) < &pivot) as usize;
}
}
}
这有所改善了(请看与前面相同的godbolt链接)。
最后,让我们尝试完全删除边界检查:
use std::ptr;
pub unsafe fn foo2(elements: &Vec<i32>, mut buffer: [i32; 64], pivot: i32) -> () {
let mut buffer_index: usize = 0;
unsafe {
for i in 0..buffer.len() {
ptr::replace(&mut buffer[buffer_index], i as i32);
buffer_index += (elements.get_unchecked(i) < &pivot) as usize;
}
}
}
这会产生与foo1
相同的输出,因此ptr::replace
仍会执行边界检查。在这里,使用那些unsafe
操作是超出我的能力范围的。这引出了我的两个问题:
- 如何消除边界检查?
- 分析这种边缘情况甚至有意义吗?还是说如果呈现整个算法而不仅仅是其中一小部分,Rust编译器会看透这一点。
关于最后一点,我很好奇,通常是否可以将 Rust 精简到与 C 一样“直接”,即更接近底层。经验丰富的 Rust 程序员可能会对这种调查方式感到不适,但它确实存在...
for (i, elt) in elements.iter().enumerate().take(buffer.len())
来取消对elements
的边界检查,然后使用elt
代替elements[i]
(playground)。 - Jmb