Struct Memory Layout in Rust

654 阅读6分钟

引言

最近在学习Rust时,对它的对象在内存中是如何布局的很感兴趣;可能由于之前的语言是C++的缘故,我个人对所写的对象到底在内存中时如何存放的有一种执念,而在探索的过程中也收获了不少,因此用一篇随笔记录下来

C++中的结构体布局

先看一个C++语言的struct:

struct MyStruct {
  int first;    // 4
  int second;   // 4
  double third; // 8
  int fourth;   // 4
};

打印出该struct的布局结构:

image.png

可以看到该结构体的大小是24,怎么得来的呢?

int + int + double + int = 20,然后再加上4字节的padding,共计24bytes

这里简单的说一下,4字节的padding是如何计算的:

先单纯看这个结构体,结构体中的字段是需要对齐的,如果这个结构体的大小只有20bytes,那么只要保证该struct的对象的起始地址是8的倍数,就可以保证这个四个字段的对齐;但是,考虑如下的声明:

MyStruct arr[4];

假设该数组的内存起始地址是0,那么arr[1]对应的结构体中的third 字段的起始地址就成了28,但是28并不能满足double类型的对齐要求,离28最近的8的倍数是32,因此MyStruct的结构体末尾会有4个字节的padding,来保证它形成的数组中的每个结构体中的每个字段都能满足对齐要求

Rust中的结构体

Rust中的结构体可以声明它的表示方法:

  • Default
  • C

Rust结构体的C representaion

先看看Rust中的结构体可以指明采用C的表示方法:

#[repr(C)]
struct MyStruct {
    first: i32,
    second: i32,
    third: i64,
    fourth: i32,
}

首先看一下这个结构体的大小:

fn main() {
    use std::mem;
    println!("size: {}, align: {}", std::mem::size_of::<MyStruct>(), std::mem::align_of::<MyStruct>());
}
// output 
// size: 24, align: 8

再看一下Rust是如何布局这个结构体的:

// rustc -Z print-type-sizes src/main.rs

...
print-type-size type: `MyStruct`: 24 bytes, alignment: 8 bytes
print-type-size     field `.first`: 4 bytes
print-type-size     field `.second`: 4 bytes
print-type-size     field `.third`: 8 bytes
print-type-size     field `.fourth`: 4 bytes
print-type-size     end padding: 4 bytes
...

可以看到在采用C表示方法的情况下,得到的大小和上面C++得到的一样,这也符合预期;另外,编译器也同样在该结构体的末尾进行了4个字节的填充,理由跟上面C++中提到的一样

Rust结构体Default representaion

首先,还是来看一下这个结构体的大小:

struct MyStruct {
    first: i32,
    second: i32,
    third: i64,
    fourth: i32,
}

fn main() {
    use std::mem;
    println!("size: {}, align: {}", std::mem::size_of::<MyStruct>(), std::mem::align_of::<MyStruct>());
}

// output 
// size: 24, align: 8

这个结果和C representation得到的结果相同啊,这只是表面现象,再打印出该结构体的布局:

// rustc -Z print-type-sizes src/main.rs

...
print-type-size type: `MyStruct`: 24 bytes, alignment: 8 bytes
print-type-size     **field `.third`: 8 bytes**
print-type-size     field `.first`: 4 bytes
print-type-size     field `.second`: 4 bytes
print-type-size     field `.fourth`: 4 bytes
print-type-size     end padding: 4 bytes
...

注意看,和C表示法有什么不同:third字段被提升到了该结构体的最上面

在Rust的官方文档《The Rust Reference》一文中,在提到Rust中Struct的默认表示法下,有这么一句话:

There are no guarantees of data layout made by this representation.

这句话的意思是:如果采用Rust默认的表示法,Struct的layout是没有保证的,这个没有保证指的是取决于编译器,编译器可以作任何它认为是对的优化

而对于MyStruct来说,在Rust的默认表示形式下,编译器做出了优化动作:把MyStruct中的最大对齐要求的字段放到了结构体的最上面

这样的优化有什么意义呢,来看一个明显的例子:

