Hi Laurent,
BTW, I wanted to point out that this is something we can look at after
we get the current set of fixes in. I just have to find an hour where I
can look over the last webrev and review all of the static methods and
it looks like it could go in.
I'm not necessarily thinking of solving everyone's caching concerns,
though that would be nice, I'm mainly looking at making the caching
solution used here easy to verify, easy to work on without making a
mistake in the future, and easy to tune without having to play with a
bunch of variants of static methods. In the end, all of them boil down
to 3 actions - allocate, grow, and dispose - but since it is using
static methods then that means an explosion to deal with things like
array type, dirtiness and, potentially in the future, different growth
metrics. I think that luckily the current difference in metrics happens
to align with whether the arrays are dirty or not, so the matrix is
smaller than it might otherwise be, but there would be an exponential
growth in the number of "very similarly named" static methods should we
introduce any other reason for one particular growable array instance to
be different...
...jim
On 6/9/2015 1:36 PM, Laurent Bourgès wrote:
Hi Jim,
Your proposal is interesting: I would have loved doing such smart array
cache for general use !
Do you think such a general ArrayCache would be helpful for other java2d
or jdk algorithms needing intensive array allocation ?
However, I focused mostly my work on getting the maximum performance and
also not changing too much the original pisces code (to let you review
it more easily).
Here's a suggestion on how I would set up the array caching code,
mainly with regard to making it easier to make sure we are managing
then consistently, but also to allow for more flexible tuning later.
I agree the array management code may look complicated (but less than
C/C++).
As I am using netbeans, I am abusing of its 'Find Usages' feature :
Usages of Renderer.edgeBuckets [11 occurrences]
sun.java2d.marlin.Renderer
addLine
394: final int[] _edgeBuckets = edgeBuckets;
Renderer
472: edgeBuckets = edgeBuckets_initial;
init
* 523: edgeBuckets = rdrCtx.getIntArray(edgeBucketsLength);
*dispose
589: if (edgeBuckets == edgeBuckets_initial) {
591: IntArrayCache.fill(edgeBuckets, buckets_minY,
* 597: rdrCtx.putIntArray(edgeBuckets, buckets_minY,
* 599: edgeBuckets = edgeBuckets_initial;
605: } else if (edgeBuckets != edgeBuckets_initial) {
* 607: rdrCtx.putIntArray(edgeBuckets, 0, 0);
* 608: edgeBuckets = edgeBuckets_initial;
_endRendering
708: final int[] _edgeBuckets = edgeBuckets;
So I can quickly check if I am using the appropriate IntArrayCache
(clean variant) !
First, make all interactions go through the cache object itself -
get, grow, put. This gets rid of a number of static methods with a
bunch of similar names and groups the management methods in a class
hierarchy.
It seems a good approach but requires me a big refactoring: a Cache_V2
will gather many Cache_V1 (including bucket / size management).
Moreover, it raises another issue: for now all caches are gathered into
the ArrayCacheHolder to wrap them with a single WeakReference => small
memory footprint + early GC cleanup.
That's the aim of the RendererContext.getArrayCachesHolder() method
which permits dynamic ArrayCachesHolder retrieval or creation. Actually
a Cache is needed to both get/grow but also release an array and
meanwhile, it is possible that GC had freed the Cache memory !
But this point is more problematic below:
Second, have each consumer of arrays use its own cache object. Some
of them may be the same object reference between different "users",
and some of them may be different objects but share their underlying
arrays with each other, but a given piece of code could be traced to
always get/grow/put its arrays into the same cache object. That way
it's easier to see that you aren't mixing/matching clean/dirty arrays.
It seems a bit verbose: 1 cache field per array = adding a lot of class
fields ! Maybe the same cache reference could be still used for "friend'
arrays like: edgeBuckets/edgeBucketCounts, crossings/aux_crossings, ...
DirtyFooCache
foo[] get(size)
foo[] grow(foo[], newsize) // or widen?
void put(foo[])
CleanFooCache
foo[] get(size)
foo[] grow(foo[], newsize, <used parts>)
void put(foo[], <used parts>)
Looks like a good design.
Then code that manages an array named bar would be managed as:
(It may be the same instance as another FooCache
or it may share its arrays with another FooCache
but the "barCache" reference is only ever used for the bar array...)
[Clean|Dirty]FooCache barCache = make/get/share a Cache instance;
foo[] bar;
...
bar = barCache.get(initSize);
...
bar = barCache.grow/widen(bar, newSize[, usage if clean]);
...
barCache.put(bar[, usage if clean]);
bar = null;
It's easier to verify that bar is being managed consistently if it
always goes back to its own bar object. You can then decide if too
different uses of foo[] arrays should share the same arrays or not
based on profiling. You can also potentially have different growth
algorithms for 2 different arrays of the same type, but possibly
even sharing the same underlying arrays. Any degree of
customization is possible underneath the covers behind a FooCache
basic interface.
I like your proposal but I maximized the array sharing ie Cache
instances for following reasons:
- maximize array reuse (bucket approach) = reduce the memory footprint
=> lower GC overhead because less memory is wasted by cached arrays
- each RendererContext has its own Cache instances: in multithreaded
environment (web server), it becomes critical to have the smaller memory
footprint and avoid concurrency issue = no shared cache between threads
- all that work can be seen as an Thread Local Array Allocation (TLAA)
that could be part of the GC algorithm itself to benefit to the all VM:
dirty vs clean arrays, reusing vs allocation + GC, partial cleaning
(Arrays 2.0 ?) ...
I'd also either make the growth heuristics an argument to
creating/fetching a FooCache instance or make it subject to a helper
"CacheSizeManager" or "CacheSizeStrategy" object in such a way that
different uses of IntCache might be able to have different growth
strategies - and to possibly even manage different sizing strategies
on top of the same set of underlying arrays (i.e. one has buckets
that grow by <<2 and another by <<1 and for the even buckets, they
share arrays, for instance).
It led me to imagine a nice Array Cache framework at the jdk or jvm
level ... at least not exclusive to Marlin !
The main thing for the near term, is that it would be nice to have
each array use its own cache object so that we can easily verify
that it is being consistent with its handling of arrays...
If you are not happy with the latest webrev (better naming convention),
we should discuss again how to do all these changes in a progressive manner.
Cheers,
Laurent