Как создать тип Rust, который может содержать либо целое число, либо указатель размером с одно слово?

(Я имею в виду упаковку как способ различать целые числа и указатели во время выполнения. В методе использовались некоторые языки программирования, чтобы помочь GC, например OCaml. Не тип Rust Box<T>.)

У меня есть перечисление Rust, которое выглядит так:

#[derive(Clone, Copy, Debug, PartialEq)]
enum Type<'ts> {
    TVar(usize),
    Constructed(&'ts ConstructedType<'ts>),
}

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

Такие языки, как OCaml использует технику, называемую «упаковкой целых чисел», которая использует тот факт, что указатели выровнены. Это означает, что младший бит будет равен 0. Если вы сдвинете биты в своем целом числе на одну позицию влево и установите младший бит вашего целого числа в 1, вы используете этот бит в качестве тега за счет одного бита целочисленная точность.

Гарантировано ли выравнивание указателей Rust? Как бы я реализовал эту технику для своего типа в Rust?


person Calebmer    schedule 08.09.2018    source источник
comment
Если вы сместите биты в своем целом числе на один пробел влево и установите младший бит вашего целого числа равным 1, то вы используете этот бит в качестве тега за счет одного бита целочисленной точности., да, но это не цель размера использования, плюс, сделать это с адресом будет полностью определено реализацией. Это AFAIK не рекомендуется с Rust, потому что это совершенно небезопасно.   -  person Stargateur    schedule 08.09.2018
comment
Кроме того, это связано с затратами во время выполнения: даже более внимательный читатель задастся вопросом о влиянии производительности на целочисленную арифметику с использованием этого представления с тегами. Поскольку нижний бит установлен, любая операция над целым числом должна сдвинуть нижний бит вправо, чтобы восстановить исходное значение. Компилятор собственного кода OCaml в этом случае генерирует эффективный ассемблерный код x86, используя современные инструкции процессора, чтобы по возможности скрыть дополнительные сдвиги. Сложение — это одна инструкция LEA x86, вычитание может состоять из двух инструкций, а умножение — еще несколько.   -  person Stargateur    schedule 08.09.2018
comment
Но я думаю, что это возможно, если вы используете необработанный указатель и необработанное перечисление (также известное как перечисление C), но, как я уже сказал, это будет не очень переносимо, и я думаю, что если вы не работаете со встроенной системой, сохраните немного памяти, как это не стоит.   -  person Stargateur    schedule 08.09.2018
comment
Я понимаю последствия для безопасности/времени выполнения. Я хотел знать, как это можно сделать, чтобы увидеть, хорошо ли эта оптимизация работает для моей программы. Но вы, вероятно, правы в том, что я пытаюсь оптимизировать. Спасибо, что указали причину.   -  person Calebmer    schedule 08.09.2018


Ответы (1)


Возможно, я не понимаю всего, что вы говорите, но я думаю, что вам нужен union.

#[derive(Clone, Copy, Debug, PartialEq)]
enum Type<'ts> {
    TVar(usize),
    Constructed(&'ts ConstructedType<'ts>),
}

union CompactType<'ts> {
    num: usize,
    ptr: &'ts ConstructedType<'ts>
}

impl<'ts> From<CompactType<'ts>> for Type<'ts> {
    fn from(compact: CompactType<'ts>) -> Type<'ts> {
        unsafe {
            if compact.num & 1 == 1 {
                Type::TVar(compact.num >> 1)
            } else {
                Type::Constructed(compact.ptr)
            }
        }
    }
}

Обратите внимание, что доступ к членам union небезопасен, и вы должны убедиться, что все инварианты применяются. Например, вы должны явно проверить, что CompactTypes правильно созданы со значениями в пределах диапазона, и предотвратить возможность создания объектов без этих проверок.

Я бы предложил добавить в CompactType функции-конструкторы, которые возвращают Result или Option на тот случай, если вы попытаетесь использовать слишком большое число или указатель на тип, который неправильно выровнен. Когда функция TryFrom стабилизируется, вы можете использовать ее, но в это время:

enum CompactConvertError {
    NumTooBig(String),
    PtrNotAligned(String),
}

impl<'ts> Type<'ts> {
    fn to_compact(&self) -> Result<CompactType<'ts>, CompactConvertError> {
        match self {
            Type::TVar(num) => {
                if num >> (mem::size_of::<usize>() * 8 - 1) == 1 {
                    Err(CompactConvertError::NumTooBig(
                        String::from("The last bit of the usize cannot be used here"))
                    )
                } else {
                    Ok(CompactType { num: num << 1 | 1usize })
                }   
            },
            Type::Constructed(c) => {
                if mem::align_of_val(*c) % 2 == 1 {
                    Err(CompactConvertError::PtrNotAligned(
                        String::from("The pointer must be to a type with even alignment"))
                    )
                } else {
                    Ok(CompactType { ptr: c })
                }
            }
        }
    } 
}

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

Что касается выравнивания, если ConstructedType имеет размер всего 1 байт, вам нужно будет добавить к нему выравнивание, чтобы убедиться, что оно только по четному байту, иначе Rust может решить упаковать их более плотно:

#[align(2)]
struct ConstructedType<'ts> { 
    // ...
}

Определенно не добавляйте #[align(2)], если размер больше 2 байт. Возможно, кто-то еще может посоветовать, как сделать эту часть более надежной.

person Peter Hall    schedule 08.09.2018
comment
Выравнивание можно проверить с помощью std::mem::align_of(), чтобы использовать его как утверждение. - person Kornel; 08.09.2018
comment
@Kornel Ага. Но я не уверен, как ограничить выравнивание самой структуры, не выбирая фиксированное значение. - person Peter Hall; 08.09.2018
comment
Я попробовал такой подход, но столкнулся с двумя вопросами: 1) нарушаю ли я какие-либо гарантии средства проверки заимствований? Будет ли ConstructedType удален, оставив мне висячий указатель? 2) могу ли я реализовать Copy в своем союзе? Так как я переношу код из предыдущей реализации, которая сильно зависит от Copy. - person Calebmer; 08.09.2018
comment
@Calebmer Насколько я могу судить, нет необходимости что-либо делать с удалением значений, поскольку у вас нет никаких данных, кроме usize. (В любом случае союзы еще не стабилизированы для реализации Drop). - person Peter Hall; 08.09.2018
comment
Я не вижу проблемы с реализацией Copy. Что вас беспокоит? - person Peter Hall; 08.09.2018
comment
По крайней мере, когда я пробовал #[derive(Copy)], я не мог скопировать значение объединения. Это известное ограничение? - person Calebmer; 09.09.2018
comment
@Calebmer Я не знаю о таком ограничении. Посмотрите этот более полный код. Его значение копируется ближе к концу (закомментировано). - person Peter Hall; 09.09.2018
comment
TryFrom стабилен — хотите показать, как его можно использовать? (Не стесняйтесь помечать этот комментарий, как только вы обратились к нему). - person Shepmaster; 12.04.2019