-
Notifications
You must be signed in to change notification settings - Fork 1
/
alloc.t
238 lines (208 loc) · 8.39 KB
/
alloc.t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
-- SPDX-FileCopyrightText: 2024 René Hiemstra <[email protected]>
-- SPDX-FileCopyrightText: 2024 Torsten Keßler <[email protected]>
--
-- SPDX-License-Identifier: MIT
--load 'terralibext' to enable raii
local C = terralib.includecstring[[
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
]]
-- terra does not import macros other than those that set a constant number
-- this causes an issue on macos, where 'stderr', etc are defined by referencing
-- to another implementation in a file. So we set them here manually.
if rawget(C, "stderr") == nil and rawget(C, "__stderrp") ~= nil then
rawset(C, "stderr", C.__stderrp)
end
if rawget(C, "stdin") == nil and rawget(C, "__stdinp") ~= nil then
rawset(C, "stdin", C.__stdinp)
end
if rawget(C, "stdout") == nil and rawget(C, "__stdoutp") ~= nil then
rawset(C, "stdout", C.__stdoutp)
end
local interface = require("interface")
local smartmem = require("smartmem")
local err = require("assert")
local size_t = uint64
--abstraction of opaque memory block used by allocators.
--allocators are factories for block
local block = smartmem.block
--allocator interface:
--'allocate' or 'deallocate' a memory block
--the 'owns' method enables composition of allocators
--and allows for a sanity check when 'deallocate' is called.
local Allocator = interface.Interface:new{
allocate = {size_t, size_t} -> {block},
reallocate = {&block, size_t, size_t} -> {},
deallocate = {&block} -> {},
owns = {&block} -> {bool}
}
--an allocator may also use one or more of the following options:
--Alignment (integer) -- use '0' for natural allignment and a multiple of '8' in case of custom alignment.
--Initialize (boolean) -- initialize memory to zero
--AbortOnError (boolean) -- abort behavior in case of unsuccessful allocation
local terra round_to_aligned(size : size_t, alignment : size_t) : size_t
return ((size + alignment - 1) / alignment) * alignment
end
local terra abort_on_error(ptr : &opaque, size : size_t)
if ptr==nil then
C.fprintf(C.stderr, "Cannot allocate memory for buffer of size %g GiB\n", 1.0 * size / 1024 / 1024 / 1024)
C.abort()
end
end
--Base class to facilitate implementation of allocators.
local function AllocatorBase(A, Imp)
terra A:owns(blk : &block) : bool
if blk:owns_resource() then
return [&opaque](self) == blk.alloc.data
end
return false
end
--single method that can free and reallocate memory
--this method is similar to the 'lua_Alloc' function,
--although we don't allow allocation here (yet).
--see also 'https://nullprogram.com/blog/2023/12/17/'
--a pointer to this method is set to block.alloc_f
terra A:__allocators_best_friend(blk : &block, elsize : size_t, counter : size_t)
var requested_size_in_bytes = elsize * counter
if not blk:isempty() then
if requested_size_in_bytes == 0 then
--free memory
self:deallocate(blk)
elseif requested_size_in_bytes > blk:size_in_bytes() then
--reallocate memory
self:reallocate(blk, elsize, counter)
end
end
end
terra A:deallocate(blk : &block)
err.assert(self:owns(blk))
Imp.__deallocate(blk)
end
terra A:reallocate(blk : &block, elsize : size_t, newcounter : size_t)
err.assert(self:owns(blk) and (blk:size_in_bytes() % elsize == 0))
if not blk:isempty() and (blk:size_in_bytes() < elsize * newcounter) then
Imp.__reallocate(blk, elsize, newcounter)
end
end
terra A:allocate(elsize : size_t, counter : size_t)
var blk = Imp.__allocate(elsize, counter)
blk.alloc = self
return blk
end
end
--implementation of the default allocator using malloc and free.
local DefaultAllocator = function(options)
--get input options
local options = options or {}
local Alignment = options["Alignment"] or 0 --Memory alignment for AVX512 == 64
local Initialize = options["Initialize"] or false -- initialize memory to zero
local AbortOnError = options["Abort on error"] or true -- abort behavior
--check input options
assert(Alignment >= 0 and Alignment % 8 == 0) --alignment is a multiple of 8 size_in_bytes
assert(type(Initialize) == "boolean")
assert(type(AbortOnError) == "boolean")
--static abort behavior
local __abortonerror = macro(function(ptr, size)
if AbortOnError then
return `abort_on_error(ptr, size)
end
end)
--low-level functions that need to be implemented
local Imp = {}
terra Imp.__allocate :: {size_t, size_t} -> {block}
terra Imp.__reallocate :: {&block, size_t, size_t} -> {}
terra Imp.__deallocate :: {&block} -> {}
if Alignment == 0 then --use natural alignment
if not Initialize then
terra Imp.__allocate(elsize : size_t, counter : size_t)
var size = elsize * counter
var ptr = C.malloc(size)
__abortonerror(ptr, size)
return block{ptr, size}
end
else --initialize to zero using 'calloc'
terra Imp.__allocate(elsize : size_t, counter : size_t)
var size = elsize * counter
var newcounter = round_to_aligned(size, elsize) / elsize
var ptr = C.calloc(newcounter, elsize)
__abortonerror(ptr, size)
return block{ptr, size}
end
end
else --use user defined alignment (multiple of 8 size_in_bytes)
if not Initialize then
terra Imp.__allocate(elsize : size_t, counter : size_t)
var size = elsize * counter
var ptr = C.aligned_alloc(Alignment, round_to_aligned(size, Alignment))
__abortonerror(ptr, size)
return block{ptr, size}
end
else --initialize to zero using 'memset'
terra Imp.__allocate(elsize : size_t, counter : size_t)
var size = elsize * counter
var len = round_to_aligned(size, Alignment)
var ptr = C.aligned_alloc(Alignment, len)
__abortonerror(ptr, size)
C.memset(ptr, 0, len)
return block{ptr, size}
end
end
end
if Alignment == 0 then
--use natural alignment provided by malloc/calloc/realloc
--reallocation is done using realloc
terra Imp.__reallocate(blk : &block, elsize : size_t, newcounter : size_t)
err.assert(blk:size_in_bytes() % elsize == 0) --sanity check
var newsize = elsize * newcounter
if blk:owns_resource() and (blk:size_in_bytes() < newsize) then
blk.ptr = C.realloc(blk.ptr, newsize)
blk.nbytes = newsize
__abortonerror(blk.ptr, newsize)
end
end
else
--use user defined alignment (multiple of 8 size_in_bytes)
--we just use __allocate to get correctly aligned memory
--and then memcpy
terra Imp.__reallocate(blk : &block, elsize : size_t, newcounter : size_t)
err.assert(blk:size_in_bytes() % elsize == 0) --sanity check
var newsize = elsize * newcounter
if not blk:isempty() and (blk:size_in_bytes() < newsize) then
--get new resource using '__allocate'
var tmpblk = __allocate(elsize, newcounter)
__abortonerror(tmpblk.ptr, newsize)
--copy size_in_bytes over
if not tmpblk:isempty() then
C.memcpy(tmpblk.ptr, blk.ptr, blk:size_in_bytes())
end
--free old resources
blk:__dtor()
--move resources
blk.ptr = tmpblk.ptr
blk.nbytes = newsize
blk.alloc = tmpblk.alloc
tmpblk:__init()
end
end
end
terra Imp.__deallocate(blk : &block)
C.free(blk.ptr)
blk:__init()
end
--local base class
local Base = function(A) AllocatorBase(A, Imp) end
--the default allocator
local struct default(Base){
}
--sanity check - is the allocator interface implemented
Allocator:isimplemented(default)
return default
end
return {
block = smartmem.block,
SmartBlock = smartmem.SmartBlock,
Allocator = Allocator,
AllocatorBase = AllocatorBase,
DefaultAllocator = DefaultAllocator
}