#[repr(C)]
struct MyStruct1 {
    a: u8,
    b: u16,
    c: u8
}

struct MyStruct2 {
    a: u8,
    b: u16,
    c: u8
}
fn main() {
    use std::mem;
    println!("size: {}, align: {}", std::mem::size_of::<MyStruct1>(), std::mem::align_of::<MyStruct1>());
    println!("size: {}, align: {}", std::mem::size_of::<MyStruct2>(), std::mem::align_of::<MyStruct2>());
}

// output
// MyStruct1: size: 6, align: 2
// MyStruct2: size: 4, align: 2

同样的字段,同样的Struct,MyStruct2的大小只有4,打印出这两个Struct的布局:

...
print-type-size type: `MyStruct1`: 6 bytes, alignment: 2 bytes
print-type-size     field `.a`: 1 bytes
print-type-size     padding: 1 bytes
print-type-size     field `.b`: 2 bytes, alignment: 2 bytes
print-type-size     field `.c`: 1 bytes
print-type-size     end padding: 1 bytes
print-type-size type: `MyStruct2`: 4 bytes, alignment: 2 bytes
print-type-size     field `.b`: 2 bytes
print-type-size     field `.a`: 1 bytes
print-type-size     field `.c`: 1 bytes
...

可以看到,通过把最大对齐要求的字段进行上移,可以省去两个字节的padding

编译器的优化策略

至此,我对于Rust编译器采用的优化策略产生了兴趣,除了把最大对齐要求的字段上移,还有什么别的策略吗?

我去查看了下Rust编译器对于布局这一块的源码(虽然99.99%并不能看懂),但是还是可以发现一点蛛丝马迹:

fn univariant_uninterned(
        &self,
        ty: Ty<'tcx>,
        fields: &[TyAndLayout<'_>],
        repr: &ReprOptions,
        kind: StructKind,
    ) -> Result<Layout, LayoutError<'tcx>> {
        // ...
        if optimize {
	          // ...
            // Otherwise we just leave things alone and actually optimize the type's fields
            } else {
                match kind {
                    StructKind::AlwaysSized | StructKind::MaybeUnsized => {
                        optimizing.sort_by_key(|&x| {
                            // Place ZSTs first to avoid "interesting offsets",
                            // especially with only one or two non-ZST fields.
                            let f = &fields[x as usize];
                            (!f.is_zst(), cmp::Reverse(field_align(f)))
                        });
                    }

                    StructKind::Prefixed(..) => {
                        // Sort in ascending alignment so that the layout stays optimal
                        // regardless of the prefix
                        optimizing.sort_by_key(|&x| field_align(&fields[x as usize]));
                    }
                }

                // FIXME(Kixiron): We can always shuffle fields within a given alignment class
                //                 regardless of the status of `-Z randomize-layout`
            }
        }
				// ...
    }

但是Rust的Default representation的优化策略,目前似乎也只有按照字段对齐要求从大到小排序这一条了

缘起何处

我好奇为什么Rust的编译器这么做了,于是在google上进行了搜索,相关资料并不是很多,但也不是没有:

  1. camlorn.net/posts/April 2017/rust-struct-field-reordering/

    这个是写Rust编译器的大佬的blog,写的就是Struct Size Optimize相关的思路,非常值得学习

  2. github.com/rust-lang/r…

    这个就是上面那位大佬的PR

基本上看完这两个,对Rust编译器此处的优化策略大概明白了一些

总结

总结就是——自己太菜,而要学的东西还很多,不管是C++还是Rust中的Struct,都是不一个简简单单的语法,只是编译器平时都帮我们多做了一些活;

虽然上面的知识可能就算完全不知道,依然可以写出优秀的业务代码,但是学计算机嘛,“勿在浮沙筑高台”,多了解了解底层的东西,对自己的成长是大有裨益的。

Reference

  1. doc.rust-lang.org/reference/t…
  2. github.com/rust-lang/r…
  3. camlorn.net/posts/April 2017/rust-struct-field-reordering/
  4. github.com/rust-lang/r…