为了确定我是否可以/应该使用 Rust 而不是默认的 C/C++,我正在研究各种边缘情况,主要是考虑到这个问题:在 0.1% 确实重要的情况下,我总能得到编译器输出和 gcc 一样好(带有适当的优化标志)?答案很可能是否定的,但让我们看看...
在Reddit上有一个相当特殊的例子,它研究了无分支排序算法的子例程的编译器输出。
这是基准 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 程序员可能会对这种调查感到畏缩,但它是……