Что такое семантика перемещения в Rust?

В Rust есть две возможности взять ссылку

  1. Взять напрокат, т. Е. Взять ссылку, но не разрешать изменять объект назначения. Оператор & заимствует собственность у значения.

  2. Занимать изменчиво, т. Е. Брать ссылку для изменения цели. Оператор &mut изменчиво заимствует собственность у значения.

Документация Rust о правилах заимствования гласит:

Во-первых, любой заем должен длиться в объеме, не превышающем возможности владельца. Во-вторых, у вас может быть один или другой из этих двух видов заимствований, но не оба одновременно:

  • одна или несколько ссылок (&T) на ресурс,
  • ровно одна изменяемая ссылка (&mut T).

Я считаю, что взятие ссылки - это создание указателя на значение и доступ к значению по указателю. Это может быть оптимизировано компилятором, если есть более простая эквивалентная реализация.

Однако я не понимаю, что означает этот шаг и как он реализован.

Для типов, реализующих свойство Copy это означает копирование, например, присваивая структуру по элементам из источника, или memcpy(). Для небольших структур или для примитивов эта копия эффективна.

А для переезда?

Этот вопрос не является дубликатом Что такое семантика перемещения? потому что Rust и C++ - это разные языки, а семантика перемещения различна.

Ответ 1

Семантика

Rust реализует так называемую систему аффинных типов:

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

Типы, которые не являются Copy и, таким образом, перемещаются, являются аффинными типами: вы можете использовать их один раз или никогда, больше ничего.

Rust квалифицирует это как передачу права собственности в своем ориентированном на владение мире взгляде (*).

(*) Некоторые из людей, работающих над Rust, гораздо более квалифицированы, чем я в CS, и они сознательно внедрили систему аффинных типов; однако, вопреки Хаскелу, который раскрывает концепции математики и математики, Руст стремится раскрыть более прагматичные концепции.

Примечание: можно утверждать, что аффинные типы, возвращаемые функцией, помеченной #[must_use], на самом деле являются линейными типами из моего чтения.


Реализация

Это зависит. Пожалуйста, имейте в виду, что Rust - это язык, созданный для скорости, и здесь есть множество проходов оптимизации, которые будут зависеть от используемого компилятора (rustc + LLVM, в нашем случае).

Внутри функционального органа (игровая площадка):

fn main() {
    let s = "Hello, World!".to_string();
    let t = s;
    println!("{}", t);
}

Если вы проверите LLVM IR (в Debug), вы увидите:

%_5 = alloca %"alloc::string::String", align 8
%t = alloca %"alloc::string::String", align 8
%s = alloca %"alloc::string::String", align 8

%0 = bitcast %"alloc::string::String"* %s to i8*
%1 = bitcast %"alloc::string::String"* %_5 to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %1, i8* %0, i64 24, i32 8, i1 false)
%2 = bitcast %"alloc::string::String"* %_5 to i8*
%3 = bitcast %"alloc::string::String"* %t to i8*
call void @llvm.memcpy.p0i8.p0i8.i64(i8* %3, i8* %2, i64 24, i32 8, i1 false)

Под покровами rustc вызывает memcpy из результата "Hello, World!".to_string() в s а затем в t. Хотя это может показаться неэффективным, проверяя тот же IR в режиме Release, вы поймете, что LLVM полностью исключил копии (понимая, что s не использовался).

Та же самая ситуация возникает при вызове функции: в теории вы "перемещаете" объект в кадр стека функций, однако на практике, если объект большой, компилятор rustc может вместо этого переключиться на передачу указателя.

Другая ситуация возвращается из функции, но даже в этом случае компилятор может применить "оптимизацию возвращаемого значения" и выполнить сборку непосредственно в кадре стека вызывающего абонента, то есть вызывающий объект передает указатель, в который записывается возвращаемое значение, которое используется без промежуточное хранение.

Ограничения владения/заимствования Rust позволяют оптимизировать, что трудно достичь в C++ (который также имеет RVO, но не может применять его во многих случаях).

Итак, дайджест версия:

  • перемещение больших объектов неэффективно, но есть ряд оптимизаций, которые могут полностью исключить движение
  • перемещение включает в себя memcpy из std::mem::size_of::<T>() байтов, поэтому перемещение большой String эффективно, потому что она занимает всего пару байтов независимо от размера выделенного буфера, на котором они хранятся

