LINUX.ORG.RU

История изменений

Исправление vertexua, (текущая версия) :

Т.е. после продолжительного времени интенсивной работы с памятью она будет фрагментирована и в лучшем случае все начнет «немножечко тормозить»?

То, что это прямо большая проблема придумали разрабочики сборщиков мусора чтобы сказать «аха, мы вот тут сжираем горы тактов CPU, но вот есть теоретическое преимущество - мы дефрагментируем память, опа». На деле что-то не очень известно чтобы это была большая проблема, даже простейший slab allocator будет по кругу циркулировать обьекты похожего размера.

В расте так происходит в случае, когда не справляется штатное средство для простановки аналога «free»?

В расте можно написать вот так

let a = A{};
f1(a);
f2(a);

Из-за того что по дефолту в расте происходит перемещение, то этот код не скомпилируется, a полностью ушла в f1. Тут есть несколько вариантов.

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

f1(&a);
f2(&a);

Разрешить копировать A неявно:

#[derive(Copy)]
struct A{}

Разрешить копировать A явно - клонировать.

#[derive(Copy)]
struct A{}

let a = A{};
f1(a.clone());
f2(a);

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

Можно применить счетчик ссылок. Сам счетчик поддерживает клонирование, освобождая сам обьект внутри него от необходимости клонироваться. Rc - не содержит атомарных инструкций и не может быть использован в многопоточном коде. Если например f1 или f2 внутри передают a в другой поток - компилятор это увидит и код не соберется. Все обьекты которые поддерживают многопоточность строго помечены специальными traits, без которых нельзя проникнуть между границами потоков. Rc - не помечен.

let a = Rc::new(A{});
f1(a.clone());
f2(a);

Для многопоточного случая есть счетчик ссылок с атомарным счетчиком.

let a = Arc::new(A{});
f1(a.clone());
f2(a);

Он позволит передать read-only копию в разные потоки. Все равно, мутировать сам a не получится. Дело в том что все это время тип a был A, а не mut A.

Для этого нужен Mutex или RwLock. В Rust Mutex - контейнер, а не независимый обьект. Он хранит в себе то, что защищает.

let a = Arc::new(Mutex::new(A{x:1}));
f1(a.clone());
f2(a);

Мы тут комбинируем возможность подсчитывать ссылки через Arc и получать доступ для записи через Mutex.

Потом где-то в дебрях f1 мы хотим изменить общую для всех копию a.

{
   let a_value = a.lock(); // lock() возвращает мутабельную ссылку `&mut A` и закрывается по выходу из блока.
   a_value.x += 1;
}

Нигде у вышеописаного кода нету явных освобождений памяти. При этом в машинном коде в итоге внутри некоторых функций, где компилятор доказывает выход обьекта out of scope статически проставляется free. Иногда для счетчика ссылок.

Гарантии насчет мутабельности и многопоточности не просто равны таковым в Java, но еще и превосходят их. Java например не заставила разработчика использовать синхронизацию, обнаружив многопоточную работу. Только в рантайме некоторые коллекции пугаются и что-то там детектируют, бросая ConcurrentModificationException.

В расте ты можешь сам освободить память вручную?

Функция освобождения памяти unsafe. Значит ее можно выполнить внутри unsafe блока. Такой блок можно написать, но он очень заметен во время ревью и по сути означает что программист сознательно делает что-то интересное с памятью. Язык программирования должен помогать ровно до того момента пока он помогает. Когда он мешает, тогда он должен отойти в сторонку. К счастью Rust в основном помогает. Там где он мешает и происходит что-то необычное можно просто написать unsafe и все обмазать комментариями почему на самом деле это safe просто мы делаем low-level манипуляции, безопасность которых компилятор доказать не может и «ошибается» в сторону «не пущать».

Исходная версия vertexua, :

Т.е. после продолжительного времени интенсивной работы с памятью она будет фрагментирована и в лучшем случае все начнет «немножечко тормозить»?

То, что это прямо большая проблема придумали разрабочики сборщиков мусора чтобы сказать «аха, мы вот тут сжираем горы тактов CPU, но вот есть теоретическое преимущество - мы дефрагментируем память, опа». На деле что-то не очень известно чтобы это была большая проблема, даже простейший slab allocator будет по кругу циркулировать обьекты похожего размера.

В расте так происходит в случае, когда не справляется штатное средство для простановки аналога «free»?

В расте можно написать вот так

let a = A{};
f1(a);
f2(a);

Из-за того что по дефолту в расте происходит перемещение, то этот код не скомпилируется, a полностью ушла в f1. Тут есть несколько вариантов.

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

f1(&a);
f2(&a);

Разрешить копировать A неявно:

#[derive(Copy)]
struct A{}

Разрешить копировать A явно - клонировать.

#[derive(Copy)]
struct A{}

let a = A{};
f1(a.clone());
f2(a);

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

Можно применить счетчик ссылок. Сам счетчик поддерживает клонирование, освобождая сам обьект внутри него от необходимости клонироваться. Rc - не содержит атомарных инструкций и не может быть использован в многопоточном коде. Если например f1 или f2 внутри передают a в другой поток - компилятор это увидит и код не соберется. Все обьекты которые поддерживают многопоточность строго помечены специальными traits, без которых нельзя проникнуть между границами потоков. Rc - не помечен.

let a = Rc::new(A{});
f1(a.clone());
f2(a);

Для многопоточного случая есть счетчик ссылок с атомарным счетчиком.

let a = Arc::new(A{});
f1(a.clone());
f2(a);

Он позволит передать read-only копию в разные потоки. Все равно, мутировать сам a не получится. Дело в том что все это время тип a был A, а не mut A.

Для этого нужен Mutex или RwLock. В Rust Mutex - контейнер, а не независимый обьект. Он хранит в себе то, что защищает.

let a = Arc::new(Mutex::new(A{x:1}));
f1(a.clone());
f2(a);

Мы тут комбинируем возможность подсчитывать ссылки через Arc и получать доступ для записи через Mutex.

Потом где-то в дебрях f1 мы хотим изменить общую для всех копию a.

{
   let a_value = a.lock(); // lock() возвращает мутабельную ссылку `&mut A` и закрывается по выходу из блока.
   a_value.x += 1;
}

Нигде у вышеописаного кода нету явных освобождений памяти. При этом в машинном коде в итоге внутри некоторых функций, где компилятор доказывает выход обьекта out of scope статически проставляется free. Иногда для счетчика ссылок.

Гарантии насчет мутабельности и многопоточности не просто равны таковым в Java, но еще и превосходят их. Java например не заставила разработчика использовать синхронизацию, обнаружив многопоточную работу. Только в рантайме некоторые коллекции пугаются и что-то там детектируют, бросая ConcurrentModificationException.