为什么零大小的类型在某些情况下会导致实际分配?

Dan*_*ver 5 pointers rust

我正在使用零大小类型(ZST),因为我很好奇它们实际上是如何在后台实现的。鉴于ZST不需要内存中的任何空间,并且采用原始指针是一种安全的操作,因此我很感兴趣,我将从不同种类的ZST“分配”中获得什么原始指针,以及结果的奇怪程度(对于安全的Rust)。

我的第一个尝试(test_stk.rs)是使用const指针指向ZST的一些堆栈实例:

struct Empty;
struct EmptyAgain;

fn main() {
    let stk_ptr: *const Empty = &Empty;
    let stk_ptr_again: *const EmptyAgain = &EmptyAgain;
    let nested_stk_ptr = nested_stk();

    println!("Pointer to on-stack Empty:        {:?}", stk_ptr);
    println!("Pointer to on-stack EmptyAgain:   {:?}", stk_ptr_again);
    println!("Pointer to Empty in nested frame: {:?}", nested_stk_ptr);
}

fn nested_stk() -> *const Empty {
    &Empty
}
Run Code Online (Sandbox Code Playgroud)

编译并运行此程序将产生以下结果:

$ rustc test_stk.rs -o test_stk
$ ./test_stk 
Pointer to on-stack Empty:        0x55ab86fc6000
Pointer to on-stack EmptyAgain:   0x55ab86fc6000
Pointer to Empty in nested frame: 0x55ab86fc6000
Run Code Online (Sandbox Code Playgroud)

对进程内存映射的简短分析表明,0x55ab86fc6000它实际上不是堆栈分配,而是本.rodata节的开头。这似乎是合乎逻辑的:编译器假装每个ZST都有一个零大小的值(在编译时已知),并且每个值都驻留在中.rodata,就像编译时常量一样。

第二次尝试是使用带框的ZST(test_box.rs):

struct Empty;
struct EmptyAgain;

fn main() {
    let ptr = Box::into_raw(Box::new(Empty));
    let ptr_again = Box::into_raw(Box::new(EmptyAgain));
    let nested_ptr = nested_box();

    println!("Pointer to boxed Empty:                 {:?}", ptr);
    println!("Pointer to boxed EmptyAgain:            {:?}", ptr_again);
    println!("Pointer to boxed Empty in nested frame: {:?}", nested_ptr);
}

fn nested_box() -> *mut Empty {
    Box::into_raw(Box::new(Empty))
}
Run Code Online (Sandbox Code Playgroud)

运行此代码段可以:

$ rustc test_box.rs -o test_box
$ ./test_box 
Pointer to boxed Empty:                 0x1
Pointer to boxed EmptyAgain:            0x1
Pointer to boxed Empty in nested frame: 0x1
Run Code Online (Sandbox Code Playgroud)

快速调试表明,这是ZST(Rust的liballoc/alloc.rs)分配器的工作方式:

unsafe fn exchange_malloc(size: usize, align: usize) -> *mut u8 {
    if size == 0 {
        align as *mut u8
    } else {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

最小可能的对齐方式是1(根据Nomicon),因此对于ZST,box操作员会调用exchange_malloc(0, 1),结果地址为0x1

注意到into_raw()返回了可变指针后,我决定使用可变指针(test_stk_mut.rs)重试之前的测试(堆栈):

struct Empty;
struct EmptyAgain;

fn main() {
    let stk_ptr: *mut Empty = &mut Empty;
    let stk_ptr_again: *mut EmptyAgain = &mut EmptyAgain;
    let nested_stk_ptr = nested_stk();

    println!("Pointer to on-stack Empty:        {:?}", stk_ptr);
    println!("Pointer to on-stack EmptyAgain:   {:?}", stk_ptr_again);
    println!("Pointer to Empty in nested frame: {:?}", nested_stk_ptr);
}

fn nested_stk() -> *mut Empty {
    &mut Empty
}
Run Code Online (Sandbox Code Playgroud)

运行此命令后,将显示以下内容:

$ rustc test_stk_mut.rs -o test_stk_mut
$ ./test_stk_mut 
Pointer to on-stack Empty:        0x7ffc3817b5e0
Pointer to on-stack EmptyAgain:   0x7ffc3817b5f0
Pointer to Empty in nested frame: 0x7ffc3817b580
Run Code Online (Sandbox Code Playgroud)

原来,这一次我有真正的堆栈分配值,每个都有自己的地址!当我尝试顺序地声明它们(test_stk_seq.rs)时,我发现这些值每个都占用了八个字节:

struct Empty;

fn main() {
    let mut stk1 = Empty;
    // <4 times more>

    let stk_ptr1: *mut Empty = &mut stk1;
    // <4 times more>

    println!("Pointer to on-stack Empty:        {:?}", stk_ptr1);
    // <4 times more>
}
Run Code Online (Sandbox Code Playgroud)

跑:

$ rustc test_stk_seq.rs -o test_stk_seq
$ ./test_stk_seq 
Pointer to on-stack Empty:        0x7ffdba303840
Pointer to on-stack Empty:        0x7ffdba303848
Pointer to on-stack Empty:        0x7ffdba303850
Pointer to on-stack Empty:        0x7ffdba303858
Pointer to on-stack Empty:        0x7ffdba303860
Run Code Online (Sandbox Code Playgroud)

因此,以下是我无法理解的事情:

  1. 为什么装箱的ZST分配使用哑0x1地址而不是更有意义的地址(如“堆栈”值)?

  2. 当存在指向它们的可变原始指针时,为什么需要为堆栈ZST值分配实际空间?

  3. 为什么恰好八个字节用于可变的堆栈分配?我应该将此大小视为“实际类型大小的0字节+对齐的8字节”吗?

Cha*_*man 3

重要提示:请记住,以下几乎没有一个是可以保证的。这就是现在的运作方式。


为什么盒装 ZST 分配使用哑0x1地址而不是更有意义的地址,例如“堆栈上”值的情况?

没有地址对 ZST 有意义。编译器只使用最简单的方法。特别是,可变指针的堆栈地址和.rodata共享地址都不是 ZST 的特殊内容,而是任何类型的通用属性,我将在稍后解释。相反,Box需要专门处理ZST。它通过最简单的方法来做到这一点 - 返回第一个可能的虚假地址。

当存在指向堆栈上 ZST 值的可变原始指针时,为什么需要为它们分配实际空间?

问题不是为什么我们需要为 ZST 分配真正的堆栈空间,问题是为什么不。每个变量和临时变量都在堆栈上分配。没有理由对 ZST 进行特殊处理。

如果您会问,“但我看到它们分配在共享引用中.rodata!”,请尝试以下操作:

struct Empty;
struct EmptyAgain;

fn main() {
    let empty = Empty;
    let empty_again = EmptyAgain;
    let stk_ptr: *const Empty = &empty;
    let stk_ptr_again: *const EmptyAgain = &empty_again;
    let nested_stk_ptr = nested_stk();

    println!("Pointer to on-stack Empty:        {:?}", stk_ptr);
    println!("Pointer to on-stack EmptyAgain:   {:?}", stk_ptr_again);
    println!("Pointer to Empty in nested frame: {:?}", nested_stk_ptr);
}

fn nested_stk() -> *const Empty {
    let empty = Empty;
    &empty
}
Run Code Online (Sandbox Code Playgroud)

您可以看到它们是在堆栈上分配的。

如果您会问“但是,当在同一个语句(let stk_ptr = &Empty;)中获取地址时,它会给出一个用于共享引用的地址.rodata和一个在堆栈上用于可变的地址!” 答案是可变情况是正常情况,而共享引用由于静态提升而属于特殊情况。这意味着与正常情况相反,具有可变引用和函数调用等,其中如下:

let v1 = &mut Foo;

let v2 = &foo();
Run Code Online (Sandbox Code Playgroud)

翻译为:

let mut __v1_storage = Foo;
let v1 = &mut __v1_storage;

let __v2_storage = foo();
let v2 = &__v2_storage;
Run Code Online (Sandbox Code Playgroud)

对于某些表达式,特别是结构文字,翻译是不同的:

let v = &Foo { ... };

// Translated into:

static __V_STORAGE: Foo = Foo { ... };
let v = &__V_STORAGE;
Run Code Online (Sandbox Code Playgroud)

作为s,它是否static存储在、ZST 中。.rodata

为什么恰好八个字节用于可变堆栈分配?我应该将此大小视为“实际类型大小的 0 字节 + 对齐的 8 字节”吗?

更像是“1 个字节的实际大小 + 7 个字节的对齐填充”。但在 Rust 中,ZST 的大小(显然)为零,(默认)对齐方式为 1,那么这里会发生什么呢?

好吧,rustc 将 ZST 降低为空的 LLVM 结构 ( %Empty = type { })。LLVM 中的结构体使用指定对齐方式的最大值(在处理它们的指令中)和目标的首选对齐方式。x86-64 的首选对齐方式是 8 字节,因此max(1, 8) = 8.

关于大小,LLVM 不处理零大小的堆栈分配。当一个空结构体被alloca分配时,LLVM 将其四舍五入到大小 1。因此,我们得到的大小为 1,对齐方式为 8,我们填充对齐方式的倍数 - 每个分配 8 个字节。

如果你尝试使用eg,struct Empty(u8);或者struct Empty(u8, u8);你会看到它分别使用1或2的堆栈空间,而不是8。这是因为这些结构(ScalarScalarPair布局,因为它们在rustc中被称为)不表示为LLVM结构,而是表示为LLVM 原语:i8{ i8, i8 }. 这些不使用首选对齐方式。但如果您使用三个字段,您将看到它也是 8 字节宽:

struct Empty(u8, u8, u8);

fn main() {
    let mut stk1 = Empty(0, 0, 0);
    let mut stk2 = Empty(0, 0, 0);
    let mut stk3 = Empty(0, 0, 0);
    let mut stk4 = Empty(0, 0, 0);
    let mut stk5 = Empty(0, 0, 0);

    let stk_ptr1: *mut Empty = &mut stk1;
    let stk_ptr2: *mut Empty = &mut stk2;
    let stk_ptr3: *mut Empty = &mut stk3;
    let stk_ptr4: *mut Empty = &mut stk4;
    let stk_ptr5: *mut Empty = &mut stk5;

    println!("Pointer to on-stack Empty: {:?}", stk_ptr1);
    println!("Pointer to on-stack Empty: {:?}", stk_ptr2);
    println!("Pointer to on-stack Empty: {:?}", stk_ptr3);
    println!("Pointer to on-stack Empty: {:?}", stk_ptr4);
    println!("Pointer to on-stack Empty: {:?}", stk_ptr5);
}
Run Code Online (Sandbox Code Playgroud)