RFR: 8247755: Leaner and more versatile GrowableArray classes

Stefan Karlsson stefan.karlsson at oracle.com
Wed Jun 17 14:16:24 UTC 2020


Hi all,

Please review and/or comment on this patch to create leaner and more 
versatile GrowableArray classes.

https://cr.openjdk.java.net/~stefank/8247755/webrev.01
https://bugs.openjdk.java.net/browse/JDK-8247755

This is a proposal to rewrite the GrowableArray classes, with two main 
goals:

1) Allow a GrowableArray instance to be allocated on the stack or 
embedded in other objects. This removes one of the two indirections when 
operating on the array elements.

2) Make the GrowableArray instances smaller, so that there's no extra 
overhead when they are embedded in other objects.

This will enable us to use GrowableArray effectively in ZGC, without 
having to rely on our own implementations. See follow-up patch:
https://cr.openjdk.java.net/~stefank/8247759/webrev.01

Some background about the GrowableArray:

GrowableArray is split into two memory parts. One for the instance of 
the GrowableArray, and a second, separate memory part for the elements. 
This memory can come from these places:

a) Resource area in the thread
b) CHeap (malloc)
c) Arena (malloced arena)
d) Stack
e) Embedded in other object

 From the C++ compiler's point-of-view, (a)-(e) can be used to allocate 
the GrowableArray, and (a)-(c) can be used to allocate the elements. 
However, there are runtime asserts that restricts the combinations.

One of the restrictions is that if either of the GrowableArray instance 
memory, or the elements memory, comes from the CHeap, then the other 
must also come from the CHeap.

I've found two reasons why we want to be restrictive with our CHeap 
allocated elements:

- if the GrowableArray instance is allocated in (a) or (c), then the 
destructor will never run and we get a memory leak because the elements 
are not deallocated.

However, for stack allocated instances the destructor will be run, and 
the memory will be handed back. For embedded instances it all depends on 
how the parent instance was allocated.

- There's no proper support for copying/moving of the elements, just a 
simple pointer copy. So, if a copy would be done, the source 
GrowableArray would still have a pointer to the elements, and if the 
source were destructed (maybe because it was stack allocated and went 
out-of-scope) then the memory of the destination GrowableArray will be 
deallocated. This isn't a problem with instances in (a) or (c), since 
their destructors won't be run.

Proposal for Goal 1:

- Allow stack allocated and embedded GrowableArrays to have CHeap 
allocated elements.
- Add an assert in a copy constructor and assignment operator that 
checks that the GrowableArray elements are not CHeap allocated.

The rest of the patch mainly deals with Goal 2:

The current fields of GrowableArrays, in product builds, are:
    int _len;
    int _max;
    Arena* _arena;
    MEMFLAGS _memflags;
    E* _data;

and the resulting is 32 bytes: 4 + 4 + 8 + 8 + 8. The _memflags size is 
due to alignment requirements for the _data field.

- By combining _arena and _memflags, this is reduced to 24 bytes.
- By having a specialized CHeap version with the MEMFLAGS encoded as a 
template parameter, we can get rid of both _memflags and _arena fields, 
and reduce this to 16 bytes.

Things to note about the patch:

- GrowableArrayMetadata: I've fused _arena and _memflags into one 
uintptr_t field, and encapsulated all bookkeeping about where and how 
the memory is allocated for the GrowableArray. The hope is to simplify 
the code a bit, and also to move some of the checking out into a 
non-template class, so that the code can be put in the .cpp file.

- GrowableArrayNestingCheck: Simplify and encapsulate resource area 
nesting checks.

- I'm using CRTP because I don't want a vtable in the class. This means 
that the super class can cast to derived type, responsible for 
allocation and deallocation:

template <typename E> class GrowableArray : public GrowableArrayWithAllocator<E, GrowableArray<E> >

and then:

void GrowableArrayWithAllocator<E, S>::grow(int j) {
...
   E* newData = static_cast<S*>(this)->allocate();

- Since I'm chainging so much of the structure of the code, I've also 
made some stylistic changes to make the code more consistent.

- Moved the CompareClosure to iterator.hpp, where many of our other 
closures are located.

- Moved _sort_Fn, since it was already used by non-GrowableArray code

- I've added a GrowableArrayCHeap class to be able to mostly side-step 
the complication of the GrowableArray elements allocation, and to 
minimize the instance size.

- The metaspaceShared.cpp was used to show how GrowableArrayCHeap can be 
used. It's not strictly necessary, and could be reverted.

-  The arguments.cpp change shows a usage of GrowableArrayView as a way 
to only expose an interface that don't allow the array to be grown.

- The arguments.cpp change also shows stack allocated 
GrowableArrayCHeap, that automatically gets deallocated when the 
function returns, and the cleanups we can do because of this.

- The hashtable.* changes show an embedded GrowableArrayCHeap.

- The SA agent. I didn't bother renaming the GenericGrowableArray file. 
I can do that upon request.

- I added gtest for GrowableArray. There was not a single unit test of 
GrowableArray before. The creates all possible memory allocation 
combinations and run limited set of sanity checks. It also checks that 
the different memory allocation combinations return the correct 
allocation types when queried. I've added negative tests to show that 
GrowableArrays with CHeap allocated elements assert when being copied.

Testing: will run at least tier1-3, but most likely more tiers on Linux 
x64. Suggestions on what to test welcome.

Thanks,
StefanK


More information about the hotspot-dev mailing list