Заменяем глобальный аллокатор в Rust
Вторая часть про работу с кучей, устанавливаем свой глобальный аллокатор. В принципе, это делается в несколько строк, но наша цель — действовать сознательно и профессионально, с глубоким проникновением в суть явлений, так что легкой жизни не будет (как и обещал).
В японском зоопарке родился слоненок — казалось бы, при чем тут Лужков? Глобальный аллокатор, static и const?! — Спокойно, действуем по ранее утвержденному плану. Да и все равно изучать.
Рассмотрим такой пример:
struct Point {
x: i32,
y: i32,
}
static STATIC_POINT: Point = new_point(10, 20);
static mut MUT_STATIC_POINT: Point = Point{x: 30, y: 40};
const CONST_POINT: Point = new_point(50, 60);
Каковы сходства и различия между разными «точками», в особенности между static STATIC_POINT
и const CONST_POINT
?
static и const: Адрес
Для всех значений можно получить сырой указатель и напечатать его значение:
fn main() {
print_pointer(100, &STATIC_POINT);
unsafe {
print_pointer(200, &MUT_STATIC_POINT);
}
print_pointer(300, &CONST_POINT);
}
#[inline(never)]
fn print_pointer(label: i32, p: *const T) {
println!("label:{} pointer:{:p}", label, p);
}
Результат какой-то такой:
label:100 pointer:0x562316fb2000
label:200 pointer:0x562316fc1008
label:300 pointer:0x562316fb2008
- Взятие адреса изменяемой глобальной статической переменной является опасной операцией
print_pointer()
с параметромlabel
введена для удобства работы с ассемблерным листингом
Посмотрим в ассемблер, благо чудесный play.rust-lang.org это позволяет:
push rax
lea rsi, [rip + playground::STATIC_POINT]
mov edi, 100
call playground::print_pointer
lea rsi, [rip + playground::MUT_STATIC_POINT]
mov edi, 200
call playground::print_pointer
lea rsi, [rip + .L__unnamed_2]
mov edi, 300
pop rax
jmp playground::print_pointer
Ну т.е. вроде как отдельные адреса заведены под все значения. Стоит ли полагаться на то, что адрес const-значения (L__unnamed_2
) постоянен? Как водится, самое внятное описание мы найдем в «старом учебнике»:
More specifically, constants in Rust have no fixed address in memory. This is because they«re effectively inlined to each place that they«re used. References to the same constant are not necessarily guaranteed to refer to the same memory address for this reason.
Нет, не стоит. Если нужен гарантированный глобальный адрес, используем static
.
static и const: Встраивание (Inlining)
»…they«re effectively inlined» — ой ли?
Любую документацию по Rust надо рассматривать либо как неполную, либо как противоречивую (собственно, Гёдель), а в The Rust Reference авторы сами пишут красными буквами:
Warning: This book is incomplete
Так что будем проверять все, что можно проверить, благо, инструментарий имеется.
Погрузимся в глубины присваивания, напрямую можно присваивать только const:
let p1 = CONST_POINT;
dbg!(p1);
// let p2 = STATIC_POINT; // Err: cannot move out of static item `STATIC_POINT`
// let p3 = MUT_STATIC_POINT; // Err: cannot move out of static item `STATIC_POINT`
Посмотрим в ассемблер вот такого кода:
...
let p1 = CONST_POINT;
print_pointer(200, &p1);
...
mov ecx, dword ptr [rip + .L__unnamed_2]
mov eax, dword ptr [rip + .L__unnamed_2+4]
mov dword ptr [rsp + 96], ecx
mov dword ptr [rsp + 100], eax
mov edi, 200
lea rsi, [rsp + 96]
call playground::print_pointer
Ну, такое себе «встраивание» — сначала все копируется в ecx
и eax
, затем оттуда в стек (локальную переменную p1
). В чем дело? А, так это режим Debug, а что в Release?:
movabs rbx, 257698037810
mov qword ptr [rsp + 8], rbx
lea rsi, [rsp + 8]
mov edi, 200
call playground::print_pointer
Вот тут, действительно, встраивание. 257698037810
это аккурат (60_i64 << 32) + 50
, т.е. упакованное значение для const CONST_POINT: Point = new_point(50, 60)
.
Но постойте, мы же можем и static
присвоить, для этого надо реализовать Copy
:
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
Вот что выходит:
...
let p1 = STATIC_POINT;
print_pointer(200, &p1);
...
movabs rax, 85899345930
mov qword ptr [rsp], rax
mov rsi, rsp
mov edi, 200
call playground::print_pointer
Тоже inlining, (20_i64 << 32) + 10 = 85899345930
как раз соответствует static STATIC_POINT: Point = new_point(10, 20)
.
Короче, разницы, в плане «встраивания», между static и const на нашем примере не видно (понятно, что с поправкой на то, что Copy
is-a-must).
static и const: Изменяемость (Mutability)
Код примеров тут.
Ни одно из «значений» не может быть изменено в безопасном режиме, по разным причинам:
// STATIC_POINT = new_point(1, 2); // Err: cannot assign to immutable static item `STATIC_POINT`
// MUT_STATIC_POINT = new_point(1, 2); // Err: use of mutable static is unsafe and requires unsafe function or block
// CONST_POINT = new_point(1, 2); // Err: invalid left-hand side of assignment
MUT_STATIC_POINT
можно изменить в unsafe:
unsafe {
MUT_STATIC_POINT.x += 2;
MUT_STATIC_POINT.y += 4;
}
unsafe {
let p_point = &MUT_STATIC_POINT as *const _ as *mut Point;
(*p_point).x += 1;
}
static mut
являет собой глобальную переменную, каковые часто рассматриваются как глобальное зло, а с такими вещами нужно работать осторожно, огораживая и помечая опасные участки при помощиunsafe
.
Попытки изменить STATIC_POINT
или CONST_POINT
заканчиваются ужасами Segmentation fault:
// unsafe {
// let p_point = &STATIC_POINT as *const _ as *mut Point;
// (*p_point).x += 1;
// }
// unsafe {
// let p_point = &CONST_POINT as *const _ as *mut Point;
// (*p_point).x += 1;
// }
static и const: Инициализация
Ну и, наконец, общим для всех этих… ээээ… «переменных» является то, что функции, их инициализирующие, должны быть объявлены с квалификатором const
:
const fn new_point(x: i32, y: i32) -> Point {
return Point { x: x, y: y };
}
- Функции такого типа являются чистыми (pure), результат их известен во время компиляции и вызов функции может быть заменен ее результатом
- Соответственно, в «кучу» лезть из таких функций нельзя
static и const: Деструктор
Как утверждает отличное старое руководство, метод Drop::drop()
для static
переменных может быть реализован, но он не вызывается.
static и const: Что использовать?
Прекрасный ответ дает RFC-0246:
- constants declare constant values. These represent a value, not a memory address. This is the most common thing one would reach for and would replace static as we know it today in almost all cases.
- statics declare global variables. These represent a memory address. They would be rarely used: the primary use cases are global locks, global atomic counters, and interfacing with legacy C libraries.
In the area of mathematical logic and computer science known as type theory, a unit type is a type that allows only one value (and thus can hold no information).
…
In Haskell and Rust, the unit type is called () and its only value is also (), reflecting the 0-tuple interpretation.
…
In Java, the unit type is called Void and its only value is null.
…
Unit type
Итак, пустой кортеж (tuple) ()
означает одновременно и тип, и литерал (фиксированное значение). Можно писать так:
fn no_result() {
}
Или так:
fn no_result() {
return ()
}
Или так:
fn no_result() -> () {
return ()
}
unit
похож наvoid
вC
, но там уvoid
нет значения- Запись
HashMap
означает словарь, у которого информацию несут только ключи (как быHashSet
)
Unit-like структуры, как понятно из названия, также представляют собой и тип, и литерал:
struct MyUnitStruct;
static MY_UNIT_STRUCT1: MyUnitStruct = MyUnitStruct;
static MY_UNIT_STRUCT2: MyUnitStruct = MyUnitStruct;
Для комплекта возьмем еще std: alloc: System (мы потихоньку подбираемся к глобальному аллокатору) и напечатаем их адреса:
// println!("&MyUnitStruct: {:p}", &MyUnitStruct);
// println!("&std::alloc::System: {:p}", &std::alloc::System);
print_pointer(100, &MyUnitStruct);
print_pointer(200, &std::alloc::System);
print_pointer(300, &MY_UNIT_STRUCT1);
print_pointer(400, &MY_UNIT_STRUCT2);
- Ну и дела, они все равны!
- Если раскомментировать
// println...
, то получаются два уникальных значения для адресов - Если нужен гарантированный адрес, то наверняка нужно использовать
static
Ассемблер намекает, что MyUnitStruct
и System
трактуются как const
, а MY_UNIT_STRUCT*
— как static
:
mov edi, 100
lea rsi, [rip + .L__unnamed_2]
call playground::print_pointer
mov edi, 200
lea rsi, [rip + .L__unnamed_2]
call playground::print_pointer
mov edi, 300
lea rsi, [rip + playground::MY_UNIT_STRUCT1]
call playground::print_pointer
mov edi, 400
lea rsi, [rip + playground::MY_UNIT_STRUCT2]
call playground::print_pointer
Это все, конечно, интересно, но каково же практическое применение? Именно практическое!
Мы почти у цели, осталось немного. Допустим, мы сделали сервер с методами start()
, stop()
, handle_request()
и запускаем его так:
struct Server{}
impl Server{
fn start(&self) {
println!("Server started");
}
...
fn run_server(s: &Server){
s.start();
s.handle_request("req1");
s.handle_request("req2");
s.stop();
}
Server{}
— пустая структура, это другое. Чудесными свойствами unit-like структур не обладает
Все работает, но в процессе эксплуатации понимаем, что println!()
надо бы заменить вызовами некоего нормального логгера, и Server
у нас теперь выглядит вот так:
trait Logger {
fn info(&self, msg: &str);
}
struct Server {
logger: L,
}
impl Server {
...
fn run_server(s: &Server) {
s.start();
s.handle_request("req1");
s.handle_request("req2");
s.stop();
}
Мы поменяли класс Server
, также пришлось поменять и код, который его использует, т.е. функцию run_server()
. Если такого кода много, возникнет много проблем, избежать их позволяют unit-like структуры.
Во-первых, на такую структуру можно навестить реализацию интерфейса, например, реализуем Logger для std: alloc: System:
impl Logger for std::alloc::System {
fn info(&self, msg: &str) {
println!("INFO: {}", msg);
}
}
fn main() {
std::alloc::System.info("Hello");
}
- «Навесить» методов можно на что угодно, не только на «свое»
- Ключевым моментом является то, что не требуется создавать экземпляр реализатора — он уже есть (как выяснилось, один на все unit-like структуры) и передается как первый параметр (
&self
)
Во-вторых, unit-like структуры можно использовать как значение по умолчанию в параметрах типа:
struct Server {
logger: L,
}
impl Server {
fn start(&self) {
self.logger.info("Server started");
...
fn run_server(s: &Server) {
s.start();
s.handle_request("req1");
s.handle_request("req2");
s.stop();
}
fn main() {
let s = Server {
logger: std::alloc::System,
};
run_server(&s);
}
- voila — функция
run_server()
вернулась к первоначальному виду, что и требовалось
Может создаться впечатление, что здесь происходит процесс, известный как «Внедрение зависимости» (Dependency Injection), т.е. код:
let s = Server {
logger: std::alloc::System,
};
… выступает в роли «внедренца» и связывает экземпляр типа Server
с интерфейсом Logger
. Нет, за кулисами генерируются разные «классы» Server
под каждую конкретную реализацию Logger
.
Этот момент чрезвычайно важен, для его понимания сделаем второй логгер:
struct MyLogger {}
impl Logger for MyLogger {
fn info(&self, msg: &str) {
println!("MyLogger-INFO: {}", msg);
}
}
...
fn main() {
let s1 = Server {
logger: std::alloc::System,
};
run_server(&s1);
let s2 = Server {
logger: MyLogger{}
};
run_server(&s2);
… и заглянем за кулисы:
playground::Server::start:
push rax
mov qword ptr [rsp], rdi
lea rsi, [rip + .L__unnamed_4]
mov edx, 14
call ::info
pop rax
ret
playground::Server::start:
push rax
mov qword ptr [rsp], rdi
lea rsi, [rip + .L__unnamed_4]
mov edx, 14
call ::info
pop rax
ret
- Налицо две версии класса кодовой базы
Server
, по одной для каждого используемого логгера - Никакого DI, конкретный вариант «класса»
Server
точно «знает», с каким логгером он работает - Такая «механика» работает быстрее, чем вызов через vtable, но при этом исполняемый файл «раздувается» в размере
Замена глобального аллокатора описана здесь, так заменим:
#[global_allocator]
struct Counter;
...
unsafe impl GlobalAlloc for Counter {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ret = System.alloc(layout);
...
static A: Counter = Counter;
- В общем, надо поставить
#[global_allocator]
и реализовать trait GlobalAlloc - Предлагаемая реализация
GlobalAlloc
пробрасывает вызовы кstd::alloc::System
и попутно ведет счетчик - Постановка
#[global_allocator]
передstruct Counter
приводит к ошибкеallocators must be statics
, т.е. такиCounter
это константа. Наверное
System.alloc()
, где это и что это? Про сам std: alloc: System мы уже знаем, это pub struct System;
, а откуда взялось System.alloc()
? Вопрос интересный, как уже понятно, навесить alloc()
на unit-like структуру System
можно где угодно (в пределах видимости).
Через std::alloc::System
реализуется std: alloc: GlobalAlloc:
pub unsafe trait GlobalAlloc {
...
unsafe fn alloc(&self, layout: Layout) -> *mut u8;
...
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);
...
Реализация зависит от платформы, «условная компиляция», видимо, происходит здесь:
cfg_if::cfg_if! {
if #[cfg(unix)] {
mod unix;
pub use self::unix::*;
} else if #[cfg(windows)] {
mod windows;
pub use self::windows::*;
Вот реализация для windows:
unsafe impl GlobalAlloc for System {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// SAFETY: Pointers returned by `allocate` satisfy the guarantees of `System`
let zeroed = false;
unsafe { allocate(layout, zeroed) }
}
...
allocate ():
extern "system" {
...
// See https://docs.microsoft.com/windows/win32/api/heapapi/nf-heapapi-heapalloc
fn HeapAlloc(hHeap: c::HANDLE, dwFlags: c::DWORD, dwBytes: c::SIZE_T) -> c::LPVOID;
...
}
unsafe fn allocate(layout: Layout, zeroed: bool) -> *mut u8 {
...
if layout.align() <= MIN_ALIGN {
// SAFETY: `heap` is a non-null handle returned by `GetProcessHeap`.
// The returned pointer points to the start of an allocated block.
unsafe { HeapAlloc(heap, flags, layout.size()) as *mut u8 }
- Ну все, добрались до системных вызовов
Теперь посмотрим, как работает std::alloc::alloc()
из предыдущей серии.
std: alloc: alloc ():
/// This function is expected to be deprecated in favor of the `alloc` method
/// of the [`Global`] type when it and the [`Allocator`] trait become stable.
...
pub unsafe fn alloc(layout: Layout) -> *mut u8 {
unsafe { __rust_alloc(layout.size(), layout.align()) }
}
- »…is expected to be deprecated in favor of Global and Allocator» — запомним это
__rust_alloc ():
// These are the magic symbols to call the global allocator. rustc generates
// them to call `__rg_alloc` etc. if there is a `#[global_allocator]` attribute
// (the code expanding that attribute macro generates those functions), or to call
// the default implementations in libstd (`__rdl_alloc` etc. in `library/std/src/alloc.rs`)
// otherwise.
...
fn __rust_alloc(size: usize, align: usize) -> *mut u8;
С #[global_allocator]
все понятно, идем в __rdl_alloc ():
pub unsafe extern "C" fn __rdl_alloc(size: usize, align: usize) -> *mut u8 {
// SAFETY: see the guarantees expected by `Layout::from_size_align` and
// `GlobalAlloc::alloc`.
unsafe {
let layout = Layout::from_size_align_unchecked(size, align);
System.alloc(layout)
}
}
Вот и все, доплыли до знакомого System.alloc()
.
Еще глянем RawVec
, и на этом закончим.
RawVec:
pub struct RawVec {
ptr: Unique,
- Не зря учили матчасть, теперь можно с чувством глубокого понимания искать
trait Allocator
,struct Global;
иimpl Allocator for Global
- Nota Bene: Именно этим и заменят
std::alloc::alloc()
trait
std: alloc: Allocator:
pub unsafe trait Allocator {
...
fn allocate(&self, layout: Layout) -> Result, AllocError>;
...
fn allocate_zeroed(&self, layout: Layout) -> Result, AllocError> {
...
struct
std: alloc: Global:
/// This type implements the [`Allocator`] trait by forwarding calls
/// to the allocator registered with the `#[global_allocator]` attribute
...
pub struct Global;
impl Allocator for Global:
unsafe impl Allocator for Global {
fn allocate(&self, layout: Layout) -> Result, AllocError> {
self.alloc_impl(layout, false)
}
...
Global.alloc_impl ():
impl Global {
...
fn alloc_impl(&self, layout: Layout, zeroed: bool) -> Result, AllocError> {
...
let raw_ptr = if zeroed { alloc_zeroed(layout) } else { alloc(layout) };
Далее пойдем только в ветку с alloc (layout):
pub unsafe fn alloc(layout: Layout) -> *mut u8 {
unsafe { __rust_alloc(layout.size(), layout.align()) }
}
- А
__rust_alloc()
уже проходили, уф
Не прощаюсь.