-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[concurrency] Implement the Task allocator as bump-pointer allocator. #34880
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
Conversation
Cool! I was just implementing an allocator in the Swift runtime for use with automatic differentiation — I used |
@rxwei There are several differences, most importantly that the task allocator also can free memory (in a stack discipline). |
edfb23b
to
5eb6027
Compare
These features would be needed for AutoDiff as well. I’m interested in using this to replace the allocator in #34886. Do you think it can be moved to a header file that we can import? |
5eb6027
to
676cd8b
Compare
@swift-ci benchmark |
Performance: -O
Code size: -OPerformance: -Osize
Code size: -OsizePerformance: -OnoneCode size: -swiftlibsHow to read the dataThe tables contain differences in performance which are larger than 8% and differences in code size which are larger than 1%.If you see any unexpected regressions, you should consider fixing the Noise: Sometimes the performance results (not code size!) contain false Hardware Overview
|
676cd8b
to
367cc64
Compare
@swift-ci smoke test |
@rxwei makes sense. I moved it to a separate file. |
include/swift/ABI/Task.h
Outdated
/// Reserved for the use of the task-local stack allocator and it's first | ||
/// slab. | ||
/// TODO: decide what's the optimal size, e.g. based on typical memory needs. | ||
void *AllocatorPrivate[64]; |
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.
Every task, including child tasks, will have these 64 words of storage. I had thought of child tasks as being fairly lightweight abstractions, but this makes them significantly larger. Is that okay?
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.
Wouldn't every task need at least one context allocation? If so, then this inline-buffer avoids an malloc call.
The size is TBD, though. 64 is just an initial random number.
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.
"its"
I think this initial slab buffer should not be in the main AsyncTask
structure, which is ABI. If you want to have swift_task_create
preallocate some space and then hand it off to the allocator during initialization, that seems fine, but you should figure out a runtime-private way of doing it. That does mean it won't be tail to the allocator, but I think you should be able to deal with that.
@swift-ci smoke test linux |
include/swift/ABI/Task.h
Outdated
/// Reserved for the use of the task-local stack allocator and it's first | ||
/// slab. | ||
/// TODO: decide what's the optimal size, e.g. based on typical memory needs. | ||
void *AllocatorPrivate[64]; |
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.
"its"
I think this initial slab buffer should not be in the main AsyncTask
structure, which is ABI. If you want to have swift_task_create
preallocate some space and then hand it off to the allocator during initialization, that seems fine, but you should figure out a runtime-private way of doing it. That does mean it won't be tail to the allocator, but I think you should be able to deal with that.
unittests/runtime/TaskAlloc.cpp
Outdated
@@ -0,0 +1,76 @@ | |||
//===--- TaskStatus.cpp - Unit tests for the task-status API --------------===// |
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.
TaskAlloc.cpp
/// A single linked list of all allocated slaps. | ||
Slab *next = nullptr; | ||
|
||
// Capacity and offset do not include this header fields. |
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.
these
/// This struct is actually just the slab header. The slab buffer is tail | ||
/// allocated after Slab. | ||
struct Slab { | ||
/// A single linked list of all allocated slaps. |
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.
slabs
|
||
// No space in the next slab. Although it's empty, the size exceeds its | ||
// capacity. | ||
// As we have to allocate a new slab anyway, free all successor slaps |
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.
slabs
|
||
// Return a slab which is suitable to allocate \p size memory. | ||
Slab *getSlabForAllocation(size_t size) { | ||
Slab *slab = (lastAllocation ? lastAllocation->slab : getFirstSlab()); |
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.
IIUC, the linked list of slabs starts at the pre-allocated slab, and then next
points to the slab allocated after the last, right? So starting at lastAllocation->slab
means you actually don't check to see if there's any space in any of the older slabs. Is that intentional? It's certainly possible that you have plenty of space for small allocations in older slabs but you needed a big new slab for the last one. But maybe not looking in them is the right trade-off.
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.
@rjmccall Yes, this is intention. As you said, it's a trade-off.
367cc64
to
8220224
Compare
I pushed a new version, which now does not use a pre-allocated first slab - to keep the task allocation small. |
@swift-ci test |
Build failed |
Build failed |
8220224
to
635a89c
Compare
@swift-ci test |
Build failed |
Build failed |
A StackAllocator performs fast allocation and deallocation of memory by implementing a bump-pointer allocation strategy. In contrast to a pure bump-pointer allocator, it's possible to free memory. Allocations and deallocations must follow a strict stack discipline. In general, slabs which become unused are _not_ freed, but reused for subsequent allocations. The first slab can be placed into pre-allocated memory.
Use the StackAllocator as task allocator. TODO: we could pass an initial pre-allocated first slab to the allocator, which is allocated on the stack or with the parent task's allocator. rdar://problem/71157018
635a89c
to
ec1490d
Compare
@swift-ci test |
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.
LGTM
The task allocator is implemented by using the new StackAllocator utility.
A StackAllocator performs fast allocation and deallocation of memory by implementing a bump-pointer allocation strategy. In contrast to a pure bump-pointer allocator, it's possible to free memory.
Allocations and deallocations must follow a strict stack discipline.
In general, slabs which become unused are not freed, but reused for subsequent allocations. The first slab is tail allocated to the StackAllocator.
TODO: we could pass an initial pre-allocated first slab to the allocator, which is allocated on the stack or with the parent task's allocator.
rdar://problem/71157018