Что такое семантика перемещения в Rust?
В Rust есть две возможности взять ссылку
-
Взять напрокат, т. Е. Взять ссылку, но не разрешать изменять объект назначения. Оператор &
заимствует собственность у значения.
-
Занимать изменчиво, т. Е. Брать ссылку для изменения цели. Оператор &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`