Dynamically select memory range(s) for MMTk's heap
Currently, in both Map32 and Map64, we assume MMTk occupies a predefined memory range. The default range for Map32 is 0x8000_0000 to 0xd000_0000, and the default range for Map64 is 0x200_0000_0000 to 0x2200_0000_0000. The generous supply of heap space allows Map64 to implement SFT efficiently, and allows all side metadata to be stored contiguously.
But in real-world applications, MMTk is not free to use the entire address space. The application may arrange its address space for its own specific need, and multiple MMTk instances will compete for the same address range.
I propose we introduce external memory sources to MMTk so that it can request memory from external sources, e.g. the OS via the mmap syscall, or the VM.
Update: Alternatively, MMTk can inspect the /proc/pid/maps and figure out one (or more) proper memory range(s). That is equivalent to calling mmap and letting the OS decide for MMTk.
Note: This is not intended to replace the existing Map32 or Map64, but provide a complement to those.
Existing approaches
CRuby's default GC
CRuby's default GC organizes the heap into 64KB blocks (CRuby calls it "page", but I'll keep using the term "block" to be consistent with MMTk). Each individual block is allocated from mmap or posix_memalign. CRuby maintains both arrays and linked lists of blocks for different purposes. The side metadata are stored in the beginning of each block. (Note that CRuby's max object size is 640B, so a 64K block is almost as big as a chunk for CRuby.)
I am not suggesting that MMTk should externalize the allocation of blocks. It would be too inefficient if we call mmap that often.
OpenJDK
TODO
Suggested interface
trait ExternalChunkResource {
/// Allocate contiguous range of `num_chunks` chunks. Must be aligned to chunk.
fn allocate_contiguous_chunks(num_chunks: usize) -> Option<Address>;
/// Free an allocated contiguous range of chunks.
fn free_contiguous_chunks(chunk_group: Address);
}
It should be as simple as that, and that's essentially what Map32 does. mmtk-core will have to maintain its own list/array/vector of acquired "contiguous chunkses", which is currently done by Map32Inner::prev_link and Map32Inner::next_link.
Using this approach, all side metadata will have to be discontiguous, including global metadata. Then "global" will simply mean it exists in every space, but a global side metadata may end up being in each chunk.
We can still have SFT and a chunk-grained Mmapper (or more precisely a MapStateStorage as described in https://github.com/mmtk/mmtk-core/pull/1345) because the interface demands the chunks are aligned. We can still implement is_mmtk_object using SFT and Mmapper, or alternatively we can do linear or binary search on allocated "contiguous chunkses" to find the chunk an address belongs to.
FreeListPageResource may need to be re-implemented so that it no longer assumes all chunks in all spaces form a contiguous memory range. Alternatively we replace FreeListPageResource with something else.
Alternatives
(Update) As we discussed, there is an important alternative to the above. We dynamically select the memory range at MMTk initialization time (either by looking at /proc/pid/maps or letting an external entity (such as the OS via mmap) decide the range), but we only request one contiguous range for MMTk's heap instead of requesting many discontiguous ranges on demand. This should be easier to implement and gives MMTk more control over the memory range, and makes certain things easier (such as side metadata). As a tradeoff, once the range is determined, it cannot be extended at run time.
I propose we introduce external memory sources to MMTk so that it can request memory from external sources, e.g. the OS via the mmap syscall, or the VM.
Do you have any real use case in mind?
It sounds strange that someone mmaps memory but they don't allocate into the memory themselves, and instead let MMTk use the memory. If they both mmap and allocate into the memory, then it is the VM space.
Do you have any real use case in mind?
Any process that has two different VMs that use MMTk will face this problem. One example is Android ART + DartVM.
And some VMs may have their own way to do mmapping. OpenJDK, for example, uses Universe::reserve_heap for its GC heaps. Under the hood, OpenJDK may choose to
// 1. Mapping backed by a file
// 2. Mapping backed by explicit large pages
// 3. Mapping backed by normal pages or transparent huge pages
The backing file can be specified by -XX:AllocateHeapAt=<path>. And the mmap address can be configured by the -XX:HeapBaseMinAddress command line option. Currently, the mmtk-openjdk binding is bypassing all those options.
It sounds strange that someone mmaps memory but they don't allocate into the memory themselves, and instead let MMTk use the memory. If they both mmap and allocate into the memory, then it is the VM space.
The main goal is simply getting rid of fixed heap address ranges in favor for dynamically allocated heap ranges. To achieve this, we just let someone else decide where the MMTk heap should be located. It could be the OS (mmap), the VM (Universe::reserve_heap) or libraries (aligned_alloc). I think it is OK if one part of the system mmap for another part. All forms of memory allocation have their granularity. We may have memory obtained from mmap, given to malloc, then given to mmtk, then given to a Java Object[], then one of its elements is given to a particular part of an application. With the "externalized" memory source, MMTk just chooses to sit lower in the allocation hierarchy.
The difference between this and the VM space is that MMTk still runs plans and enforces policies (ImmixSpace, LOS, CopySpace, ...) on those memory. MMTk still does object-level allocation, but some external parts are responsible for chunk-level or multi-chunk-level allocations (or even coarser grains such as 1024 chunks, where some chunks are used for objects and others are used for side metadata). On the contrary, the VM space enforces no policy. The VM is responsible for allocating objects in it and freeing memory if needed.
I think a better way to think about this is what do we need to do in order to let MMTk have a dynamic heap range. In other words, how to make it tolerant to mmap errors. One of the main reasons why we currently assume we have a fixed heap range is that our metadata calculation requires it (for example, we have a fixed metadata base address). So, first, we would need to have a new metadata implementation (ideally not replacing the existing one) that has a variable base address that is stashed in the TLS. This can allow us to move the heap anywhere since the metadata is now independent of the fixed base address.
The main goal is simply getting rid of fixed heap address ranges in favor for dynamically allocated heap ranges.
We should be able to set heap start and heap end by using VMLayout: https://github.com/mmtk/mmtk-core/blob/31a78a41f02fc7228780b501c4944ba750e32ee4/src/util/heap/layout/vm_layout.rs#L32-L35
I don't think we need an 'external VMMap' to solve it.
In other words, how to make it tolerant to mmap errors.
This is a related issue, but not the same thing.
One of the main reasons why we currently assume we have a fixed heap range is that our metadata calculation requires it (for example, we have a fixed metadata base address).
Metadata base address should be not hard to fix. We can just change the constant base address to a static variable (and measure whether this has any impact on performance).
I think a better way to think about this is what do we need to do in order to let MMTk have a dynamic heap range.
Yes. That's the point.
In other words, how to make it tolerant to
mmaperrors.
No. Supporting dynamic heap range is not equivalent to tolerating mmap errors. mmtk-core doesn't need to call mmap by itself. Even if it offloads the multi-chunk-level memory allocation to external entities, they don't have to call mmap, either. They can use malloc. They can even mmap a whole large memory area, and selectively give different portions of the memory to MMTk or another VM in the same process. In this way, MMTk can be oblivious to mmap altogether. In this use case, MMTk can still call "ensure_mapped" which is implemented as a no-op.
One of the main reasons why we currently assume we have a fixed heap range is that our metadata calculation requires it (for example, we have a fixed metadata base address).
Yes. We really need to change this. A single heap range will not be scalable.
So, first, we would need to have a new metadata implementation (ideally not replacing the existing one) that has a variable base address that is stashed in the TLS. This can allow us to move the heap anywhere since the metadata is now independent of the fixed base address.
Yes. We can slightly modify our existing "chunked metadata" to get the new implementation. We may store the metadata at the beginning of a chunk instead of in a dedicated area, but I think it should be a minor modification.
We should be able to set heap start and heap end by using
VMLayout: I don't think we need an 'external VMMap' to solve it.
If we set the start and the end addresses of the heap, then the heap will be confined within that range.
We may argue that the region may be sufficiently large so that it will always be enough. But in reality it is still not ideal. If a process contains multiple VMs, maybe one VM consume terabytes of memory while other VMs each cosumes a few megabytes. And the actual memory usage depends on the program input. I expect the heap space can scale from somewhere near 4MB to several TB with default configuration (dynamic heap size). So it is not desirable to statically allocate any amount of memory in advance to any particular VM in a process.
And we never know what application the user runs in a VM. For example, what if the user of a CRuby interpreter (not CRuby or MMTk developer) wants to run an OpenJDK instance in the same processs? Neither the developers of CRuby nor OpenJDK would know where their heaps will be located in advance, and it is undesirable if CRuby and OpenJDK can't run together unless the user manually specifies the heap range.
The best strategy is that both CRuby and OpenJDK will ask a third party (such as the OS, via mmap) to allocate the initial chunks for their min heap size, and then CRuby and OpenJDK can independently call mmap to acquire more chunks if their heaps grow. If the application is Java-heavy, OpenJDK will mmap more chunks, and if it is Ruby-heavy, CRuby will mmap more chunks. The advantage is simply that mmap just works. The OS decides where the chunks will be located, and guarantees memory return from mmap(NULL, ...) don't overlap with existing mmaped ranges. With proper changes to mmtk-core, both the CRuby and the OpenJDK VM in the example should be able to adapt to chunks allocated at random locations.
If we set the start and the end addresses of the heap, then the heap will be confined within that range.
Yes. MMTk manages its heap, there will be a bound for the heap range. What is the problem?
We may argue that the region may be sufficiently large so that it will always be enough. But in reality it is still not ideal. If a process contains multiple VMs, maybe one VM consume terabytes of memory while other VMs each cosumes a few megabytes. And the actual memory usage depends on the program input. I expect the heap space can scale from somewhere near 4MB to several TB with default configuration (dynamic heap size). So it is not desirable to statically allocate any amount of memory in advance to any particular VM in a process.
I don't understand your argument. The heap range is virtual memory address. It has nothing to do with the actual memory usage. Especially in 64 bits settings, we assume virtual memory address is unlimited.
For example, what if the user of a CRuby interpreter (not CRuby or MMTk developer) wants to run an OpenJDK instance in the same processs?
Thus I asked earlier if you have a real use case. Running CRuby and OpenJDK in the same process is a highly hypothetical scenario, not a real use case.
The best strategy is that ...
I think the best strategy is giving each MMTk instance a separate address range, each instance will allocate and manage memory within their own range -- this seems to be a cleaner solution.
ask a third party (such as the OS, via mmap)
Honestly I don't know if I understand you. MMTK calls mmap to OS. We don't consider OS is a 'third party'. There must be something that we are not communicating.
To me, the only reason we may want a 'external vmmap/mmapper' is that the runtime really wants to do the mmap itself, and doesn't want MMTk to do mmapping for some reason. But I cannot think up why a runtime would require so.
All the arguments in the post seem to be related with multiple MMTk instances:
- We have a separate issue for supporting MMTk instances. Related discussions should go there: https://github.com/mmtk/mmtk-core/issues/100
- I don't think we need the 'external vmmap/mmapper' to support multiple instances.
- The reason that we haven't started working on supporting multiple instances is that we do not have a use case that requires multiple instances. Thus all the discussions or any implementation cannot be verified as correct if we don't have a use case for it.
- I think the current mmapper/vmmap might be global across multiple instances. It is stated in the following code. However, I am not sure if that's a correct design or not, as I said, there is no use case for us to test our idea at the moment. https://github.com/mmtk/mmtk-core/blob/31a78a41f02fc7228780b501c4944ba750e32ee4/src/mmtk.rs#L39
If we set the start and the end addresses of the heap, then the heap will be confined within that range.
Yes. MMTk manages its heap, there will be a bound for the heap range. What is the problem?
Having bounds will limit the scalability. In theory the bounds should be 0x0000_0000_0000 to 0xffff_ffff_ffff and MMTk should be able to use as much memory as there are physically available. Limiting it to 0x2200_0000_0000 is a restriction.
We may argue that the region may be sufficiently large so that it will always be enough. But in reality it is still not ideal. If a process contains multiple VMs, maybe one VM consume terabytes of memory while other VMs each cosumes a few megabytes. And the actual memory usage depends on the program input. I expect the heap space can scale from somewhere near 4MB to several TB with default configuration (dynamic heap size). So it is not desirable to statically allocate any amount of memory in advance to any particular VM in a process.
I don't understand your argument. The heap range is virtual memory address. It has nothing to do with the actual memory usage. Especially in 64 bits settings, we assume virtual memory address is unlimited.
They conflict. Multiple MMTk instances may conflict. MMTk and third-party libraries may conflict. No. 64-bit address space (actually only 47-bit virtual address space for x86_64) is not unlimited. If every component reserves 0x2000_0000_0000 bytes, the address space can be used up in no time.
And here is an example, the /proc/pic/maps for a Ruby test case. Just see that the last side metadata [anon:mmtk:sidemeta:los:VMLocalLOSMarkNurserySpec] is already after the /home/wks/projects/mmtk-github/ruby/build-release/ruby executable. If ASLR shifts the executable a bit, it will conflict with one of the side metadata. Remember that the executable is mmap-ed first before MMTk core maps its memory. I feel lucky that it is working.
20000000000-20000010000 rw-p 00000000 00:00 0 [anon:mmtk:misc:RawMemoryFreeList]
200ffc00000-20101000000 rw-p 00000000 00:00 0 [anon:mmtk:space:immix]
60000000000-60000010000 rw-p 00000000 00:00 0 [anon:mmtk:misc:RawMemoryFreeList]
600ffc00000-60100400000 rw-p 00000000 00:00 0 [anon:mmtk:space:los]
c0800000000-c0803c00000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VO_BIT]
c0803c00000-c0804400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VO_BIT]
c0804400000-c1000000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VO_BIT]
c1000000000-c1800000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immortal:VO_BIT]
c1800000000-c1803c00000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:los:VO_BIT]
c1803c00000-c1804400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:los:VO_BIT]
c1804400000-c2000000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:los:VO_BIT]
c2000000000-c2800000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:nonmoving:VO_BIT]
e0002000000-e0002400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:CHUNK_MARK]
e0804000000-e0807c00000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMGlobalLogBitSpec]
e0807c00000-e0808400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMGlobalLogBitSpec]
e0808400000-e1004000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMGlobalLogBitSpec]
e1004000000-e1804000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immortal:VMGlobalLogBitSpec]
e1804000000-e1807c00000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:los:VMGlobalLogBitSpec]
e1807c00000-e1808400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:los:VMGlobalLogBitSpec]
e1808400000-e2004000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:los:VMGlobalLogBitSpec]
e2004000000-e2804000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:nonmoving:VMGlobalLogBitSpec]
4e0a00000000-4e0a00c00000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:IX_LINE_MARK]
4e0a00c00000-4e0a01400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:IX_LINE_MARK]
4e0a01400000-4e0c00000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:IX_LINE_MARK]
4e1000000000-4e1200000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:nonmoving:IX_LINE_MARK]
4e8804000000-4e8804400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:IX_BLOCK_DEFRAG]
4e8804400000-4e8808000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:IX_BLOCK_DEFRAG]
4e8810000000-4e8814000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:nonmoving:IX_BLOCK_DEFRAG]
4e8904000000-4e8904400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:IX_BLOCK_MARK]
4e8904400000-4e8908000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:IX_BLOCK_MARK]
4e8910000000-4e8914000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:nonmoving:IX_BLOCK_MARK]
4eba80000000-4eba87c00000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalForwardingBitsSpec]
4eba87c00000-4eba88400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalForwardingBitsSpec]
4eba88400000-4eca80000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalForwardingBitsSpec]
4eea80000000-4efa80000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:nonmoving:VMLocalForwardingBitsSpec]
52b280000000-52b283c00000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalMarkBitSpec]
52b283c00000-52b284400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalMarkBitSpec]
52b284400000-52ba80000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalMarkBitSpec]
52ba80000000-52c280000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immortal:VMLocalMarkBitSpec]
52ca80000000-52d280000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:nonmoving:VMLocalMarkBitSpec]
54b280000000-54b283c00000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalPinningBitSpec]
54b283c00000-54b284400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalPinningBitSpec]
54b284400000-54ba80000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:immix:VMLocalPinningBitSpec]
54ca80000000-54d280000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:nonmoving:VMLocalPinningBitSpec]
55e01a983000-55e01ac46000 r-xp 00000000 00:00 0 [anon:Ruby:rb_yjit_reserve_addr_space]
55e01ac46000-55e022983000 ---p 00000000 00:00 0 [anon:Ruby:rb_yjit_reserve_addr_space]
55e022aee000-55e022b31000 r--p 00000000 103:03 87456374 /home/wks/projects/mmtk-github/ruby/build-release/ruby
55e022b31000-55e02303e000 r-xp 00043000 103:03 87456374 /home/wks/projects/mmtk-github/ruby/build-release/ruby
55e02303e000-55e02322a000 r--p 00550000 103:03 87456374 /home/wks/projects/mmtk-github/ruby/build-release/ruby
55e02322a000-55e023241000 r--p 0073b000 103:03 87456374 /home/wks/projects/mmtk-github/ruby/build-release/ruby
55e023241000-55e023244000 rw-p 00752000 103:03 87456374 /home/wks/projects/mmtk-github/ruby/build-release/ruby
55e023244000-55e02325a000 rw-p 00000000 00:00 0
55e02e925000-55e0320df000 rw-p 00000000 00:00 0 [heap]
56aa98000000-56aa98400000 rw-p 00000000 00:00 0 [anon:mmtk:sidemeta:los:VMLocalLOSMarkNurserySpec]
56aa98400000-56aaa0000000 ---p 00000000 00:00 0 [anon:mmtk:sidemeta:los:VMLocalLOSMarkNurserySpec]
7f97ac000000-7f97ac021000 rw-p 00000000 00:00 0
7f97ac021000-7f97b0000000 ---p 00000000 00:00 0
7f97b0000000-7f97b0021000 rw-p 00000000 00:00 0
7f97b0021000-7f97b4000000 ---p 00000000 00:00 0
...
For example, what if the user of a CRuby interpreter (not CRuby or MMTk developer) wants to run an OpenJDK instance in the same processs?
Thus I asked earlier if you have a real use case. Running CRuby and OpenJDK in the same process is a highly hypothetical scenario, not a real use case.
But Art and Dart is a real use case. But that's not the point. It's not the right cause and effect. We design for newer use cases so that MMTk can be used on other use cases, not the other way round. For example, the 4G mobile network is not designed for TikTok. TikTok only becomes a phenomenon after the 4G mobile network increased its bandwidth by 20x.
The best strategy is that ...
I think the best strategy is giving each MMTk instance a separate address range, each instance will allocate and manage memory within their own range -- this seems to be a cleaner solution.
Look at the current MMAP above. The 47-bit address space currently doesn't fit another MMTk instance, and doesn't work with another library that also assumes a 64-bit address space is "infinite". Then the immediate question is what default heap range should we provide then? There is just no answer. The most reasonable default is always "scaling dynamically".
ask a third party (such as the OS, via mmap)
Honestly I don't know if I understand you. MMTK calls mmap to OS. We don't consider OS is a 'third party'. There must be something that we are not communicating.
MMTk currently always calls mmap with a given address range. I meant calling mmap(NULL, ...) to let the OS (or anything that is not MMTk) decide where to put the heap.
To me, the only reason we may want a 'external vmmap/mmapper' is that the runtime really wants to do the mmap itself, and doesn't want MMTk to do mmapping for some reason. But I cannot think up why a runtime would require so.
All the arguments in the post seem to be related with multiple MMTk instances:
1. We have a separate issue for supporting MMTk instances. Related discussions should go there: [Support MMTk instances #100](https://github.com/mmtk/mmtk-core/issues/100) 2. I don't think we need the 'external vmmap/mmapper' to support multiple instances.
You are right in this part. External vmmap/mmapper is just one way to achieve it. That's why I don't want to completely replace our existing VMMap/Mmapper.
3. The reason that we haven't started working on supporting multiple instances is that we do not have a use case that requires multiple instances. Thus all the discussions or any implementation cannot be verified as correct if we don't have a use case for it.
We already have utilities that can annotate and view /proc/pid/mmap.
4. I think the current mmapper/vmmap might be global across multiple instances. It is stated in the following code. However, I am not sure if that's a correct design or not, as I said, there is no use case for us to test our idea at the moment.
Well, it is one way to do it.
All you said has nothing to do with 'external vmmap/mmapper'. All you just said is about that MMTk needs to be able to handle mmap failures, and handle more flexible heap range/side metadata range/etc. I agree with all those.
I disagree that the solution to those issues is an 'external vmmap/mmapper' which mmaps memory for MMTk to use.
Define what you mean by 'external vmmap/mapper'.
If you meant that the binding should implement the proposed ExternalChunkResource which mmaps memory for MMTk, I don't agree with this solution.
Define what you mean by 'external vmmap/mapper'.
If you meant that the binding should implement the proposed
ExternalChunkResourcewhich mmaps memory for MMTk, I don't agree with this solution.
An "external VMMap" is something outside mmtk-core that determines where chunks are located. mmap is implemented by the OS which is not mmtk-core, so it is one "external" source.
Both mmtk-core and the VM binding may implement it. MMTk can provide implementations (as Rust structs that implement the ExternalChunkResource) that wrap mmap or aligned_alloc. I still consider them "external" because the decision of where to put the chunks is made outside mmtk-core.
And the VM binding can implement ExternalChunkResource and it will mmap memory (or acquire memory from malloc or any other means) to be used by MMTk. OpenJDK, for example, can implement such thing by wrapping its existing mechanisms in its Universe class.
This is an extremely confusing way to phrase it imo. All you are trying to say is we need to make MMTk more tolerant to mmap failures which is something both Yi and I agree with. Essentially we should be using mmap(NULL, ...) to get memory from the OS for both objects and metadata. This is not controversial. Like I said earlier, there are a couple of things that need to be resolved before this can be possible, for example, the metadata layout/implementation.
Just want to say that many runtimes do actually have fixed address ranges, such as with ART. Their starting heap address is fixed. And specifically, the heap addresses are such that they all lie < 4 GB of the address space since ART uses 32-bit pointers. So we should still be able to support this model in case it is required by the execution environment.
I would make an even stronger argument. Fixed address ranges will simply not (always) work.
When a program starts, the first thing that manages the memory is the system linker/loader. It loads the executable and shared libraries by mapping them at addresses determined by the loader.
Then can libraries will map memory, too. That include malloc implementations which can map its [heap] anywhere, usually decided by the OS through anonymous mmap(NULL, ...) system calls.
Then the application can call malloc or even mmap to serve its own need.
The application may initialize MMTk at some point. At that point, there are already many things in the address space, and they are non-deterministic. And systems usually make them non-deterministic for ASLR. If MMTk demands any particular address range, mmap will fail.
All you said has nothing to do with 'external vmmap/mmapper'. All you just said is about that MMTk needs to be able to handle mmap failures, and handle more flexible heap range/side metadata range/etc. I agree with all those.
No. It's not about handling mmap failures. If, for example, the VMLocalPinningBitSpec side metadata is computed (by constants in mmtk-core) to locate at 0x5500_0000_0000, and that address is already mapped to the executable itself (such as /usr/bin/ruby), then how does mmtk-core handle the mmap failure? Retry won't work. As long as the MMTk instance expects the side metadata to be located at 0x5500_0000_0000, it won't be able to handle the failure because something else is mapped there and we can't unmap it.
Start-up-time configuration may workaround some failures, but not always. For example, if the user specifies the heap location on the command line so that the metadata is now at 0x4400_0000_0000, it may still conflict with something else. Maybe a shared library, maybe the malloc [heap], maybe a memory-mapped file opened by the application, etc. It may not always fail, depending on the mood of linker's ASLR. That's why we run JikesRVM tests with setarch -R.
I disagree that the solution to those issues is an 'external vmmap/mmapper' which mmaps memory for MMTk to use.
But here is the point. I think the best solution is letting an external entity, such as mmap(NULL...) implemented by the OS, decide where to place the MMTk heap. (If you insist that mmap is not "external", I am OK with that. We may be just interpreting the English word "external" differently.) I think mmap(NULL, ...) is a good candidate because it is probably also the thing that determined the addresses of other mappings in a process, such as the malloc [heap].
Of course mmtk-core can do the same thing by scanning the /proc/pid/maps file and find a nice gap for the chunks needed by MMTk. It is possible, and it's making decisions internally. But it's probably not necessary because mmap(NULL, ...) should work just fine.
This is an extremely confusing way to phrase it imo. All you are trying to say is we need to make MMTk more tolerant to
mmapfailures which is something both Yi and I agree with. Essentially we should be usingmmap(NULL, ...)to get memory from the OS for both objects and metadata. This is not controversial. Like I said earlier, there are a couple of things that need to be resolved before this can be possible, for example, the metadata layout/implementation.
Yes. Let me phrase it differently. There are multiple ways to determine where the MMTk heap is located.
- Statically determined. We use constants to make the spaces and side metadata locate at compile-time-determined locations.
- Configurable heap range. We let the binding specify a contiguous address range when initializing MMTk so that MMTk will put its spaces and side metadata in that range.
- Gradually acquire memory ranges. When MMTk needs more heap space, MMTk asks
mmap,mallocor other external sources for a range large enough to hold multiple (one, several, or thousands of) chunks. Alternatively, MMTk inspects/proc/pid/mapsto find a suitable memory range large enough for multiple chunks. In either way, the address space occupied by the MMTk heap is not a contiguous memory range. The ranges MMTk can use may interleave with other things in the process (such as the malloc[heap]or shared objects), but MMTk needs to manage them properly and efficiently.
I am arguing for Option (3). Maybe "external" is not the right word, but I think it is the easiest to let something outside mmtk-core (including mmap which I consider as part of the OS) decide where to locate the MMTk heap, or pieces after pieces of memory that belong to the MMTk heap.
I agree that we need to refactor the current metadata implementation to achieve that. And yes. This effort is in line with the goal of supporting multiple MMTk instances.
The inherent assumption here is that we should move to discontiguous spaces for everything in MMTk. This may-or-may-not be possible. But like I said, being able to configure the heap range is quite important for certain bindings, for example ART.
As @steveblackburn suggested, there is another option between (2) and (3).
Option 2.5: Configurable heap size. The binding specifies a max heap size (not range). When initializing MMTk, it figures out the needed memory range size, including spaces and metadata (may overestimate). Then MMTk either calls mmap(NULL, ...) to let the OS assign a contiguous memory range for MMTk, or inspects /proc/pid/maps and selects a vacant address range.
Similar to (2), but different to (3), option (2.5) gives MMTk one contiguous virtual address range. Unlike (2) but similar to (3), (2.5) selects the address range automatically according to the memory maps that are already existing in the process (i.e. current /proc/pid/maps) instead of forcing a fixed range.
Having a single address range for a MMTk instance makes certain things easier. For example, heap_start and heap_end is clear, and the Mmapper can be limited in size. It is straightforward to lay out side metadata because we know the address range. The disadvantage compared to (3) is that once the size is specified, it can't grow beyond that.
Option (3) is more flexible, but may come at the cost of implementation complexities and performance penalties. Without a bounded heap range, we may have to side metadata in front of each chunk because otherwise it will need a huge amount of virtual address space to cover an unbounded address range. But that may cause cache conflict problems in some CPUs because the same kind of metadata are always at the same offset from the start of a chunk. It is also hard to implement global field logging bits metadata for multi-chunk large objects.