Skip to content
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

WIP: Simplify lowest level compiler allocation classes #3396

Closed
wants to merge 3 commits into from

Conversation

mstoodle
Copy link
Contributor

This PR prereqs a change in OpenJ9 to avoid breaking OpenJ9, so
it has been marked WIP. Reviews are welcome at this stage.

The lowest level memory allocation classes in the OMR compiler
have a complicated hierarchy and relationship that many people
have struggled to understand and maintain. The complexity came
about trying to hold a unified picture of memory allocation
between OMR (which just uses a raw malloc allocator) and OpenJ9
(which uses J9MemorySegments allocated from the VM a.k.a.
"the system"). Unfortunately, this unity did not materialize
and so these lower level allocation classes are not actually
shared with OpenJ9.

The set of changes in this commit is motivated by the effort
to try to compile the basic JitBuilder files in compiler/ilgen
as part of OpenJ9. In this commit, I brought over the code from
OpenJ9's SystemSegmentProvider and refactored it so that it
could operate in OMR using TR::MemorySegments rather than
J9MemorySegments. I changed its name to SegmentAllocator and
declare the SegmentAllocator as the base layer for the higher
level allocation support for e.g. regions and STL and other
objects in the compiler component.

This commit is the first stage in a series of coordinated
changes to OMR and OpenJ9 to migrate from the "old" memory
allocation scheme to the "new" memor allocation scheme. In
this change, most of the new code is implemented inside
a new compiler/env/mem directory which contains any files
from compiler/env that required substantial modification.
This commit also introduces a temporary macro called,
uninspiringly, OLD_MEMORY: if OLD_MEMORY is defined then
the old OMR memory allocation classes will be built rather
than the new ones. By default after this commit, OMR will
build with the new memory allocation classes (e.g. into
jitbuilder and compilertest). Most consumers of the OMR
compiler should experience zero functional differences
as the memory classes being changed are at the lowest levels
of the compiler implementation (well lower than the facilities
most compiler developers use directly).

The SegmentAllocator will hold a BackingMemoryAllocator and a
RawAllocator. In fact, the BackingMemoryAllocator in OMR is
an only slightly beefed up RawAllocator which returns memory
wrapped in a TR::MemorySegment. I deleted the SegmentPool code
because nobody was using it. The DebugSegmentProvider code has
been moved into DebugRawAllocator (subclass of TR::RawAllocator)
as well as DebugSegmentAllocator (subclass of TR::SegmentAllocator).

I added some tracing output to the SegmentAllocator and
BackingMemoryAllocator to track segments as they are allocated
and deallocated (see MEMLOG macros in each file). This debug
output uses printfs due to the primordial environment of the
memory allocators, and is activated by environment variables
OMRDebug_BackingMemory=1 or 2 or 3 (increasing detail)
OMRDebug_SegmetnAllocator=1 or 2 or 3 (increasing detail)

While debugging the memory allocation layers, I discovered that
the original list management for segments was not very efficient
in that new segments were typically added at the end of lists but
searched starting from the beginning. This commit changes the
order of search to require fewer probes (less than half the
probes required in some very unscientific studies). I doubt
it will have any performance impact in most workloads but
it felt better to fix it.

BackingMemoryAllocator, SegmentAllocator, and DebugSegmentAllocator
are all extensible classes to facilitate a similar refactoring
extravaganza to be inflicted upon OpenJ9 very soon.

@mstoodle
Copy link
Contributor Author

genie-omr build all

@mstoodle
Copy link
Contributor Author

genie-omr build all

@mstoodle mstoodle force-pushed the simplifyMemory branch 5 times, most recently from 2768da9 to 224c7ae Compare December 24, 2018 01:15
@mstoodle
Copy link
Contributor Author

genie-omr build all

@mstoodle
Copy link
Contributor Author

genie-omr build all

@mstoodle
Copy link
Contributor Author

genie-omr build arm

@mstoodle
Copy link
Contributor Author

genie-omr build all

@mstoodle
Copy link
Contributor Author

Note to reviewers; I still have to update the compiler memory documentation in this PR.

debugSegmentAllocator : defaultSegmentAllocator;
#endif

TR::Region dispatchRegion(segmentAllocator, rawAllocator);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

