Repository navigation
ENH: cache large memory allocations on systems with MADV_FREE - #8783
juliantaylor wants to merge 2 commits into
Conversation
afe2754 to
1563351
Compare
|
So if I understand correctly, if we do (1) allocate a large array, (2) free the large array, (3) allocate a small array, (4) free the small array, then we end up with a pointer in the cache that points to a large amount of address space, but we've "lost track" of most of that and think it's a pointer to a small amount of address space, right? Are we worried about the impact of this kind of address space leakage on 32-bit systems? ...also this is vague, but I wonder if we should be worried at all about transparent hugepages screwiness? |
|
Indeed on 32 bit it should be disabled as this does waste address space. |
|
I'm going to schedule this for 1.14. I'd like to branch 1.13 soon and it is already loaded with risk. |
|
A naive question, can these functions be called by multiple threads? Is thread safety a concern? In general I have a hard time understanding what can or cannot be safely called by multiple threads, and the GIL is another layer of complexity/protection. |
|
Currently these functions must be called under a lock, namely the GIL. |
|
☔ The latest upstream changes (presumably #8907) made this pull request unmergeable. Please resolve the merge conflicts. |
replace path seperators with underscore
The |
Large allocations on glibc systems need to be faulted into the process before use each use. This can account for a significant fraction of the runtime of frequent array allocation and deallocation. This can be avoidded by caching used memory blocks inside numpy for later reuse. To not waste the memory in the cache we mark the memory as reclaimable by other processes (or numpy itself) with MADV_FREE. Should we try to use memory claimed by other processes we will receive newly faulted memory instead just as if it were freed normally. MADV_FREE is supported on Linux >= 4.5 and BSD. The cache is intentionally very simple queue storing the last large deallocations in a queue. New allocations pick the first block of memory that matches in size from the cache. It does not pick larger blocks, while the memory can be reclaimed by other processes it is not available for the page cache used to buffer IO. On each deallocation the cache is aged and if no entries have been removed for too long the oldest memory block is properly deallocated to avoid that large unused blocks stay in the cache too long. In most cases where it is relevant are fast allocation/deallocation cycles that appear in operations with temporaries or indexing. A simple small cache is sufficient for this. It will cache at most half of the available physical memory so that there is always enough free to be used in the page cache.
|
The code is still quite messy and still needs some changing (e.g. the exposed function is just for testing for now) but I think this approach should work. I have changed it to address that the uncommitted memory is available for other processes but not for the page cache. |
|
@juliantaylor I'm going to push this off to 1.15 as it sounds like it needs more work and time, |
|
well it needs review, the code cleanup is minor and can be done when agreed this concept works for all. |
| #else | ||
| #define PyTraceMalloc_Track(...) | ||
| #define PyTraceMalloc_Track(...) -2 | ||
| #define PyTraceMalloc_Untrack(...) |
|
@juliantaylor Needs rebase. |
|
This seems to be languishing. I want to do 1.15 soon, so going to push this off to 1.16. |
|
Delaying it is fine but some opinions on whether we want this in or not would be useful. Maybe I should post it to the list? |
|
Is this code/approach freshly developed for NumPy or has it been battle tested elsewhere? It seems like we need some kind of stress tests to make sure the cache works as advertised. I don't know enough about kernel internals to have an opinion on the impact this code might have on security, stability, and performance. Maybe there could be a switch or environment variable to control cache use, so a single binary could be run in various modes, and we could default to having it off for a release or two, then turn it on? |
|
I hate to keep pushing this off, but am going to do it again. @juliantaylor Needs a rebase. I think it would also help if it could be demonstrated to improve performance in common cases. As to threading safety, is Python threading (GIL) the only thing we need worry about? The libraries we use certainly do threading. There is currently other caching as well in Python itself, so I wonder if this will play well with that. |
| * Large memory allocation cache. | ||
| * Large allocations on glibc systems need to be faulted into the process | ||
| * before use each use. This can account for a significant fraction of the | ||
| * runtime of frequent array allocation and deallocation. This can be avoidded |
There was a problem hiding this comment.
| * runtime of frequent array allocation and deallocation. This can be avoidded | |
| * runtime of frequent array allocation and deallocation. This can be avoided |
|
I'm going to close this. @juliantaylor If you want to continue this line of thought please open a new PR. |
still a prototype missing documentation and cleanup, but should be usable to test how effective it is in practice.