Proxy.isProxyClass scalability

Peter Levart peter.levart at gmail.com
Tue Apr 16 14:18:27 UTC 2013


Hi Mandy,

I prepared a preview variant of j.l.r.Proxy using WeakCache (turned into 
an interface and a special FlattenedWeakCache implementation in 
anticipation to create another variant using two-levels of 
ConcurrentHashMaps for backing storage, but with same API) just to 
compare performance:

https://dl.dropboxusercontent.com/u/101777488/jdk8-tl/proxy-wc/webrev.01/index.html

As the values (Class objects of proxy classes) must be wrapped in a 
WeakReference, the same instance of WeakReference can be re-used as a 
key in another ConcurrentHashMap to implement quick look-up for 
Proxy.isProxyClass() method eliminating the need to use ClassValue, 
which is quite space-hungry.

Comparing the performance, here's a summary of all 3 variants (original, 
patched using a field in ClassLoader and this variant):


Summary (4 Cores x 2 Threads i7 CPU):

Test                     Threads  ns/op Original  Patched (CL field)  
Patched (WeakCache)
=======================  =======  ============== ==================  
===================
Proxy_getProxyClass            1 2,403.27              
163.70               206.88
                                4 3,039.01              
202.77               303.38
                                8 5,193.58              
314.47               442.58

Proxy_isProxyClassTrue         1 95.02               
10.78                41.85
                                4 2,266.29               
10.80                42.32
                                8 4,782.29               
20.53                72.29

Proxy_isProxyClassFalse        1 95.02                
1.36                 1.36
                                4 2,186.59                
1.36                 1.37
                                8 4,891.15                
2.72                 2.94

Annotation_equals              1 240.10              
152.29               193.27
                                4 1,864.06              
153.81               195.60
                                8 8,639.20              
262.09               384.72


The improvement is still quite satisfactory, although a little slower 
than the direct-field variant. The scalability is the same as with 
direct-field variant.

Space consumption of cache structure, calculated as deep-size of the 
structure, ignoring interned Strings, Class and ClassLoader objects 
unsing single non-bootstrap ClassLoader for defining the proxy classes 
and using 32 bit addressing is the following:

original Proxy code:

proxy     size of   delta to
classes   caches    prev.ln.
--------  --------  --------
        0       400       400
        1       768       368
        2       920       152
        3      1072       152
        4      1224       152
        5      1376       152
        6      1528       152
        7      1680       152
        8      1832       152
        9      1984       152
       10      2136       152

Proxy patched with the variant using FlattenedWeakCache, run on current 
JDK8/tl tip (still uses old ConcurrentHashMap implementation with segments):

proxy     size of   delta to
classes   caches    prev.ln.
--------  --------  --------
        0       560       560
        1       936       376
        2      1312       376
        3      1688       376
        4      2064       376
        5      2352       288
        6      2728       376
        7      3016       288
        8      3392       376
        9      3592       200
       10      3872       280

...and the same with current JDK8/lambda tip (using new segment-less 
ConcurrentHashMap):

proxy     size of   delta to
classes   caches    prev.ln.
--------  --------  --------
        0       240       240
        1       584       344
        2       768       184
        3       952       184
        4      1136       184
        5      1320       184
        6      1504       184
        7      1688       184
        8      1872       184
        9      2056       184
       10      2240       184


So with new ConcurrentHashMap the patched Proxy uses about 32 bytes more 
per proxy class.


Is this satisfactory or should we also try a variant with two-levels of 
ConcurrentHashMaps?


Regards, Peter


P.S. Comment to your comment in-line...