segmentAllocator isn't defined when using OLD_MEMORY; I guess the variable is named segmentProvider.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the change below (ie using segmentProvider for OLD_MEMORY, and segmentAllocator for the new stuff), it seems like you're going to have to duplicate the initialization of the TR::Region in each of the ifdef sections

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, will fix

void *p = _rawAllocator.allocate(sizeToAllocate, hint);
TR::MemorySegment *newSegment = new (_rawAllocator) TR::MemorySegment(p, sizeToAllocate);
if (newSegment == NULL)
throw std::bad_alloc();
Copy link
Contributor

@dsouzai dsouzai Dec 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless std::nothrow_t tag is specified as a parameter to the placement new (which in this case it isn't), OMR::RawAllocator throws a std::bad_alloc(), so this is redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another good catch, will remove the check

break;
}
}
TR_ASSERT(found, "Request to deallocate segment not found in allocated segment list");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How "fatal" is it if we can't find the segment in this deque?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At best it's going to be a memory leak, and at worst I suppose it's a sign that memory corruption is likely happening in the compilation. I suppose a TR_ASSERT_FATAL would be a better option.

while (!_allocatedSegments.empty())
{
TR::MemorySegment &segment = _allocatedSegments.front().get();
deallocate(segment);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious as to whether BackingMemoryAllocator can be extensible, and if so, whether we want to enforce that the OMR version of this is guaranteed to get called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that's appropriate: if someone creates an extension of BackingMemoryAllocator that implements a different allocate() function then presumably they should have the ability to also provide a different deallocate() function that matches?

Someone who did that would probably want to further break down deallocate() so they could reuse some parts of it without necessarily using other parts (exercise left for those who need it :) ?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah fair enough; I figured I'd mention it jic :)

Copy link
Contributor

@dsouzai dsouzai Jan 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this a second time, wouldn't this result in the code going through the _allocatedSegments list for every iteration of the loop (since deallocate goes through the list to see if the segment is in the list)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, because they start at the same end of the list :). It asks what the head is, then removes the head (so deallocate just picks off the first segment). It might be possible to refactor deallocate to not rely on it and it probably deserves a comment, but i guess I went the easy way.

For the record, I found the list iteration in the earlier implementation, which I initially duplicated, was not as efficient as it should be, so I tweaked it a bit to make it better. So good catch :) .

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh I see, sneaky :P



TR::MemorySegment &
OMR::BackingMemoryAllocator::allocate(size_t requiredSize, void * hint)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method doesn't check the _allocationLimit even though that concept exists at the BackingMemoryAllocator level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um, yeah...good point :)

@dsouzai
Copy link
Contributor

dsouzai commented Dec 24, 2018

What is the reason OMRCompilerEnv is in the mem dir?

@@ -203,7 +208,8 @@ class TypeDictionary
MemoryManager();
~MemoryManager();

TR::SegmentProvider *_segmentProvider;
TR::BackingMemoryAllocator *_backingMemoryAllocator;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth having a comment here stating how this should never be used by anything but _segmentAllocator.