Ответ 2

Когда вы перемещаете элемент, вы передаете право собственности на этот элемент. Это ключевой компонент Rust.

Скажем, у меня была структура, а затем я назначаю структуру из одной переменной в другую. По умолчанию это будет шаг, и я передал право собственности. Компилятор будет отслеживать эту смену владельца и не позволит мне больше использовать старую переменную:

pub struct Foo {
    value: u8,
}

fn main() {
    let foo = Foo { value: 42 };
    let bar = foo;

    println!("{}", foo.value); // error: use of moved value: `foo.value`
    println!("{}", bar.value);
}

как он реализован.

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

В практических целях вы можете думать, что когда вы что-то перемещаете, биты, представляющие этот элемент, дублируются, как если бы через memcpy. Это помогает объяснить, что происходит, когда вы передаете переменную функции, которая ее потребляет, или когда вы возвращаете значение из функции (опять-таки оптимизатор может делать другие вещи, чтобы сделать его эффективным, это просто концептуально):

// Ownership is transferred from the caller to the callee
fn do_something_with_foo(foo: Foo) {} 

// Ownership is transferred from the callee to the caller
fn make_a_foo() -> Foo { Foo { value: 42 } } 

"Но подождите!", вы говорите: "memcpy только вступает в игру с типами, реализующими Copy!". Это в основном верно, но большая разница в том, что когда тип реализует Copy, как источник, так и получатель действительны для использования после копирования!

Один из способов мышления семантики перемещения - это то же самое, что и семантика копирования, но с добавленным ограничением на то, что перемещаемая вещь больше не является допустимым элементом для использования.

Однако часто бывает проще подумать об этом по-другому: самая простая вещь, которую вы можете сделать, это переместить/отдать права собственности, а способность копировать что-то является дополнительной привилегией. Это способ, которым Rust моделирует его.

Это сложный вопрос для меня! После использования Rust некоторое время семантика перемещения естественна. Дайте мне знать, какие части я забыл или плохо объяснил.

Ответ 3

Пожалуйста, позвольте мне ответить на мой собственный вопрос. У меня были проблемы, но, задав здесь вопрос, я решил "Решение проблемы с резиновыми ушками" . Теперь я понимаю:

A move - это передача прав собственности.

Например, присваивание let x = a; передает право собственности: сначала a принадлежит значение. После let it x, которому принадлежит значение. Руста запрещает использовать a после этого.

Фактически, если вы выполняете println!("a: {:?}", a); после let, компилятор Rust говорит:

error: use of moved value: `a`
println!("a: {:?}", a);
                    ^

Полный пример:

#[derive(Debug)]
struct Example { member: i32 }

fn main() {
    let a = Example { member: 42 }; // A struct is moved
    let x = a;
    println!("a: {:?}", a);
    println!("x: {:?}", x);
}

И что означает этот move?

Похоже, что концепция взята из С++ 11. A документ о семантике перемещения С++ говорит:

С точки зрения клиентского кода выбор перемещения вместо копирования означает, что вам все равно, что происходит с состоянием источника.

Ага. С++ 11 не волнует, что происходит с источником. Таким образом, в этом ключе Rust может принять решение запретить использовать источник после переезда.

И как это реализовано?

Я не знаю. Но я могу представить, что Руста буквально ничего не делает. x - это просто другое имя для того же значения. Имена обычно компилируются (за исключением конечно отладочных символов). Таким образом, это тот же машинный код, имеет ли привязка имя a или x.

Кажется, что С++ делает то же самое в редакторе конструктора экземпляров.

Ничего не делать наиболее эффективно.

Ответ 4

Передача значения для функции также приводит к передаче права собственности; он очень похож на другие примеры:

struct Example { member: i32 }

fn take(ex: Example) {
    // 2) Now ex is pointing to the data a was pointing to in main
    println!("a.member: {}", ex.member) 
    // 3) When ex goes of of scope so as the access to the data it 
    // was pointing to. So Rust frees that memory.
}

fn main() {
    let a = Example { member: 42 }; 
    take(a); // 1) The ownership is transfered to the function take
             // 4) We can no longer use a to access the data it pointed to

    println!("a.member: {}", a.member);
}

Следовательно, ожидаемая ошибка:

post_test_7.rs:12:30: 12:38 error: use of moved value: `a.member`