% repr(Rust)

Во-первых, все типы имеют выравнивание, указываемое в байтах. Выравнивание типа определяет в каких адресах разрешается хранить значения. Значение, имеющее выравнивание n, должно храниться по адресу кратному n. То есть выравнивание 2 означает, что его можно хранить только по четным адресам, а 1 означает, что по любым. Выравнивание всегда больше или равно 1, и всегда является степенью 2. Большинство примитивов выровнены по своему размеру, хотя это очень платформно- специфичное поведение. В частности, на x86 u64 и f64 могут быть выровнены только по 32 бита.

Размер типа всегда должен быть кратным его выравниванию. Это гарантирует, что массив типов может всегда быть проиндексирован посредством смещения на величину, кратную размера типа. Имейте ввиду, что размер и выравнивание типа могут быть неизвестны статически в случае типов динамического размера.

Rust дает вам следующие способы для размещения составных данные:

  • struct (именованные типы-произведения)
  • tuple (анонимные типы-произведения)
  • array (гомогенные типы-произведения)
  • enum (именованные типы-суммы)

Enum называется Си-подобным если ни у одного из его вариантов нет связанных данных.

Составные структуры будут иметь выравнивание, равное максимуму из выравниваний их полей. Вследствие этого Rust добавит вставки (padding), где это необходимо, чтобы гарантировать, что все поля правильно выровнены и общий размер типа соответствует этому выравниванию. Например:


#![allow(unused)]
fn main() {
struct A {
    a: u8,
    b: u32,
    c: u16,
}
}

будет выровнено по 32-бита в архитектуре, которая выравнивает эти примитивы по соответствующему размеру. Таким образом полная struct будет иметь размер, кратный 32-бит. Вероятно, все станет таким:


#![allow(unused)]
fn main() {
struct A {
    a: u8,
    _pad1: [u8; 3], // для выравнивания `b`
    b: u32,
    c: u16,
    _pad2: [u8; 2], // для того чтобы сделать размер равным 4
}
}

Для этих типов не используется косвенная адресация; все данные хранятся внутри структуры, также как было бы и в Си. За исключением массивов (которые плотно упакованы и имеют порядок), положение данных, по умолчанию, не определено в Rust. Возьмем два определения struct:


#![allow(unused)]
fn main() {
struct A {
    a: i32,
    b: u64,
}

struct B {
    a: i32,
    b: u64,
}
}

Rust гарантирует, что два экземпляра А будут иметь одинаковое расположение данных. Но Rust не гарантирует на данный момент, что экземпляр A будет иметь такой же порядок или паддинг, как и экземпляр B, хотя на практике нет никакого основания предполагать, почему он должен отличаться.

Возьмем A и B как здесь написано, кажется, что все ясно, но другие особенности Rust используют более сложные игры с расположением данных внутри языка.

Например, считаем, что есть такая struct:


#![allow(unused)]
fn main() {
struct Foo<T, U> {
    count: u16,
    data1: T,
    data2: U,
}
}

Рассмотрим мономорфизации Foo<u32, u16> и Foo<u16, u32>. Если Rust расположит поля в указанном порядке, мы ожидаем, что он набьет вставками (padding) значения в struct для того, чтобы удовлетворить требования выравнивания. Поэтому, если Rust не переопределит порядок полей, мы ожидаем, что он выработает следующее:

struct Foo<u16, u32> {
    count: u16,
    data1: u16,
    data2: u32,
}

struct Foo<u32, u16> {
    count: u16,
    _pad1: u16,
    data1: u32,
    data2: u16,
    _pad2: u16,
}

В последнем случае место просто напрасно расходуется. Оптимальное использование места, таким образом, требует, чтобы у разных мономорфизаций менялся порядок полей.

Внимание: это гипотетическая оптимизация, которая еще не реализована в Rust 1.0

Перечисления делают этот анализ даже еще сложнее. Наивно предполагаем, что перечисление:


#![allow(unused)]
fn main() {
enum Foo {
    A(u32),
    B(u64),
    C(u8),
}
}

будет расположен так:


#![allow(unused)]
fn main() {
struct FooRepr {
    data: u64, // здесь или u64, u32 или u8 - зависит от `tag`
    tag: u8,   // 0 = A, 1 = B, 2 = C
}
}

И на самом деле почти так и будет все расположено в памяти в общем случае (размер данных и позиция tag).

Но есть случаи, в которых такое представление является неэффективным. Классический вариант - "оптимизация нулевого указателя" Rust: перечисления, состоящие из одного варианта (например, None) и (возможно вложенного) варианта с ненулевым указателем (например, &T) делают тэг необязательным, потому что значение нулевого указателя может безопасно означать, что выбран первый вариант (в данном примере, None). Конечный результат - например, будет таким size_of::<Option<&T>>() == size_of::<&T>().

Много типов в Rust, которые являются или содержат ненулевые указатели, такие как Box<T>, Vec<T>, String, &T и &mut T. По аналогии, можно представить, как вложенные перечисления объединяют свои тэги в одно значение, так как по определению известно, что у них ограниченный набор правильных значений. В принципе перечисления могут использовать довольно сложные алгоритмы для размещения битов всех вложенных типов в специальном ограничивающем их представлении. Поэтому сейчас нам кажется, что оставить нетронутым расположение перечислений в памяти, будет особенно целесообразно.