我有u8
的向量,我想将其解释为u32
的向量。假设字节顺序正确。我不想在转换后分配新内存和复制字节。我得到以下工作:
use std::mem;
fn reinterpret(mut v: Vec<u8>) -> Option<Vec<u32>> {
let v_len = v.len();
v.shrink_to_fit();
if v_len % 4 != 0 {
None
} else {
let v_cap = v.capacity();
let v_ptr = v.as_mut_ptr();
println!("{:?}|{:?}|{:?}", v_len, v_cap, v_ptr);
let v_reinterpret = unsafe { Vec::from_raw_parts(v_ptr as *mut u32, v_len / 4, v_cap / 4) };
println!("{:?}|{:?}|{:?}",
v_reinterpret.len(),
v_reinterpret.capacity(),
v_reinterpret.as_ptr());
println!("{:?}", v_reinterpret);
println!("{:?}", v); // v is still alive, but is same as rebuilt
mem::forget(v);
Some(v_reinterpret)
}
}
fn main() {
let mut v: Vec<u8> = vec![1, 1, 1, 1, 1, 1, 1, 1];
let test = reinterpret(v);
println!("{:?}", test);
}
然而,这里存在一个明显的问题。来自the shrink_to_fit
documentation:
它会尽可能地下降到长度,但是分配器仍然可以通知向量有更多元素的空间。
这是否意味着在调用u32
后,我的容量可能仍然不是shrink_to_fit
的倍数?如果在from_raw_parts
中我将v_len/4
的容量设置为v.capacity()
而不是4的精确倍数,我是否会泄漏这1-3个字节,或者由于{而将它们返回到内存池中{1}}上的{1}}
我在这里忽略了其他问题吗?
我认为移动mem::forget
进行重新解释可以保证从那时起无法访问,因此v
调用之后只有一位所有者。
答案 0 :(得分:2)
这是一个古老的问题,似乎在注释中有一个可行的解决方案。我刚刚写了这里到底出了什么问题,以及一些可能在当今的Rust中创建/使用的解决方案。
Vec::from_raw_parts
是不安全的函数,因此,您必须满足其不变式,或者调用undefined behavior。
引用the documentation for Vec::from_raw_parts
:
ptr
需要事先通过String / Vec分配(至少,如果不是,则很可能是不正确的)。T
必须具有与分配ptr相同的大小和对齐方式。 (严格的对齐方式是不够的,对齐方式实际上必须等于满足内存必须以相同布局分配和释放的解除分配要求。)- 长度必须小于或等于容量。
- 容量必须是分配指针的容量。
因此,要回答您的问题,如果capacity
不等于原始vec的容量,那么您已经打破了这个不变式。这会给您带来不确定的行为。
请注意,尽管size_of::<T>() * capacity
上也没有要求,但这使我们进入了下一个主题。
我在这里还有其他问题吗?
三件事。
首先,编写的函数无视from_raw_parts
的另一个要求。具体来说,T
的对齐方式大小必须与原始T
的对齐方式相同。 u32
的大小是u8
的四倍,因此这再次违反了这一要求。即使capacity*size
保持不变,size
也不会,capacity
也不会。该功能永远不会听起来像实现。
第二,即使以上所有条件均有效,您也将忽略对齐方式。 u32
必须与4字节边界对齐,而Vec<u8>
仅保证与1字节边界对齐。
关于OP的评论中提到:
我认为在x86_64上,未对准会降低性能
值得注意的是,尽管这在机器语言中可能是正确的,但对于Rust却不是。 The rust reference explicitly states“对齐n的值只能存储在n的倍数的地址上。”这是一个硬性要求。
Vec::from_raw_parts
似乎很严格,这是有原因的。在Rust中,分配器API不仅在分配大小上运行,而且在Layout
上运行,memalloc
是大小,事物数量和单个元素的对齐方式的组合。在带有Layout
的C中,所有分配器可以依靠的是大小相同,并且有一些最小对齐方式。不过,在Rust中,允许依赖整个Vec
,如果不依赖则调用未定义的行为。
因此,为了正确地分配内存,Vec<u32>
需要知道分配内存的确切类型。通过将Vec<u8>
转换为Vec::from_raw_parts
,它不再知道此信息,因此它不再能够正确地重新分配此内存。
&[u32]
的严格性来自于它需要释放内存的事实。如果我们创建借用切片&[u8]
,则不再需要处理它!将&[u32]
转换为[u8]
时没有能力,所以我们应该都很好,对吧?
好吧,差不多。您仍然必须处理对齐问题。 Primitives are generally aligned to their size,因此只能保证[u32]
对齐1字节边界,而[u32]
必须对齐4字节边界。
但是,如果您愿意,并创建一个pub unsafe fn align_to<U>(&self) -> (&[T], &[U], &[T])
,则有一个功能-<[T]>::align_to
:
u8
这将修剪所有未对齐的开始和结束值,然后在新类型的中间给您一个切片。这是不安全的,但您唯一需要满足的不变条件是中间切片中的元素有效。
听起来可以将4个u32
值重新解释为Vec
值,所以我们很好。
将所有内容放在一起,原始功能的声音版本将如下所示。这是根据借来的值而不是拥有的值进行的,但是考虑到在任何情况下重新解释拥有的use std::mem;
fn reinterpret(v: &[u8]) -> Option<&[u32]> {
let (trimmed_front, u32s, trimmed_back) = unsafe { v.align_to::<u32>() };
if trimmed_front.is_empty() && trimmed_back.is_empty() {
Some(u32s)
} else {
// either alignment % 4 != 0 or len % 4 != 0, so we can't do this op
None
}
}
fn main() {
let mut v: Vec<u8> = vec![1, 1, 1, 1, 1, 1, 1, 1];
let test = reinterpret(&v);
println!("{:?}", test);
}
都是即时未定义的行为,我认为可以肯定地说这是最接近的声音函数:
align_to
请注意,也可以使用std::slice::from_raw_parts
而不是align_to
来完成。但是,这需要手动处理对齐方式,而它真正提供的只是确保我们做得正确的更多事情。好吧,它与旧编译器的兼容性-Vec<u32>
于2018年在Rust 1.30.0中引入,问这个问题时就不存在了。
如果您确实需要u8
来进行长期数据存储,我认为最好的选择是分配新的内存。无论如何,旧内存已分配给fn reinterpret(v: &[u8]) -> Option<Vec<u32>> {
let v_len = v.len();
if v_len % 4 != 0 {
None
} else {
let result = v
.chunks_exact(4)
.map(|chunk: &[u8]| -> u32 {
let chunk: [u8; 4] = chunk.try_into().unwrap();
let value = u32::from_ne_bytes(chunk);
value
})
.collect();
Some(result)
}
}
,并且无法正常工作。
这可以通过一些功能性编程变得相当简单:
4
首先,我们使用<[T]>::chunks_exact
遍历u8
&[u8]
s的块。接下来,try_into
从[u8; 4]
转换为&[u8]
。 u32
的长度保证为4,因此永远不会失败。
我们使用u32::from_ne_bytes
使用本地字节序将字节转换为Vec<u32>
。如果与网络协议交互或在磁盘上进行序列化,则最好使用from_be_bytes
或from_le_bytes
。最后,我们collect
将结果转换为{{1}}。
最后一点,一个真正通用的解决方案可能同时使用这两种技术。如果我们将返回类型更改为Cow<'_, [u32]>
,则可以返回对齐的借用数据(如果可以的话),如果不能,则分配一个新数组!并非两全其美,而是接近。