@@ -120,12 +123,14 @@ initializeJitBuilder(TR_RuntimeHelper *helperIDs, void **helperAddresses, int32_
// Create a bootstrap raw allocator.
//
TR::RawAllocator rawAllocator;
TR::BackingMemoryAllocator backingAllocator(rawAllocator, 1<< 20);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above. Although now that I think of it (and I'm not saying it should be done here), but since the backingAllocator and segmentAllocator seem to always go together at least during initialization, perhaps it's worth having them encapsulated in a struct so that the lifetimes of both are linked in places where it makes sense that they be linked?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don't have to be linked together. That's just how they're used currently....I don't believe there's a requirement for the BackingMemoryAllocator to die just because the SegmentAllocator dies...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe there's a requirement for the BackingMemoryAllocator to die just because the SegmentAllocator dies...

Yeah that's a good point; I guess I was just looking at the current uses of it, and it seems to have the same lifetime as the SegmentAllocator. That said, there's no reason that in the future you couldn't initialize another SegmentAllocator once the first one has been destroyed. Still, it's worth maybe putting some warning comments to alert people to the fact that this is a very subtle piece of code.

Copy link
Contributor

@dsouzai dsouzai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation lgtm (aside from the comments already made)

@dsouzai
Copy link
Contributor

dsouzai commented Jan 7, 2019

Btw, forgot to mention earlier, but the copyrights need to be updated :)

@mstoodle
Copy link
Contributor Author

mstoodle commented Jan 7, 2019

Ah, now I understand the source of your question: the mem directory is temporary (yes, poorly named but hopefully not long lived): it holds the files requiring substantial changes between "old" and "new" memory management.

@mstoodle mstoodle force-pushed the simplifyMemory branch 2 times, most recently from c1394f5 to 69880ab Compare January 7, 2019 22:10
Copy link
Contributor

@dsouzai dsouzai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

doc/compiler/memory/MemoryManager.md Outdated Show resolved Hide resolved
doc/compiler/memory/MemoryManager.md Outdated Show resolved Hide resolved
doc/compiler/memory/MemoryManager.md Outdated Show resolved Hide resolved
doc/compiler/memory/MemoryManager.md Show resolved Hide resolved
doc/compiler/memory/MemoryManager.md Outdated Show resolved Hide resolved
RawAllocator classes:
+----------------------------+ +-----------------------------+
| | using | |
| TR::RawAllocator +<--------+ OMR::RawAllocator |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the word using a bit confusing here. Is it referring to the fact that OMR::RawAllocator should be "used" via TR::RawAllocator? Why not just show the inheritance relationship instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see a good way to represent it, so "confusing" is a perfectly apt word to use.

OMR::RawAllocator is placed into the TR namespace via a using statement:
namespace TR { using OMR::RawAllocator };

doc/compiler/memory/MemoryManager.md Outdated Show resolved Hide resolved
doc/compiler/memory/MemoryManager.md Outdated Show resolved Hide resolved
doc/compiler/memory/MemoryManager.md Outdated Show resolved Hide resolved
@mstoodle
Copy link
Contributor Author

mstoodle commented Jan 8, 2019

genie-omr build all

@mstoodle
Copy link
Contributor Author

mstoodle commented Jan 8, 2019

genie-omr build all

@mstoodle
Copy link
Contributor Author

mstoodle commented Jan 8, 2019

genie-omr build all

The lowest level memory allocation classes in the OMR compiler
have a complicated hierarchy and relationship that many people
have struggled to understand and maintain. The complexity came
about trying to hold a unified picture of memory allocation
between OMR (which just uses a raw malloc allocator) and OpenJ9
(which uses J9MemorySegments allocated from the VM a.k.a.
"the system"). Unfortunately, this unity did not materialize
and so these lower level allocation classes are not actually
shared with OpenJ9.

The set of changes in this commit is motivated by the effort
to try to compile the basic JitBuilder files in compiler/ilgen
as part of OpenJ9. In this commit, I brought over the code from
OpenJ9's SystemSegmentProvider and refactored it so that it
could operate in OMR using TR::MemorySegments rather than
J9MemorySegments. I changed its name to SegmentAllocator and
declare the SegmentAllocator as the base layer for the higher
level allocation support for e.g. regions and STL and other
objects in the compiler component.

This commit is the first stage in a series of coordinated
changes to OMR and OpenJ9 to migrate from the "old" memory
allocation scheme to the "new" memor allocation scheme. In
this change, *most* of the new code is implemented inside
a new compiler/env/mem directory which contains any files
from compiler/env that required substantial modification.
This commit also introduces a temporary macro called,
uninspiringly, OLD_MEMORY: if OLD_MEMORY is defined then
the old OMR memory allocation classes will be built rather
than the new ones. By default after this commit, OMR will
build with the *new* memory allocation classes (e.g. into
jitbuilder and compilertest). Most consumers of the OMR
compiler should experience *zero* functional differences
as the memory classes being changed are at the lowest levels
of the compiler implementation (well below than the facilities
most compiler developers use directly).

The SegmentAllocator will hold a BackingMemoryAllocator which
will itself hold a RawAllocator.  In fact, the
BackingMemoryAllocator in OMR is an only slightly beefed up
RawAllocator which returns memory wrapped in a TR::MemorySegment.
The SegmentPool code was removed because nobody was using it.
he DebugSegmentProvider code has been moved into DebugRawAllocator
(subclass of TR::RawAllocator) as well as DebugSegmentAllocator
(subclass of TR::SegmentAllocator).

I added some tracing output to the SegmentAllocator and
BackingMemoryAllocator to track segments as they are allocated
and deallocated (see MEMLOG macros in each file). This debug
output uses printfs due to the primordial environment of the
memory allocators, and is activated by environment variables
OMRDebug_BackingMemory=1 or 2 or 3    (increasing detail)
OMRDebug_SegmentAllocator=1 or 2 or 3 (increasing detail)

While debugging the memory allocation layers, I discovered that
the original list management for segments was less efficient than
it could be, in that new segments were typically added at the
end of lists but searched starting from the beginning. This commit
changes the order of search to require fewer probes (less than
half the probes required in some very unscientific studies with
very simple compilations). I doubt it will have any performance
impact in most workloads but it felt better to fix it.

BackingMemoryAllocator, SegmentAllocator, and DebugSegmentAllocator
are all extensible classes to facilitate a similar refactoring
extravaganza to be inflicted upon OpenJ9 very soon.

Signed-off-by: Mark Stoodley <[email protected]>
The compiler's lowest level memory allocation implementation
is changing so provide documention for the new scheme. But since
the old implementation is still available via the OLD_MEMORY macro,
the older documentation is not removed, but moved to a file prefixed
by "Old" and with comments at the top directing towards the new
file. Once all downstream projects have stopped using the old scheme,
this older documentation will be removed.

Signed-off-by: Mark Stoodley <[email protected]>
@mstoodle
Copy link
Contributor Author

mstoodle commented Jan 9, 2019

genie-omr build all

fragmentation from the raw allocator. For example, the default
method compilation requests will construct a backing memory allocator
with a default segment size of 64KB (`1 << 16`), which means all
allocations from the backing memory allocator will rounded up to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
allocations from the backing memory allocator will rounded up to
allocations from the backing memory allocator will be rounded up to

The backing memory allocator tracks how much memory has been allocated
via `_bytesAllocated` which can be queried via `bytesAllocated()`. The
backing memory allocator can also be directed to enforce an
allocation limit via a constructor option which sets the `_allocationLimit`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say "constructor option", do you mean "constructor parameter"?

Suggested change
allocation limit via a constructor option which sets the `_allocationLimit`
allocation limit via a constructor parameter which sets the `_allocationLimit`

required by the compiler).

