-
Notifications
You must be signed in to change notification settings - Fork 5.8k
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
8297718: Make NMT free:ing protocol more granular #11390
Conversation
👋 Welcome back jsjolen! A progress list of the required criteria for merging this PR into |
@jdksjolen The following label will be automatically applied to this pull request:
When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command. |
Webrevs
|
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.
Mostly good. Mostly style nits.
We now handle correctly a very rare condition that would lead to misaccounting if a realloc failure occurred and the old block was accounted under a different MEMFLAGs. The price is a very slight premium we pay now because we dive down into NMT twice, once to check block integrity, and once to de-account. But since reallocs are not that hot, this should not be noticeable.
Can you please add a small gtest to test that MallocHeader::mark_block_as_dead is fully reversible with mark_block_as_alive, and that the block passes integrity checks afterwards?
@@ -115,15 +122,20 @@ class MallocHeader { | |||
void set_footer(uint16_t v) { footer_address()[0] = v >> 8; footer_address()[1] = (uint8_t)v; } | |||
|
|||
public: | |||
|
|||
MallocHeader(const MallocHeader&) = default; |
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.
This makes me a bit nervous, where do we copy malloc headers?
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.
Thanks for the catch! It's not necessary, I replaced it with NONCOPYABLE(MallocHeader)
.
} | ||
static inline void record_free(FreePackage free_package) { | ||
assert(enabled(), "NMT must be enabled"); | ||
MallocTracker::record_free(free_package); |
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.
Please leave the enabled()
checks down in MemTracker
. You rob it of its purpose in life :) Seriously, should we want to revise that, we should do it for all MemTracker methods.
const size_t size; | ||
const MEMFLAGS flags; | ||
const uint32_t mst_marker; | ||
}; |
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.
Could this be nested into MallocHeader, and renamed to something like "FreeInfo"?
inline void mark_block_as_dead(); | ||
inline void mark_block_as_alive(); |
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.
Proposal: name this "revive" and assert for dead markers. So far this is a one-trick pony used in realloc(), we don't really want a general way to mark dead headers as life.
inline MallocHeader(size_t size, MEMFLAGS flags, uint32_t mst_marker); | ||
|
||
inline size_t size() const { return _size; } | ||
inline MEMFLAGS flags() const { return (MEMFLAGS)_flags; } | ||
inline uint32_t mst_marker() const { return _mst_marker; } | ||
bool get_stack(NativeCallStack& stack) const; | ||
|
||
// Return the necessary data to record the block this belongs to as freed |
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.
Proposal: ".. to deaccount block with NMT" (since "record as freed" is ambivalent and could refer to different things, e.g. header marks.
if (MemTracker::tracking_level() == NMT_detail) { | ||
MallocSiteTable::deallocation_at(header->size(), header->mst_marker()); | ||
} | ||
record_free(header->free_package()); |
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.
Proposal: name this "deaccount(x)", since that's what it does. Then you can keep the original name for record_free().
// Returns the outer pointer to this block. | ||
static void* record_free_block(void* memblock); | ||
// Record a free. | ||
static void record_free(FreePackage free_package); |
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.
Can you expand on the comment a bit?
Given a block returned by os::malloc() or os::realloc(), de-account block from NMT, mark its header as dead and return pointer to header.
.
Given the free info from a block, de-account block from NMT.
.
MallocHeader* header = MallocTracker::malloc_header(memblock); | ||
header->assert_block_integrity(); // Assert block hasn't been tampered with. | ||
const FreePackage free_package = header->free_package(); | ||
header->mark_block_as_dead(); |
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.
Please revive the old comment, suitably adapted. It should still mostly apply, but please mention that de-accounting happens delayed since realloc may fail.
NOT_LP64(_alt_canary = _header_alt_canary_life_mark); | ||
set_footer(_footer_canary_life_mark); | ||
} | ||
|
||
inline void MallocHeader::mark_block_as_dead() { |
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.
Can you please add a comment to mark_block_as_dead() that its effects should be fully reversible with mark_block_as_alive (or "revive()", if you take my suggestion).
Thank you for the input! I've gone through each comment and fixed the code according to them. I'm far happier with the naming of some of these things now :-). |
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.
Looks good to me! Thank you for taking my suggestions.
Cheers, Thomas
@jdksjolen This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be:
You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been 282 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. ➡️ To integrate this PR with the above commit message to the |
Just so I understand the motivation for this fix correctly - the "double registration of a failed os::realloc in NMT" does not actually mess up the memory calculations, correct? We do The flags issue that Thomas is referring makes me want to ask: are we really allowed to So if that's the only issue, then we could have potentially just passed I do appreciate an attempt to clean this area up though and that we only mark the old memory free, after we know that |
I had to dig into the NMT code to understand the change and this is my understanding: OLD:
NEW:
So basically you are re-implementing |
I think we should have reused the original OLD:
NEW:
This is way we are re-using as much code as possible without needing to duplicate much of it. But more importantly, we would NOT miss this code:
As it is now, we are calling NMT code always, even when NMT is OFF. You can of course add that check itself back to your re-implementation copy directly in |
Hi @gerard-ziemski,
Yes, the only issue is that if
Arguably yes, but the API allows for this. Not sure if there are valid uses for it.
Yes, that would have worked. One would have to take care not to use header->flags() if realloc succeeded because the header could have moved in memory. Other than that, this would have been fine too.
Arguably the API surface is now broader, so more complex; but I can live with the current patch too. |
Matter of taste. I dislike this kind of coding, even though it is common in hotspot, where you have a compound function and a bunch of flags (often uncommented) fine-controlling its behavior. You then end up having to read the code to understand its behavior exactly. I like it better to have simple prototypes, simple building blocks and compound actions, and if needed, expose more granular actions with equally simple prototypes. Like here, we have MemTracker::record_free - a compound action that
and then, we have "deaccount" and "mark header as dead" exposed as more granular action that can be used where you need finer control.
There should be little duplication now as well.
You lost me here. Where would that be? Note that in os::realloc(), we are in the code path for NMT enabled already. |
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.
Good.
src/hotspot/share/runtime/os.cpp
Outdated
|
||
// the real realloc | ||
ALLOW_C_FUNCTION(::realloc, void* const new_outer_ptr = ::realloc(old_outer_ptr, new_outer_size);) | ||
ALLOW_C_FUNCTION(::realloc, void* const new_outer_ptr = ::realloc((void*)header, new_outer_size);) |
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.
The cast is not necessary.
@@ -106,10 +106,14 @@ class MemTracker : AllStatic { | |||
static inline void* record_free(void* memblock) { | |||
// Never turned on | |||
assert(memblock != NULL, "caller should handle NULL"); | |||
if (!enabled()) { | |||
if(!enabled()) { |
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.
space?
return MallocTracker::record_free_block(memblock); | ||
} | ||
static inline void deaccount(MallocHeader::FreeInfo free_info) { | ||
assert(enabled(), "NMT must be enabled"); |
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.
breaks protocol for MemTracker (checks enabled) but I think this is here okay. To get a free_info in the first place, we must have had NMT enabled.
Ah I see it now, it was hiding in the unexpanded portion of the review code - I just needed to click the UI arrow up to reveal it. |
Currently we have gone from:
to:
But the change could have been just from:
to:
Where Does |
BTW I filed NMT: should realloc() API take flags as arguments? to have a chance to consider whether it is a good idea for realloc to be able to change the MEMFLAGS. |
As I wrote, it's a matter of taste. Sometimes a bit verbose is a good tradeoff if it makes for easy understanding. So, operation x does A B C D, but if I pass false, its A B D. Okay, what if you need a different set of operations? Maybe you need just A and D? Give it another parameter? One for every permutation? That is exactly what often happens, and then you end up with long chain of arguments, each one subtly changing behavior, many of them often one-trick-ponies that are only used at one site. I quickly find this more complex than another line of plain code at the call site would be. You soon arrive at a point where you cannot tell from the call site what will happen. You have to dive into the code of the subroutine. Especially if both call and parameters are badly named and undocumented, which is often the case within hotspot. |
I don't disagree with you in principle, though here I'd prefer modifying the |
// realloc(3) failed and the block still exists. | ||
// We have however marked it as dead, revert this change. | ||
header->revive(); | ||
return nullptr; |
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.
This is the very first usage of nullptr
instead of NULL
, and we now have a mix of both.
Was this a deliberate choice? Should we then switch all NULLs
to nullptr
in here?
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.
We should try to use nullptr
in new code and change from NULL within those methods for consistency, but a broader cleanup should be done as a separate cleanup RFE.
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.
My strategy for the NULL
to nullptr
conversion is that if I touch the line of code, I change it to nullptr
. Changing whole methods lead to a larger diff for reviewers, which can be annoying when looking for the relevant changes.
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.
Introducing inconsistency in surrounding code is not a good thing though. If you think it too distracting to do the entire method then I would suggest don't do it at all and just leave it all for a cleanup RFE.
Looks good to me, thank you for the fix Johan! |
My concern with the latter is the breaking of encapsulation and pollution of the callsite with details it has no business knowing about. To some extent some knowledge has to leak to the callsite to know either which API to call, or which flag parameters to pass, but that should be minimised and as generic as possible. |
Okay, I see your point (and @gerard-ziemski ). But having to know when to pass false, and what false does, also breaks encapsulation. A good encapsulation would be breaking MemTracker::record_free into three clearly named APIs then, without demanding knowledge about what they do:
|
A general comment on this thread of reasoning: The original I'm generally against flags to alter behavior, for the reasons Thomas has already expressed. |
Thanks for the nits, Thomas. I've pushed them. |
Tests passing, integrating this. /integrate |
Going to push as commit 165dcdd.
Your commit was automatically rebased without conflicts. |
@jdksjolen Pushed as commit 165dcdd. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
This PR avoids the double registration of a failed
os::realloc
in NMT by doing the NMT free protocol steps inline (asserting block integrity, marking of MallocHeader, and registrering results to statistics). This is done by creating a new structFreePackage
which includes all of the necessary data for registrering results.I also did some light refactoring which I think makes the protocol clearer, these are done piece wise as separate commits for the reviewers' convenience.
Progress
Issue
Reviewers
Reviewing
Using
git
Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk pull/11390/head:pull/11390
$ git checkout pull/11390
Update a local copy of the PR:
$ git checkout pull/11390
$ git pull https://git.openjdk.org/jdk pull/11390/head
Using Skara CLI tools
Checkout this PR locally:
$ git pr checkout 11390
View PR using the GUI difftool:
$ git pr show -t 11390
Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/11390.diff