Skip to content

Commit

Permalink
src: fast deinit feature. (#15)
Browse files Browse the repository at this point in the history
* src: fast deinit feature.

don't traverse the tree in deinit(), if the memory can be freed on the allocator level.
useful for arena and fixed buffer allocators.
  • Loading branch information
avdva authored Oct 17, 2024
1 parent 1caf8b1 commit 197e7d1
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 4 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/zig-out
/.vscode
/.zig-cache
*.code-workspace
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ To use this library, you need at least Zig 0.13.x.

## API
```zig
// create tree type:
pub const Options = struct {
countChildren: bool = false,
};
pub fn TreeWithOptions(comptime K: type, comptime V: type, comptime Cmp: fn (a: K, b: K) math.Order, comptime options: Options) type
pub fn Tree(comptime K: type, comptime V: type, comptime Cmp: fn (a: K, b: K) math.Order) type
// init/deinit:
pub const InitOptions = struct {
allowFastDeinit: enum { always, auto, never } = .never,
};
pub fn init(a: std.mem.Allocator) Self
pub fn initWithOptions(a: std.mem.Allocator, io: InitOptions) Self
pub fn deinit()
// insert:
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = "zigavl",
.version = "0.6.1",
.version = "0.7.0",
.dependencies = .{},
.paths = .{""},
}
102 changes: 101 additions & 1 deletion src/avl.zig
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@ fn locationCache(comptime K: type, comptime V: type, comptime Tags: type) type {
};
}

const allocAddrs = blk: {
var aa: std.heap.ArenaAllocator = undefined;
const aaa = aa.allocator();
var fba: std.heap.FixedBufferAllocator = undefined;
const fbaa = fba.allocator();
break :blk [_]*const anyopaque{
@ptrCast(@alignCast(aaa.vtable.alloc)),
@ptrCast(@alignCast(fbaa.vtable.alloc)),
};
};

fn fastDeinitAllowed(self: *Self) bool {
const ourAllocAddr: *const anyopaque = @ptrCast(@alignCast(self.a.vtable.alloc));
inline for (allocAddrs) |ptr| {
if (ourAllocAddr == ptr) return true;
}
return false;
}

fn create(self: *Self) !Location {
const node = try self.a.create(Location.Node);
node.* = Location.Node.init();
Expand All @@ -125,13 +144,27 @@ fn locationCache(comptime K: type, comptime V: type, comptime Tags: type) type {
};
}

// Options defines some parameters of the tree.
// Options defines some comptime parameters of the tree type.
pub const Options = struct {
// countChildren, if set, enables children counts for every node of the tree.
// the number of children allows to locate a node by its position with a guaranteed complexity O(logn).
countChildren: bool = false,
};

// InitOptions defines some runtime parameters of the tree instance.
pub const InitOptions = struct {
// allowFastDeinit speeds up deinit() call by making it a no-op in cases
// where all the memory can be freed on the allocator level.
// normally, deinit() traverses the tree removing each node, however,
// this might not be necessary, if certain types of allocators are used.
// enum values:
// always - deinit() never deletes the nodes.
// auto - deinit() does not delete the nodes,
// if std.heap.ArenaAllocator or std.heap.FixedBufferAllocator are for allocations.
// never[default] - deinit() always deletes the nodes.
allowFastDeinit: enum { always, auto, never } = .never,
};

// Tree is a generic avl tree.
// AVL tree (https://en.wikipedia.org/wiki/AVL_tree) is a self-balancing binary search tree.
// For each node of the tree the heights of the left and right sub-trees differ by at most one.
Expand Down Expand Up @@ -353,6 +386,7 @@ pub fn TreeWithOptions(comptime K: type, comptime V: type, comptime Cmp: fn (a:
return b;
}

// Iterator traverses the tree.
pub const Iterator = struct {
tree: *Self,
loc: ?Location,
Expand Down Expand Up @@ -387,6 +421,7 @@ pub fn TreeWithOptions(comptime K: type, comptime V: type, comptime Cmp: fn (a:
}
};

io: InitOptions,
lc: Cache,
length: usize,
root: ?Location,
Expand All @@ -395,16 +430,29 @@ pub fn TreeWithOptions(comptime K: type, comptime V: type, comptime Cmp: fn (a:

// init initializes the tree.
pub fn init(a: std.mem.Allocator) Self {
return Self.initWithOptions(a, .{});
}

// initWithOptions initializes the tree.
pub fn initWithOptions(a: std.mem.Allocator, io: InitOptions) Self {
return Self{
.lc = Cache.init(a),
.length = 0,
.root = null,
.min = null,
.max = null,
.io = io,
};
}

// deinit releases the memory taken by all the nodes.
// Time complexity:
// O(1) - if fast deinit is enabled (see InitOptions.allowFastDeinit).
// O(n) - otherwise.
pub fn deinit(self: *Self) void {
if (self.io.allowFastDeinit == .always or self.io.allowFastDeinit == .auto and self.lc.fastDeinitAllowed()) {
return;
}
const min = self.min orelse return;
var loc = goLeftRight(min);
while (true) {
Expand Down Expand Up @@ -1407,6 +1455,58 @@ test "tree random" {
}
}

const failingFreeAllocator = struct {
ptr: *anyopaque,
vtable: std.mem.Allocator.VTable,

fn free(_: *anyopaque, _: []u8, _: u8, _: usize) void {
@panic("should not happen");
}

fn init(a: std.mem.Allocator) failingFreeAllocator {
return failingFreeAllocator{ .ptr = a.ptr, .vtable = .{
.alloc = a.vtable.alloc,
.free = free,
.resize = a.vtable.resize,
} };
}

fn allocator(self: *failingFreeAllocator) std.mem.Allocator {
return std.mem.Allocator{
.ptr = self.ptr,
.vtable = &self.vtable,
};
}
};

test "arena allocator: auto fast deinit" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
try testFastDeinit(.{ .allowFastDeinit = .auto }, arena.allocator());
}

test "arena allocator: always fast deinit" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
try testFastDeinit(.{ .allowFastDeinit = .always }, arena.allocator());
}

test "fixed buffer allocator: auto fast deinit" {
var buff: [512]u8 = undefined;
var fb = std.heap.FixedBufferAllocator.init(&buff);
try testFastDeinit(.{ .allowFastDeinit = .auto }, fb.allocator());
}

fn testFastDeinit(io: InitOptions, a: std.mem.Allocator) !void {
const TreeType = TreeWithOptions(i64, i64, i64Cmp, .{});
var ta: failingFreeAllocator = failingFreeAllocator.init(a);
var t = TreeType.initWithOptions(ta.allocator(), io);
defer t.deinit();
_ = try t.insert(0, 0);
_ = try t.insert(1, 1);
_ = try t.insert(2, 2);
}

fn checkHeightAndBalance(comptime T: type, loc: ?T.Location) !void {
_ = try recalcHeightAndBalance(T, loc);
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ const avl = @import("./avl.zig");
pub const Tree = avl.Tree;
pub const TreeWithOptions = avl.TreeWithOptions;
pub const Options = avl.Options;
pub const InitOptions = avl.InitOptions;
9 changes: 7 additions & 2 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ fn i64Cmp(a: i64, b: i64) math.Order {
test "test pub decls" {
const a = std.testing.allocator;
const TreeType = lib.Tree(i64, i64, i64Cmp);
var t = TreeType.init(a);
const options = lib.InitOptions{
.allowFastDeinit = .auto,
};
var aa = std.heap.ArenaAllocator.init(a);
defer aa.deinit();
var t = TreeType.initWithOptions(aa.allocator(), options);
defer t.deinit();
_ = try t.insert(0, 0);
var it = t.ascendFromStart();
Expand All @@ -29,7 +34,7 @@ test "tree example usage" {
defer _ = gpa.detectLeaks();
// first, create an i64-->i64 tree
const TreeType = lib.TreeWithOptions(i64, i64, i64Cmp, .{ .countChildren = true });
var t = TreeType.init(gpa.allocator());
var t = TreeType.initWithOptions(gpa.allocator(), .{ .allowFastDeinit = .auto });
defer t.deinit();
// add some elements
var i: i64 = 10;
Expand Down

0 comments on commit 197e7d1

Please sign in to comment.