This reuse has one restriction to simplify the reuse algorithm:
only segments smaller than the default segment size (which will
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not completely clear to me what the "default segment size" is referring to. I assume it's the "minimum" size of segments returned by the segment allocator; the 64KB carved out of some other, larger segment in the example above. Is that correct?

Copy link
Contributor Author

@mstoodle mstoodle Jan 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that's correct; i'll see if I can make that more obvious

free segments, which are `TR::MemorySegment`s that have already
been cut up into the default allocation size. Because all
allocation requests are rounded up to this size anyway (except
for larger memory requests), any segment on this free list can
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the phrasing of this sentence slightly confusing because, if I understand correctly, larger memory requests are rounded up to a multiple of the base size. Can the sentence be changed to something like:

Because all allocation requests smaller than the base are rounded up, ...

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll try to reword that phrase to make it less awkward

`TR::MemorySegment` allocated by the backing memory allocator
that has not been fully consumed yet. If the request can
be satisfied from this memory segment, then a new
`TR::MemorySegment` will be created (using the raw allocator)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might also be worth mentioning where the raw allocator is coming from. I assume it's the one inside the backing allocator but I'd prefer to have that explicitly stated for clarity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes sense.

@mstoodle
Copy link
Contributor Author

mstoodle commented Jan 10, 2019

There are a couple more changes coming already, inspired by the work in OpenJ9. I would characterize these changes as mostly "cosmetic" in that they do not impact how the memory allocation classes operate; the changers make them more useful for reuse in other projects:

  1. I have now realized the awkwardness in the using construct for mapping an extensible class into the TR namespace, so I'll be promoting many of these classes to full OMR_EXTENSIBLE classes and also bring in self()
  2. some of the fields in OMR classes are marked as private but are useful to reference directly in subclasses, so I'll be promoting those fields to protected

...probably more to come...

@mstoodle
Copy link
Contributor Author

This pull request no longer reflects the direction I think this work should take. Following the compiler architecture meeting where we discussed this design, I received significant input from others that motivated a somewhat different take. I also gained an improved understanding of how RawAllocator interacts with STL allocators that makes some of this design, as written, problematic. I'm going to close this PR at this point, since it's obviously stale.

Note to any (optimistically presumed) subsequent readers: please feel free to contact me to discuss this work if you're interested :) .

@mstoodle mstoodle closed this Oct 22, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants