I think the author might be being a bit naive about the role of the allocator. Sure, it could aggressively madvise()/munmap() unused pages to return them to the kernel but as soon as memory is needed again there's a significant overhead to bring it back again. For desktop apps it might be fine to run with few spare pages (and save the RAM) but doing so on a busy server would likely result in different inefficiencies.
jemalloc used to have an option (opt.lg_dirty_mult: https://linux.die.net/man/3/jemalloc) to control how many dirty pages it'd allow to build up before calling madvise(.., MADV_FREE) to give them back to the OS. This allowed one to choose between saving CPU by maintaining free pages or saving RAM by giving them back. Later jemallocs switched to decay-based purging of dirty pages (via a dedicated thread) to get the best of both worlds.
There are arguments that GCs can be faster, yes, but that speed almost always comes with an increase in memory usage not a reduction. After all asynchronous periodic collection must necessarily increase the transient working set of memory used over immediate synchronous collection.
The advantages of a GC here in typical contexts are that GC'd languages usually allow for compaction which can reduce fragmentation. But this isn't an inherent benefit of a GC. You can have a GC'd language where you don't get this benefit, though, it's not an inherent property of the GC itself.
For example D is a GC'd language, but because D's allocations are allowed to interop with C libraries you can no longer do compaction. There's thoughts on how you could do it with automatic pining but it's not done currently. But either way it's really the lack of raw pointers that make compaction possible, not whether or not you have a GC.
Such are the tradeoffs with a conservative garbage collector.
I would avoid Boehm GC (and other conservative GCs) in almost all situations.
Yes, on 32-bit platform false pointers are a problem esp as you get closer to 1g for instance.
On the other hand on 64-bit I’ve never seen it at all. The chance of arbitrary long/double to point to some object in the heap area is 1/2^32 smaller and that turns the problem from common nuisance to purely theoretical problem.
So I would think this requires some compiler support or some sort of runtime.
If you know more about this or know any cool implementations of it please elaborate!
We already have this - almost all modern computers have something called virtual memory, that allows you to de-couple your memory addresses from where the memory actually is. This allows you to 'move' memory without changing the addresses that you use to refer to it. You can then defragment memory by mapping two sets of addresses to the same memory physical memory, as long as the holes in the two sets overlap.
In order words, if you have two pages, one with only the first half used, the other with only the second half used, you can copy the used part of the second page into the gap in the first page, and map the old addresses of the second page to the location of the first, now shared, page. You are using twice the address space (which costs just a few bytes in bookkeeping) but have defragmented so you're only using half the physical memory.
You can then completely return the second page, now un-used, to the kernel.
I would think the page level granularity of virtual memory makes it not that useful for this purpose, since the used and unused parts of the two pages would have to line up nicely and larger objects tend to cause less fragmentation.
Just to further clarify the terms if someone is looking for more info on this:
In the context of system memory people mostly refer to this as compaction and not defragmentation.
A core benefit of GC is you can have pretty complex memory management techniques that run more or less totally transparently. You can leverage generations (or defragmentation) to keep allocation latency low, etc.
Most userspace memory allocators are based on mmap(). Using sbrk() is legacy. The slab allocator (Bonwick, 1994) is the dominant algorithm (both in userspace and the UNIX-like kernels), although there are various flavours and somewhat different implementations of it. Since it uses fixed-sized allocations under the hood, they are nicely packed in pages. Those pages can be released with munmap() once there are no used blocks. Sure, depending on the application and workload, allocations can be become quite distributed amongst the pages over the time, but that is a separate problem.
Also, as another comment states: madvise() is also an option, but it doesn't reclaim the virtual address space. On 32-bit systems VA space exhaustion can be a problem, especially with "modern" applications.
Also you can verify in the code, e.g. https://git.musl-libc.org/cgit/musl/tree/src/malloc/expand_h...
I believe OpenBSD's malloc only uses mmap and also aggressively munmap's memory--to better catch application bugs and to keep addresses randomized.
Facts and to the point.
madvise( start, size, MADV_DONTNEED );
start must be page-aligned. size is in bytes, but if you take one byte of a page, you take the whole thing, so clip as appropriate.this informs the kernel you're done with some part of the memory. mapped things will be re-read and others will act as if zero'd when later accessed. both can be safely dropped without taking space in the swap, instead being newly created when next needed.
[1]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/p...
I guess I didn't see how long that operation takes though, but it could certainly be useful for long-running processes like web browsers to call it every once in a while...
What you can do is search through all allocated memory for full 4K pages which are unused, which can be "given back" to the OS.
However, it's unclear how much benefit there is from doing this. If you give back memory too eagerly, and then need to request more later, you end up thrashing the kernel. Assuming you have some swap space, this unused memory gets swapped out.
It could be (hard to tell, but I do this in my own memory managers) that the memory is technically still assigned to the program, but the pages are marked as unused, in which point there is no "memory" there at all, just address space which you can start using again later. However, it can still look like my program is using something crazy like 1TB of memory.
Glibc malloc can return memory to the OS. This is what the trim that people are talking about does.
Nevertheless I prefer jemalloc because I find the stats and memory profiling awesome to find leaks or structural over assignation.
It'z possible to use via .so injection with ld_preload.
Multithreaded programs often do not scale because the heap is a bottleneck.
When multiple threads simultaneously allocate or deallocate memory from the allocator, the allocator will serialize them. Programs making intensive use of the allocator actually slow down as the number of processors increases.
Malloc (libc) is the worse memory allocation API to use.
Programs should avoid, if possible, allocating/deallocations memory too often and in particular whenever a packet is received.
In the Linux kernel there are available kernel/driver patches for recycling skbuff (kernel memory used to store incoming/outgoing packets).
Using PF_RING (into the driver) for copying packets from the NIC to the circular buffer without any memory allocation increases the capture performance (around 10%) and reduces congestion issues.
Design Evolution
Basic design of malloc() is to dynamically pre-allocate a pool of memory from the OS in which applications can then take smaller pieces from. malloc() is a standard API having a choice of different allocation algorithms and to mitigate the expensive OS system calls (typically done at program initialization time) during allocation of its system memory. The first memory allocation scheme started with a stack-based memory allocation.
Next came the dynamic-based memory allocation scheme where linked-list and bucket-heap mechanism are used to divide the private-heap using size class approach.
Soon, garbage collection algorithm introduced the initial backend of the memory allocation scheme. Frontend covers the usual malloc() API, et. al.
In 2006, a third pool was introduced (after operating system memory pool and library-based memory pool) called the “arena”. Arena is a jemalloc-term and is intended to deal with different memory types such as different-speed memory bank or NUMA-architecture, as well as memory tied to specific to each of the multiple CPU core or even CPU infinity.
Frontend Evolution Frontend manages the memory being given to the application.
Within the frontend of the memory allocation system, the evolution went in the following order:
- link-list free space - heap-bucket size classes (eliminating an object header) - (Process) Owner encoding - single core local allocation buffers (CLABs) - Epoch encoding - Large-size class memory block by direct mmap() Hazard pointers (safe memory reclamation for lock-free objects) (M.M. Michael, 2004) - Arena memory pool - thread-specific local allocation buffers (TLABs) - constant-time modulo synchronization (early return to OS pool, or FreeBSD madvise call)
Backend Evolution
Backend of the memory allocation system manages the empty, straggling, fragmented or no-longer used memory blocks back to the OS (thereby reducing RSS).
- Pool semantic: Remote f-list encoding, using Treiber stack), (R.K. Treiber, 1986) - buddy algorithm - binary buddy algorithm - BIPOP Table (span-based allocator)(S. Schneider, 2006) aka local free list and - remote free list - segment queue (Quasi-linearizability, Y. Afek, 2010) - multi-core distributed queue (A. Haas, 2013) - k-FIFO queue (T.A. Henzinger, 2013)
Memory allocators designed for multithreaded use will not serialize allocations and frees. Such allocators use thread-local data structures and try to avoid touching non-thread-local structures that require synchronization. I am "S. Schneider, 2006" and that was one of the main points of that paper. Modern memory allocators (tcmalloc, jemalloc, even I believe modern glibc) follow similar designs.
Evolution doesn’t always means most efficient or better. :-)
It could be, but it's counter-intuitive to me that processes keeping unused memory in use is better for the system.
Even going over physical memory is not a death sentence anymore because of fast SSDs being used for virtual memory. Because that is slower however, freeing up memory will still speed everything up, even if the maximum memory used needs to go over physical memory.
The OS also caches memory mapped pages of files, so memory used means more disk IO from that standpoint as well.
Even disregarding all of that, heaps get fragmented, and if many virtual memory allocations can be given back to the OS, the next time more memory is needed it will be allocated in large, 'virtually contiguous' chunks that start out without fragmentation.
"Usually" has a well-defined meaning, readily available from your nearest English dictionary, that is rather opposite to "always".
If a pause in execution of other threads is tolerable, then taking it could be the right choice. But taking it every time is usually (that word again) the wrong choice. So, identify times where it is warranted, and do it just at those times.