On branch main
modified: src/ch06.md new file: src/ex-ch06-01.zig new file: src/ex-ch06-02.zig
This commit is contained in:
322
src/ch06.md
322
src/ch06.md
@@ -1,7 +1,7 @@
|
||||
|
||||
# Динамическая память и аллокаторы
|
||||
|
||||
Всё, счем имели дело до сих пор, требовало знаний размеров всех сущностей
|
||||
Всё, с чем имели дело до сих пор, требовало знаний размеров всех сущностей
|
||||
во время компиляции. Массивы всегда имеют размер, известный во время
|
||||
компиляции (фактически, длина массива является частью типа). Все наши
|
||||
строки были строковыми литералами, длина которых тоже понятна по
|
||||
@@ -16,7 +16,7 @@
|
||||
модифицированы), переменные в стеке живут, пока работает функция, который
|
||||
принадлежит этот стековый кадр.
|
||||
|
||||
Эта главв содержит 2 темы. Первая это общий обзор нашей третьей области
|
||||
Эта глава содержит 2 темы. Первая это общий обзор нашей третьей области
|
||||
памяти, кучи. А вот вторая более интересна - в языке Zig имеется вполне
|
||||
ясный, но тем не менее уникальный подход к управлению динамической
|
||||
памятью. Даже если Вы так или иначе умеете работать с кучей
|
||||
@@ -26,17 +26,17 @@
|
||||
|
||||
## Динамическая память (heap, "куча")
|
||||
|
||||
Куча это наша третья область памяти в нашем распоряжении.
|
||||
В сравнении с первыми двумя это немного "дикий запад": дозволено всё.
|
||||
Именно, мы можем создавать сущности в куче с размером,
|
||||
известным только во время исполнения программы и при этом
|
||||
имеем полный контроль над временем жизни этих сущностей.
|
||||
Куча это третья область памяти, имеющаяся в нашем распоряжении. В
|
||||
сравнении с первыми двумя это немного "дикий запад": дозволено всё.
|
||||
Именно, мы можем создавать сущности в куче с размером, известным только
|
||||
во время исполнения программы и при этом имеем полный контроль над
|
||||
временем жизни этих сущностей.
|
||||
|
||||
Стек вызовов это замечательная штука ввиду простоты и предсказуемости
|
||||
способа, которым он управляет данными (создание/удаление стековых
|
||||
кадров). С другой стороны, эта простота одновременно является
|
||||
недостатком: времена жизни переменных в стеке жёстко привязяны ко времени
|
||||
жизни самого стекового кадра, в котором они "живут". Куча это полная
|
||||
недостатком: времена жизни переменных в стеке жёстко привязаны ко времени
|
||||
жизни самого стекового кадра, в котором они "живут". Куча это полная
|
||||
противоположность этому. Для объектов в куче нет никакого встроенного
|
||||
механизма контроля времени жизни, поэтому эти объекты могут существовать
|
||||
столь долго или, наоборот, столь коротко, сколько мы захотим. И эти
|
||||
@@ -87,7 +87,7 @@ fn getRandomCount() !u8 {
|
||||
как правило, имеет соответствующий вызов `free`. Первый выделяет память,
|
||||
второй освобождает. Однако, не дайте этому примеру ограничить Ваше
|
||||
воображение. Да, этот паттерн, `try alloc` и сразу следом `defer free`
|
||||
действительно часто испольузется, и не зря: такое размещение инструкций
|
||||
действительно часто используется, и не зря: такое размещение инструкций
|
||||
является относительно надёжной защитой от ошибок в виде того, что мы
|
||||
"забыли" освободить. Но так же часто используется совершенно иная схема:
|
||||
выделение памяти делается в одном месте, а освобождение совершенно в
|
||||
@@ -96,7 +96,7 @@ fn getRandomCount() !u8 {
|
||||
управления временем жизни объектов в куче, всё в нашей власти.
|
||||
Представьте себе веб-сервер с постадийной обработкой запроса, причём
|
||||
каждую стадию отрабатывает выделенная для этого нить - тогда память может
|
||||
быть выделена на стадии получения HTTP-запроса, а освождена где-то на
|
||||
быть выделена на стадии получения HTTP-запроса, а освобождена где-то на
|
||||
конечной стадии, например, при записи в журнал, в котором фиксируются все
|
||||
запросы.
|
||||
|
||||
@@ -115,13 +115,313 @@ fn getRandomCount() !u8 {
|
||||
в Zig отложенные инструкции будут выполнены по завершении
|
||||
*блока*, где они заданы, а в Go они будут выполнены в конце *функции*.
|
||||
|
||||
Родственником `defer` является `errdefer`. Так же, как и `defer`,
|
||||
он выполняет заданный код по завершении блока, но только в случае,
|
||||
если произошла ошибка. Это полезно, когда логика инициализации
|
||||
более сложная, чем в примере выше и нам нужно отменить
|
||||
уже совершённые процедуры в случае, если на очередном шаге
|
||||
случилась ошибка.
|
||||
|
||||
Следующий пример - своего рода прыжок в сложность. Он демонстрирует
|
||||
использование `errdefer`, а также общую схему, когда `init`
|
||||
используется для выделения памяти, а `deinit` для её освобождения:
|
||||
|
||||
```zig
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
pub const Game = struct {
|
||||
players: []Player,
|
||||
history: []Move,
|
||||
allocator: Allocator,
|
||||
|
||||
fn init(allocator: Allocator, player_count: usize) !Game {
|
||||
var players = try allocator.alloc(Player, player_count);
|
||||
errdefer allocator.free(players);
|
||||
|
||||
// store 10 most recent moves per player
|
||||
var history = try allocator.alloc(Move, player_count * 10);
|
||||
|
||||
return .{
|
||||
.players = players,
|
||||
.history = history,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(game: Game) void {
|
||||
const allocator = game.allocator;
|
||||
allocator.free(game.players);
|
||||
allocator.free(game.history);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Как можно надеяться, тут подчёркиваются две вещи. Первая - это полезность
|
||||
`errdefer`. Если всё идёт хорошо, память под поле `players` выделяется в
|
||||
`init` и освобождается в `deinit`. Но есть особый случай (маловероятный,
|
||||
но тем не менее) - если выделение памяти под `history` завершилось
|
||||
неудачно, то в этом и только в этом случае нужно освободить память,
|
||||
выделенную под `players`.
|
||||
|
||||
Второй аспект этого пример, на который стоит обратить внимание, состоит в
|
||||
том, что жизненный цикл наших двух срезов, то есть `players` и `history`,
|
||||
задаётся логикой прикладного кода. Нет никаких правил насчёт того, когда
|
||||
должна быть вызвана `deinit` и кто это должен сделать. С одной стороны,
|
||||
это хорошо, поскольку позволяет нам иметь произвольные времена жизни. С
|
||||
другой стороны, это плохо, потому что мы можем напортачить и никогда не
|
||||
вызвать `deinit` или вызвать её два раза.
|
||||
|
||||
Замечание по поводу именований "конструктора" (`init`) и "деструктора"
|
||||
(`deinit`). В именно таких названиях нет ничего особенного, это просто
|
||||
соглашения, принятые в стандартной библиотеке Zig и принятые к
|
||||
использованию сообществом Zig. В некоторых случаях, включая стандартную
|
||||
библиотеку, используются другие более подходящие для этих случаев имена,
|
||||
например, `open` и `close`.
|
||||
|
||||
## Повторное освобождение и утечки памяти
|
||||
|
||||
Только что мы отметили, что не существует никаких правил относительно
|
||||
того, кто и когда должен освободить память. Однако, это верно лишь
|
||||
отчасти, всё таки имеется несколько важных правил, просто ничто, кроме
|
||||
Вашей педантичности и аккуратности, не заставит Вас их соблюдать.
|
||||
|
||||
Первое правило состоит, что Вы не можете освободить одну и ту же
|
||||
область памяти дважды, смотрим на пример:
|
||||
|
||||
```zig
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var arr = try allocator.alloc(usize, 4);
|
||||
allocator.free(arr);
|
||||
allocator.free(arr);
|
||||
|
||||
std.debug.print("Это никогда не напечатается\n", .{});
|
||||
}
|
||||
```
|
||||
|
||||
Последняя строчка этого кода, как выяснится после попытки его запустить,
|
||||
оказалась пророческой, действительно, строчка не будет напечатана. Так
|
||||
получилось потому, что мы два раза делаем `allocator.free(arr);` и на
|
||||
втором разе программа "вылетит" с ошибкой. Это называется
|
||||
повторное/двойное освобождение (double free) и так делать не надо. Может
|
||||
показаться, что этого довольно просто избежать, однако, в сложных больших
|
||||
проектах со сложными правилами по поводу времён жизни бывает трудно всё
|
||||
это отследить.
|
||||
|
||||
Второе правило гласит, что Вы не можете освобождать память, на которую у
|
||||
Вас нет ссылки. Это вроде как самоочевидно, но бывает не всегда ясно, кто
|
||||
именно ответственен за освобождение. Вот функция, которая создаёт строку в
|
||||
нижнем регистре:
|
||||
|
||||
```zig
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
fn allocLower(allocator: Allocator, str: []const u8) ![]const u8 {
|
||||
var dest = try allocator.alloc(u8, str.len);
|
||||
|
||||
for (str, 0..) |c, i| {
|
||||
dest[i] = switch (c) {
|
||||
'A'...'Z' => c + 32,
|
||||
else => c,
|
||||
};
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
```
|
||||
|
||||
Сама по себе эта функция корректная, но вот такое её использование неправильно:
|
||||
|
||||
```zig
|
||||
// For this specific code, we should have used std.ascii.eqlIgnoreCase
|
||||
fn isSpecial(allocator: Allocator, name: [] const u8) !bool {
|
||||
const lower = try allocLower(allocator, name);
|
||||
return std.mem.eql(u8, lower, "admin");
|
||||
}
|
||||
```
|
||||
|
||||
Это утечка памяти. Память, выделенная внутри `allocLower`, никогда не
|
||||
освобождается. Но суть утечки не в этом, а в том, что как только мы
|
||||
вернулись из `isSpecial`, мы уже никогда не сможем её освободить. В
|
||||
языках со сборкой мусора память под данные, которые стали не нужны, в
|
||||
конце концов будет освобождена сборщиком мусора. В коде выше после
|
||||
возврата из `isSprecial` мы потеряли единственную ссылку на переменную
|
||||
`lower`. В итоге память занята, но ни использовать, ни освободить мы её
|
||||
не можем, потому что у нас нет нужного указателя. Занятая впустую память
|
||||
будет освобождена операционной системой только тогда, когда наша
|
||||
программа завершится. Функция `isSpecial`, возможно, не так уж и много
|
||||
памяти теряет, но если она многократно вызывается в программе, которая по
|
||||
своей сути работает "всегда" (любой сервер), то в итоге у нас на машине
|
||||
кончится память.
|
||||
|
||||
В случае с повторным освобождением программа просто аварийно завершится.
|
||||
А вот утечки памяти могут быть весьма коварными: мало того, что
|
||||
первопричину бывает довольно трудно установить, ситуацию ещё более могут
|
||||
усугубить небольшие утечки или утечки в не часто вызываемом коде, тогда
|
||||
Вы даже какое-то время даже и знать не будете, что имеет место утечка
|
||||
памяти, соответственно и причину искать не будете.
|
||||
|
||||
Поскольку утечки это довольно распространённая проблема, Zig
|
||||
предоставляет некоторую помощь, о которой мы узнаем, когда будем изучать
|
||||
аллокаторы.
|
||||
|
||||
## `create` и `destroy`
|
||||
|
||||
Метод `alloc` структуры `std.mem.Allocator` возвращает срез с длиной,
|
||||
указанной вторым параметром. Если Вам нужно одиночное значение,
|
||||
вместо `alloc` и `free` используйте `create` и `destroy`, соответственно.
|
||||
Пару глав назад, изучая указатели, мы создавали пользователя
|
||||
и пытались увеличить его силу. Вот работающий пример с динамическим
|
||||
выделением памяти при помощи метода `create`:
|
||||
|
||||
```zig
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
// again, we'll talk about allocators soon!
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
// create a User on the heap
|
||||
var user = try allocator.create(User);
|
||||
// free the memory allocated for the user at the end of this scope
|
||||
defer allocator.destroy(user);
|
||||
|
||||
user.id = 1;
|
||||
user.power = 100;
|
||||
|
||||
// this line has been added
|
||||
levelUp(user);
|
||||
std.debug.print("Пользователь {d} имеет силу {d}\n", .{user.id, user.power});
|
||||
}
|
||||
|
||||
fn levelUp(user: *User) void {
|
||||
user.power += 1;
|
||||
}
|
||||
|
||||
pub const User = struct {
|
||||
id: u64,
|
||||
power: i32,
|
||||
};
|
||||
```
|
||||
|
||||
Метод `create` принимает только один параметр, тип (`T`) создаваемого
|
||||
объекта. Возвращает он или указатель на выделенное место или ошибку, то
|
||||
есть `!*T`. У вас может возникнуть вопрос - а что, если мы таким образом
|
||||
создадим пользователя, но при этом не назначим никаких значений полям
|
||||
`id` и `power`? Тут всё просто - получится так же, как если бы назначили
|
||||
"значения" `undefined`.
|
||||
|
||||
Когда мы изучали висячие указатели, у нас была функция, которая ошибочно
|
||||
возвращала адрес локальной переменной:
|
||||
|
||||
```zig
|
||||
pub const User = struct {
|
||||
fn init(id: u64, power: i32) *User{
|
||||
var user = User{
|
||||
.id = id,
|
||||
.power = power,
|
||||
};
|
||||
// this is a dangling pointer
|
||||
return &user;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
В этом случае имеет смысл возвращать `User`, а не указатель на него.
|
||||
Но иногда нужно, чтобы функция возвращала именно указатель на то,
|
||||
что она создала. Так нужно делать, если Вы хотите, чтобы время
|
||||
жизни создаваемой сущности не было привязано к механизму стека вызовов.
|
||||
Чтобы решить проблему висячего указателя, можно использовать `create`:
|
||||
|
||||
```zig
|
||||
// our return type changed, since init can now fail
|
||||
// *User -> !*User
|
||||
fn init(allocator: std.mem.Allocator, id: u64, power: i32) !*User{
|
||||
var user = try allocator.create(User);
|
||||
user.* = .{
|
||||
.id = id,
|
||||
.power = power,
|
||||
};
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
Тут у нас появилась новая синтаксическая конструкция, `user.* = .{...}`.
|
||||
Она немного странная и некоторым она не нравится, но, так или иначе, она
|
||||
используется. То, что написано справа от знака присваивания, мы уже
|
||||
видели, это инициализатор структуры с выводимым из левой части типом.
|
||||
Можно и явно написать, то есть `user.* = User{...}`, по сути ничего не
|
||||
поменяется. А вот то, что слева от знака присваивания, называется
|
||||
"разыменование указателя" (pointer dereference). Уже знакомый нам
|
||||
оператор `&` (взятие адреса) берёт тип `T` и возвращает указатель на
|
||||
него, то есть `*T`. А разыменование указателя, то есть `.*` делает
|
||||
противоположное, то есть берёт `*T` и возвращает `T`. Помните, что
|
||||
`create` тут возвращает `!*User`, стало быть, тип переменной `user` есть
|
||||
указатель на `User`, то есть `*User` и чтобы заполнить память, где
|
||||
расположен этот экземпляр, указатель нужно разыменовать.
|
||||
|
||||
## Аллокаторы
|
||||
|
||||
Одним из базовых принципов Zig является принцип "никаких скрытых выделений памяти".
|
||||
В зависимости от Вашего предыдущего опыта программирования,
|
||||
возможно, для Вас в этом принципе нет ничего особенного.
|
||||
Тем не менее, это сильно контрастирует с тем, что мы имеем в стандартной
|
||||
библиотеке языка C, а там для выделения памяти мы имеем `malloc`.
|
||||
В C для того, чтобы понять, использует ли та или иная функция кучу,
|
||||
нужно глядеть в исходный код этой функции и искать там `malloc`
|
||||
и родственные ей, например, `calloc`.
|
||||
|
||||
А вот в Zig нет аллокатора "по умолчанию". Во всех примерах выше
|
||||
функции, которые размещают сущности в куче, принимают в качестве
|
||||
параметра `std.mem.Allocator`. По соглашению, обычно это первый параметр.
|
||||
Вся стандартная библиотека Zig, а также большинство сторонних библиотек
|
||||
требуют, чтобы вызывающая сторона передала аллокатор в случае,
|
||||
если этим библиотекам требуется выделять память динамически.
|
||||
|
||||
Такое явное указание аллокаторов может принимать две формы.
|
||||
В простых случаях, аллокатор передаётся каждой функции, например:
|
||||
|
||||
```zig
|
||||
onst say = std.fmt.allocPrint(allocator, "It's over {d}!!!", .{user.power});
|
||||
defer allocator.free(say);
|
||||
```
|
||||
|
||||
Функция `std.fmt.allocPrint` похожа на `std.debug.print`, только вместо
|
||||
того, чтобы печатать в `stderr`, она выделяет память под строку и "печатает"
|
||||
в эту строку (как `sprintf` в C).
|
||||
|
||||
Другая форма это когда аллокатор передаётся в "конструктор" (`init`),
|
||||
там запоминается и далее используется для внутренних нужд объекта.
|
||||
Мы видели такую форму выше, в реализации структуры `Game`.
|
||||
Такая форма менее явна, потому что Вы знаете, что объект будет
|
||||
использовать динамическую память, но не знаете, в каких именно своих
|
||||
методах он будет это делать. Такой подход более практичен для долго живущих сущностей.
|
||||
|
||||
Преимущество "внедрения" аллокатора состоит не только в том, что
|
||||
явно обозначено, что функция/объект будут использовать кучу,
|
||||
но также и в том, что это предоставляет некоторую гибкость.
|
||||
Дело в том, что `std.mem.Allocator` это *интерфейс* (а не конкретный аллокатор),
|
||||
который предоставляет методы `alloc/free` и `create/destroy`, наряду с некоторыми другими.
|
||||
До этого момента мы видели только `std.heap.GeneralPurposeAllocator`,
|
||||
но в стандартной библиотеке имеются реализации и других аллокаторов.
|
||||
|
||||
Если Вы разрабатываете библиотеку, лучше всего будет, если она будет
|
||||
принимать аллокатор, это позволит пользователям Вашей библиотеки
|
||||
сами выбирать, какой конкретно аллокатор будет использоваться.
|
||||
В противном случае Вам будет нужно выбирать "правильный" аллокатор и,
|
||||
как мы далее увидим, аллокаторы не являются взаимоисключающими -
|
||||
программа может одновременно использовать несколько видов аллокаторов,
|
||||
и на то могут быть свои причины.
|
||||
|
||||
|
||||
|
||||
## Аллокатор общего назначения
|
||||
|
||||
## `std.testing.allocator`
|
||||
|
||||
13
src/ex-ch06-01.zig
Normal file
13
src/ex-ch06-01.zig
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var arr = try allocator.alloc(usize, 4);
|
||||
allocator.free(arr);
|
||||
allocator.free(arr);
|
||||
|
||||
std.debug.print("Это никогда не напечатается\n", .{});
|
||||
}
|
||||
30
src/ex-ch06-02.zig
Normal file
30
src/ex-ch06-02.zig
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
// again, we'll talk about allocators soon!
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
// create a User on the heap
|
||||
var user = try allocator.create(User);
|
||||
|
||||
// free the memory allocated for the user at the end of this scope
|
||||
defer allocator.destroy(user);
|
||||
|
||||
user.id = 1;
|
||||
user.power = 100;
|
||||
|
||||
// this line has been added
|
||||
levelUp(user);
|
||||
std.debug.print("User {d} has power of {d}\n", .{user.id, user.power});
|
||||
}
|
||||
|
||||
fn levelUp(user: *User) void {
|
||||
user.power += 1;
|
||||
}
|
||||
|
||||
pub const User = struct {
|
||||
id: u64,
|
||||
power: i32,
|
||||
};
|
||||
Reference in New Issue
Block a user