如何安全地将 Vec<f64> 重新解释为一半大小的 Vec<num_complex::Complex<f64>>?

10
我有一些复数数据,由外部C库(不希望更改)填充到一个 Vec<f64> 中,形式为 [i_0_real, i_0_imag, i_1_real, i_1_imag, ...],该 Vec<f64> 似乎具有与 Vec<num_complex::Complex<f64>> 相同的内存布局,而后者长度减半,因为 num_complex::Complex<f64> 的数据结构与 [f64; 2] 兼容,并在此处文档中记录。我想这样使用它,而不需要重新分配可能很大的缓冲区。

我假设可以在 std::vec::Vec 中使用 from_raw_parts() 来模拟一个新的 Vec,它接管旧 Vec 的内存(通过遗忘旧的 Vec),并使用 size / 2capacity / 2,但这需要不安全的代码。是否有“安全”的方法来执行这种数据重新解释?

Vec 在 Rust 中被分配为 Vec<f64>,并通过使用 .as_mut_ptr() 的 C 函数来填充 Vec<f64>

我当前编译的不安全实现:

extern crate num_complex;

pub fn convert_to_complex_unsafe(mut buffer: Vec<f64>) -> Vec<num_complex::Complex<f64>> {
    let new_vec = unsafe {
        Vec::from_raw_parts(
            buffer.as_mut_ptr() as *mut num_complex::Complex<f64>,
            buffer.len() / 2,
            buffer.capacity() / 2,
        )
    };
    std::mem::forget(buffer);
    return new_vec;
}

fn main() {
    println!(
        "Converted vector: {:?}",
        convert_to_complex_unsafe(vec![3.0, 4.0, 5.0, 6.0])
    );
}

在C语言中,f64不是一个复数。 - Stargateur
1个回答

20
有没有一种“安全”的方法来进行这种数据重新解释?
没有。至少,这是因为你需要知道的信息不是通过Rust类型系统表达的,而是通过散文(即文档)表达的:
Complex 与数组 [T;2] 的内存布局兼容。
- Complex docs 如果一个 Vec 分配了内存,则其指针指向按顺序初始化的 len 个连续元素(如果将其强制转换为 slice,则会看到这些元素)。
- Vec docs 数组可以强制转换为 slice ([T])。
- Array docs 由于 Complex 与数组兼容,数组的数据与 slice 兼容,Vec 的数据与 slice 兼容,因此这种转换应该是安全的,尽管编译器无法确定这一点。
此信息应附加(通过注释)到您的不安全块中。

我会对你的函数进行一些小的调整:

  • 同时拥有两个指向相同数据的Vec让我感到非常紧张。可以通过引入一些变量并在创建另一个之前忘记一个来轻松避免这种情况。

  • 删除return关键字以更符合惯用法

  • 添加一些断言,以确保数据的起始长度是二的倍数。

  • 正如rodrigo所指出的, 容量很容易成为奇数。为了尝试避免这种情况,我们调用 shrink_to_fit。这样做的缺点是根据实现,Vec可能需要重新分配和复制内存。

  • 扩展unsafe块以涵盖所有必需的相关代码,以确保安全不变量得到维护。

pub fn convert_to_complex(mut buffer: Vec<f64>) -> Vec<num_complex::Complex<f64>> {
    // This is where I'd put the rationale for why this `unsafe` block
    // upholds the guarantees that I must ensure. Too bad I
    // copy-and-pasted from Stack Overflow without reading this comment!
    unsafe {
        buffer.shrink_to_fit();

        let ptr = buffer.as_mut_ptr() as *mut num_complex::Complex<f64>;
        let len = buffer.len();
        let cap = buffer.capacity();

        assert!(len % 2 == 0);
        assert!(cap % 2 == 0);

        std::mem::forget(buffer);

        Vec::from_raw_parts(ptr, len / 2, cap / 2)
    }
}

为了避免对容量的担忧,您可以将一个切片转换为Vec。这也不需要额外的内存分配。这样做更简单,因为我们可以“丢弃”任何奇数的尾随值,因为Vec仍然保留它们。
pub fn convert_to_complex(buffer: &[f64]) -> &[num_complex::Complex<f64>] {
    // This is where I'd put the rationale for why this `unsafe` block
    // upholds the guarantees that I must ensure. Too bad I
    // copy-and-pasted from Stack Overflow without reading this comment!
    unsafe {
        let ptr = buffer.as_ptr() as *const num_complex::Complex<f64>;
        let len = buffer.len();

        assert!(len % 2 == 0);
        
        std::slice::from_raw_parts(ptr, len / 2)
    }
}

1
希望我能再为这段代码的评论点赞。 - Michael Anderson
1
让我有点紧张的是 assert!(cap % 2 == 0);,因为容量通常不由用户管理,它可能无意中变成奇数并导致程序崩溃。我更喜欢一个函数,例如 as_complex_unsafe(buffer: &[f64] -> &[Complex<f64>],这样就不涉及容量问题了。 - rodrigo
@rodrigo 很好的观点。我跳过了关于“Vec”的切片答案,因为OP明确要求了,但你说得对,应该包括在内。同时更新以处理奇数容量。 - Shepmaster
所有关于奇数容量的好点子 - 我已经为 &[f64]&mut [f64] 构建了并行实现,也许最好只允许这些转换。 - jv-dev
问一下 @Shepmaster,你不想让两个指向同一内存的 Vec 存在的原因是因为如果在其他地方发生终止/恐慌,如果执行在 std :: mem :: forget() 之前,从该线程展开可能会导致双重释放的风险吗? - jv-dev
在这种特定情况下,是的,@jv-dev。原始代码不可能存在这个问题,因为在创建和“忘记”之间没有代码,但随着时间的推移,稍微重构一下就很容易出现这种情况。 - Shepmaster

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