-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support for custom allocator in ByteBuffer #1845
Conversation
Should this really be static? What if you want several different allocators? |
How would the api look like if this was per-instance ? |
Wait, I want a custom |
I guess all allocations would have to go through an allocator instance and not by simply using constructor. |
Nice one. Are you thinking |
Alright, so maybe AbstractAllocator: abstract class {
allocate: abstract func (size: SIzeT) -> Void*
deallocate: abstract func (pointer: Void*)
}
...
MallocAllocator: class extends AbstractAllocator {
allocate: override func (size: SizeT) -> Void* { malloc(size) }
deallocate: override func (pointer: Void*) { memfree(pointer) }
}
....
ByteBuffer: class {
...
_allocator := MallocAllocator new()
init: func ~withAllocator (size: SizeT, allocator: AbstractAllocator)
} should work ? |
@thomasfanell : I need that for some external library, but I have no idea what they are using under the hood. |
Would it be worth adding something like this then? AlignedMemoryAllocator: class extends AbstractAllocator {
alignment: SizeT
init: func (=alignment)
allocate: override func (size: SizeT) -> Void* { _mm_malloc(size, this alignment) }
deallocate: override func (pointer: Void*) { _mm_free(pointer) }
} |
I guess so, there is also |
You don't have to implement the Allocator instance here though, I was just thinking there will probably be times where you have different allocation requirements in different parts of the code, and then a static allocator will not be enough. |
Yeah, I agree. I'm almost done with this anyway. |
pointer ::= this _pointer | ||
size ::= this _size | ||
referenceCount ::= this _referenceCount | ||
|
||
init: func (=_pointer, =_size, ownsMemory := false) { | ||
init: func (=_pointer, =_size, ownsMemory := false, allocator := null as AbstractAllocator) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason allocator: AbstractAllocator = null
does not compile here, it is somehow affected by the previous false
default argument:
base/ByteBuffer.ooc:24:47 error Couldn't unwrap decl
false: AbstractAllocator
, trail =
_, Module ByteBuffer
_, ClassDecl ByteBuffer
_, ClassDecl ByteBufferClass
_, static ByteBuffer new(=_pointer, =_size, ownsMemory: = (false: AbstractAllocator, allocator: AbstractAllocator = null)) -> ByteBuffer
_, ownsMemory: = (false: AbstractAllocator, allocator: AbstractAllocator = null)
_, (false: AbstractAllocator, allocator: AbstractAllocator = null)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, rock does not handle this correctly: ooc-lang/rock#990
Updated. @thomasfanell can you review again ? |
this _pointer = null | ||
this _allocator free() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the allocator really be owned by the buffer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that's easiest for now. Allocator could also have a Owner
flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's quite natural for the buffer to take ownership of the allocator. @emilwestergren can you think of a case were this would be undesirable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not uncommon to have an allocator as a singleton for example. In most cases you will maybe have a few different allocators, and to allocate and free these for every ByteBuffer seems like a waste. We could for example have a few static standard allocators in the ByteBuffer or Allocator class.
buffer := ByteBuffer new(size, Allocator malloc)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and a possibility to override the default allocator, as my point here was to allow custom memory allocation for byte buffers in raster image classes. This is currently not possible, but for the sake of code quality I happily sacrifice my own personal short-sighted goals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point
Added |
this _allocCount += 1 | ||
malloc(size) | ||
} | ||
deallocate: override func (pointer: Void*) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Void*
is called Pointer
in ooc
|
||
import Owner | ||
|
||
AbstractAllocator: abstract class { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way is to simply have a concrete Allocator
class where you can supply allocate/deallocate closures. That way you don't have to inherit the class for every single allocator, only if you need to add more behaviour. Also, then we can add static default allocators in this class.
Allocator: class
mallocAllocator := static This new( func { malloc ... }, func { free... })
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I agree that allocating allocators for every object seems unnecessary.
I shall add default allocators now to prevent this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can also use template classes to create composable allocators (I'm thinking about Andrei Alexandrescu's design of the D std.allocator), although they are quite broken right now.
By the way, another nice way to expose allocators to any ooc code would be to add a global current_allocator
variable and use it to allocate and free (in Object alloc
and Object free
) although that means that you then have to keep track of what variables were allocated with what allocator etc.
With such a design, I expect you would use a single well tuned allocator for most allocations and switch allocators in specific places (for example, current_allocator = SomePoolAllocator; /* allocate thousands of small objects here, use them, free them */; current_allocator = SystemWideAllocator
).
EDIT: Also, you could then do stuff like that:
with_allocator(PoolAllocator, ||
// Do stuff
)
// Better (avoids closure)
switch_allocator(PoolAllocator)
// Do stuff
reset_allocator()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @Shamanas
use template classes
⬆️ that was the first thing I though of, but I gave up after fighting some compiler errors :/ This here is not perfect, but suits my needs for now. You have some nice ideas here, I think we may revisit this someday.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sebastianbaginski
Yeah template classes definitely do need a whole lot of patching, althoug hthe only error I can currently think of right now (which is pretty big itself) is that modules that define them need to import all modules of the types they are instanciated with.
Looks good to me, just had some ideas/opinions but this is fine anyhow. |
@thomasfanell @emilwestergren can you take a look at the latest changes ? |
Looks good to me |
Looks good |
free: static func ~all { _RecyclableByteBuffer _free~all() } | ||
free: static func ~all { | ||
_RecyclableByteBuffer _free~all() | ||
Allocator free~all() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it would be possible to use Allocator
for other purposes besides ByteBuffer
, I'd suggest not freeing it here but rather adding its own explicit register call to GlobalCleanup
.
...the only bothersome thing is that the order is important (it needs to happen after ByteBuffer free~all()
).
GlobalCleanup register(|| Allocator free~all(), true)
puts the call at the bottom of the (current) stack. If all else fails just put it after line 67 of this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I've tried that and the order was messed up. Maybe we could use a priority
integer value instead of bool
to control the cleanup order.
But ok, for now I can put it after the line 67.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks.
#1848
Adds possibility to allocate memory for byte buffers with something other than default
malloc
.@thomasfanell or @emilwestergren review