On 04/16/2013 12:58 AM, Mandy Chung wrote:
>
> On 4/13/2013 2:59 PM, Peter Levart wrote:
>>
>>>>
>>>> I also devised an alternative caching mechanism with scalability in 
>>>> mind which uses WeakReferences for keys (for example ClassLoader) 
>>>> and values (for example Class) that could be used in this situation 
>>>> in case adding a field to ClassLoader is not an option:
>>>>
>>>
>>> I would also consider any alternative to avoid adding the 
>>> proxyClassCache field in ClassLoader as Alan commented previously.
>>>
>>> My observation of the typical usage of proxies is to use the 
>>> interface's class loader to define the proxy class. So is it 
>>> necessary to maintain a per-loader cache?  The per-loader cache maps 
>>> from the interface names to a proxy class defined by one loader. I 
>>> would think it's reasonable to assume the number of loaders to 
>>> define proxy class with the same set of interfaces is small.  What 
>>> if we make the cache as "interface names" as the key to a set of 
>>> proxy class suppliers that can have only one proxy class per one 
>>> unique defining loader.  If the proxy class is being generated i.e. 
>>> ProxyClassFactory supplier, the loader is available for comparison. 
>>> When there are more than one matching proxy classes, it would have 
>>> to iterate all in the set.
>>
>> I would assume yes, proxy class for a particular set of interfaces is 
>> typically defined by one classloader only. But the API allows to 
>> specify different loaders as long as the interfaces implemented by 
>> proxy class are "visible" from the loader that defines the proxy 
>> class. If we're talking about interface names - as opposed to 
>> interfaces - then the possibility that a particular set of interface 
>> names would want to be used to define proxy classes with different 
>> loaders is even bigger, since an interface name can refer to 
>> different interfaces with same name (think of interfaces deployed as 
>> part of an app in an application server, say a set of annotations 
>> used by different apps but deployed as part of each individual app).
>>
>
> Agree.  I was tempted to consider making weak reference to the 
> interface classes as the key but in any case the overhead of 
> Class.getClassLoader() is still a performance hog.   Let's move 
> forward with the alternative you propose.
>
>> The scheme you're proposing might be possible, though not simple: The 
>> factory Supplier<Class> would become a Function<ClassLoader, Class> 
>> and would have to maintain it's own set of cached proxy classes. 
>> There would be a single ConcurrentMap<List<String>, 
>> Function<ClassLoader, Class>> to map sets of interface names to 
>> factory Functions, but the cached classes in a particular factory 
>> Function would still have to be weakly referenced. I see some 
>> difficulties in implementing such a scheme:
>> - expunging cleared WeakReferences could only reliably clear the 
>> cache inside each factory Function but removing the entry from the 
>> map of  factory Functions when last proxy class for a particular set 
>> of interface names is expunged  would become a difficult task if not 
>> impossible with all the scalability constraints in mind (just 
>> thinking about concurrent requests into same factory Function where 
>> one is requesting new proxy class and the other is expunging cleared 
>> WeakReference which represents the last element in the set of cached 
>> proxy classes).
>> - one of my past ideas of implementing scalable Proxy.isProxyClass() 
>> was to maintain a Set<Class> in each ClassLoader populated with all 
>> the proxy classes defined by a particular ClassLoader. Benchmarking 
>> such solution showed that Class.getClassLoader() is a peformance hog, 
>> so I scraped it in favor of ClassValue<Boolean> that is now 
>> incorporated in the patch. In order to "choose" the right proxy class 
>> from the set of proxy classes inside a particular factory Function, 
>> the Class.getClassLoader() method would have to be used, or entries 
>> would have to (weakly) reference a particular ClassLoader associated 
>> with each proxy class.
>>
>
> Thanks for reminding me your earlier prototype.  I suspect the cost of 
> Class.getClassLoader() is due to its lookup of the caller class every 
> time it's called.

Even without SecurityManager installed the performance of native 
getClassLoader0 was a hog. I don't know why? Isn't there an implicit 
reference to defining ClassLoader from every Class object?

>
>> Considering all that, such solution starts to look unappealing. It 
>> might even be more space-hungry then the presented WeakCache.
>>
>> WeakCache is currently the following:
>>
>> ConcurrentMap<WeakReferenceWithInterfaceNames<ClassLoader>, 
>> WeakReference<Class>>
>>
>> another alternative would be:
>>
>> ConcurrentMap<WeakReference<ClassLoader>, 
>> ConcurrentMap<InterfaceNames, WeakReference<Class>>>
>>
>> ...which might need a little less space than WeakCache (only one 
>> WeakReference per proxy class + one per ClassLoader instead of two 
>> WeakReferences per proxy class) but would require two map lookups 
>> during fast-path retrieval. It might not be performance critical and 
>> the expunging could be performed easily too.
>>
>
> I am fine with either of these alternatives.  As you noted, the latter 
> one would save little bit of memory for the cases when several proxy 
> classes are defined per loader e.g. one per each annotation type.
>
> Mandy




More information about the core-libs-dev mailing list