在原地且最小化开销地将Vec<u32>转换为Vec<u8>

14

我试图将一个类型的Vec转换为类型的Vec,最好是原地进行转换而不会产生太多开销。

我的当前解决方案依赖于不安全的代码来重新构建这个Vec。有没有更好的方法来解决这个问题?使用我的解决方案会有什么风险?

use std::mem;
use std::vec::Vec;

fn main() {
    let mut vec32 = vec![1u32, 2];
    let vec8;
    unsafe {
        let length = vec32.len() * 4; // size of u8 = 4 * size of u32
        let capacity = vec32.capacity() * 4; // ^
        let mutptr = vec32.as_mut_ptr() as *mut u8;
        mem::forget(vec32); // don't run the destructor for vec32

        // construct new vec
        vec8 = Vec::from_raw_parts(mutptr, length, capacity);
    }

    println!("{:?}", vec8)
}

Rust Playground link


3
由于您没有考虑字节顺序,因此这取决于平台。 - Tim Diekmann
6
可以使用from_slice_u32函数来处理字节顺序,无需复制,如果源字节顺序与主机字节顺序相同,则该函数不执行任何操作。否则,您的代码应该是安全的。请注意,反过来不一定是安全的!(因为内存对齐问题)。 - BurntSushi5
1
相关链接:https://codereview.stackexchange.com/questions/187013/base64-string-↔-float-array/187077 - Boiethios
5个回答

20
  1. 每当编写一个unsafe块时,我强烈建议在该块上加上注释,解释为什么您认为代码实际上是安全的。这种信息对于将来阅读代码的人非常有用。

  2. 不要添加关于"魔法数字" 4 的注释,只需使用mem::size_of::<u32>。甚至为了最大的清晰度,我会使用size_of并执行除法以替代u8

  3. 你可以从unsafe块中返回新创建的Vec。

  4. 如评论中所述,像这样“转储”一段数据会使数据格式与平台相关;在小端和大端系统上会得到不同的答案。这可能会导致将来大量的调试头疼。文件格式或者将平台字节顺序编码到文件中(使读取者的工作变得更困难),或者只向文件写入特定字节顺序(使写入者的工作更困难)。

  5. 出于组织目的,我可能会将整个unsafe块移动到一个函数中,并命名该函数。

  6. 您不需要导入Vec,它在预定义模块中。

use std::mem;

fn main() {
    let mut vec32 = vec![1u32, 2];

    // I copy-pasted this code from StackOverflow without reading the answer 
    // surrounding it that told me to write a comment explaining why this code 
    // is actually safe for my own use case.
    let vec8 = unsafe {
        let ratio = mem::size_of::<u32>() / mem::size_of::<u8>();

        let length = vec32.len() * ratio;
        let capacity = vec32.capacity() * ratio;
        let ptr = vec32.as_mut_ptr() as *mut u8;

        // Don't run the destructor for vec32
        mem::forget(vec32);

        // Construct new Vec
        Vec::from_raw_parts(ptr, length, capacity)
    };

    println!("{:?}", vec8)
}

Playground

我最担心这段代码的问题在于与 Vec 关联的内存对齐。

Rust 的底层分配器分配释放具有特定的Layout内存,Layout 包含指针的大小对齐等信息。

我认为这段代码需要在成对调用 allocdealloc 时匹配Layout。如果是这种情况,从一个 Vec<u32> 构建的 Vec<u8>销毁可能会向分配器传递错误对齐信息,因为这些信息是基于元素类型的。

没有更好的方法,"最好"的做法是将 Vec<u32> 保持原样,并简单地获得一个 &[u8]。这个切片没有与分配器的交互,避免了这个问题。

即使没有与分配器的交互,你也需要对齐方式小心谨慎!

另请参见:


1
我不确定 size_of::<u32>() / size_of::<u8>()4 好多少。你必须知道这些类型的大小,才能知道不会有任何舍入误差。最好是先乘以 size_of::<u32>(),然后再除以 size_of::<u8>(),如果你写成通用形式,就必须这样做。 - Peter Hall
1
@PeterHall,您还需知道类型的大小才能确定4是正确的结果,这只是将注释转化为代码。如果常量求值进一步完善,我会将let移至const并添加compile_error!,以确保该值为4。我强烈建议不要通用写作,因此我对此情况不太担心;-) - Shepmaster
@PeterHall 如果你想要一个更有原则性的通用工具,类似于safe-transmute crate可能更适合。 - Shepmaster
1
我不知道那个板条箱!我敢肯定以前没有这么多板条箱... ;) - Peter Hall
2
Layout 的问题并不是我预料到的。在低级代码中,我习惯于担心将指针转换为具有更大对齐方式的指针,但将指针转换为具有较低对齐方式的指针始终是安全的。我想知道是否应该规定分配器应该优雅地处理这种情况。 - Matthieu M.
显示剩余3条评论

6
如果不是必须进行原地转换,可以使用类似以下内容来控制字节序并避免使用不安全的代码块:bytes order
extern crate byteorder;

use byteorder::{WriteBytesExt, BigEndian};

fn main() {
    let vec32: Vec<u32> = vec![0xaabbccdd, 2];
    let mut vec8: Vec<u8> = vec![];

    for elem in vec32 {
        vec8.write_u32::<BigEndian>(elem).unwrap();
    }

    println!("{:?}", vec8);
}

2
编译器的优化(使用-O3)会使整个for循环成为无操作吗? - SOFe

1

简单地转换Vec或使用from_raw_parts是未定义的行为,因为释放API 要求传递的Layout与分配时分配的相同。要安全地执行此类转换,您需要通过Vec的相关分配器并调用shrink将布局转换为新对齐方式,然后调用from_raw_parts。这取决于分配器能够执行原地重新分配。

如果您不需要结果向量可调整大小,则将vec的&mut [u32]借用重新解释为&mut [u8]将是一个更简单的选项。


0

这是我使用位移复制解决问题的方法。

它在我的x64机器上运行良好,但我不确定是否对小/大端做出了不安全的假设。

如果可以在不需要复制的情况下进行内存就地转换,运行时性能将更快,但我还没有想出如何实现。

/// Cast Vec<u32> to Vec<u8> without modifying underlying byte data
/// ```
/// # use fractals::services::vectors::vec_u32_to_u8;
/// assert_eq!( vec_u32_to_u8(&vec![ 0x12345678 ]), vec![ 0x12u8, 0x34u8, 0x56u8, 0x78u8 ]);
/// ```
#[allow(clippy::identity_op)]
pub fn vec_u32_to_u8(data: &Vec<u32>) -> Vec<u8> {
    // TODO: https://dev59.com/SL74oIgBc1ULPQZF7B4k
    // TODO: https://dev59.com/Tojca4cB1Zd3GeqP0riC
    let capacity = 32/8 * data.len() as usize;  // 32/8 == 4
    let mut output = Vec::<u8>::with_capacity(capacity);
    for &value in data {
        output.push((value >> 24) as u8);  // r
        output.push((value >> 16) as u8);  // g
        output.push((value >>  8) as u8);  // b
        output.push((value >>  0) as u8);  // a
    }
    output
}

-1

试试这个

let vec32: Vec<u32> = vec![1u32, 2u32];
let mut vec8: Vec<u8> = vec![];
for v in &vec32{
    for b in v.to_be_bytes(){
        vec8.push(b);
    }
}
println!("{:?}", vec32);
println!("{:?}", vec8);

游乐场


谢谢,但这不是原地操作。 - Thom Wiggers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接