groovy-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Jochen Theodorou <blackd...@gmx.org>
Subject Re: Improve Groovy class loading performance and memory management
Date Fri, 13 May 2016 11:03:27 GMT
On 13.05.2016 02:22, Alain Stalder wrote:
[...]
> Qualitatively this often has the following result in the Java VM:
> Metaspace resp. PermGen, and Heap in parallel, just grow until a
> configured limit is reached (and note that there is none by default for
> Metaspace in Java 8 and later), often only then is it garbage collected.
> With Java classes, at least with simple ones, this looks often
> different, those appear to be garbage collected much more quickly.
>
> Another qualitative difference is that loading a Groovy class and
> instantiating it seems typically to be considerably slower than
> instantiating a Java class with similar functionality, even quite
> drastically so, more than one would expect even considering the need to
> create metadata for dynamic function calls etc.
>
> At least that has been my experience over the past few years.

this is going to be a long mail ;)

so let us make three things to discuss here:

1) Object initialization performance
2) class verification/complexity
3) garbage collection of unused classes

And we have to distinguish here between usages of ClassValue, 
invokedynamic and the traditional version of those... that makes 4 aspects.

So I will write several things you probably already know, but other 
reading here might not. And even though I simplify a bit, please bear 
with me ;) And first of all, let us talk about the meta class system... 
that mostly targets ClassValue then.

The old version of the meta class system uses a big global table for all 
meta classes, with a class key and ClassInfo as value. In ClassInfo we 
have the meta class, which might be either recreate-able (in which case 
the meta class is soft referenced) or not (in which case it is a strong 
reference). The idea being, that as soon as the class can be garbage 
collected, the ClassInfo can as well, and with it the meta class.

Problem 1 here is, the meta class holds a strong reference to the class, 
so if the ClassInfo holds a strong reference to the meta class, this 
entry in the table will never be collected. I mention this only for 
completeness, since you did not set a permanent meta class in your test

Problem 2 here is, the code is concurrent, which rules out WeakHashMap 
and forced us to implement our own map.

In the ClassValue version we do not have our own table anymore and let 
the JVM manage this.

To avoid the lookup cost of the meta class, every Groovy class has a 
reference to its ClassInfo. The meta class and ClassInfo are lazy 
initialized.... well, "populated with actual data" in case of ClassInfo.

Some classes extend GroovyObjectSupport, which does the initialization 
in the constructor already, groovy.lang.Script is one of them. That 
means every time you create an instance of a new script class, you get 
the meta class already, even if the meta class is not used. let us have 
a small look at the bytecode of the constructors (x.groovy which only 
returns 42) of such a script:

>   // access flags 0x1
>   public <init>()V
>     ALOAD 0
>     INVOKESPECIAL groovy/lang/Script.<init> ()V
>    L0
>     INVOKESTATIC x.$getCallSiteArray ()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
>     ASTORE 1
>    L1
>     RETURN
>
>   // access flags 0x1
>   public <init>(Lgroovy/lang/Binding;)V
>    L0
>     INVOKESTATIC x.$getCallSiteArray ()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
>     ASTORE 2
>     ALOAD 0
>     ALOAD 1
>     INVOKESPECIAL groovy/lang/Script.<init> (Lgroovy/lang/Binding;)V
>    L1
>     RETURN

as you can see here, there is a getCallSiteArray call in here and direct 
methods calls for initialization. The getCallSiteArray call in here is 
actually surplus, but it is difficult to decide in the compiler early on 
if we will need it or not, because the callsite array here is basically 
an array wrapper, which supplies method names for method calls as well 
as specialized code for doing dynamic method calls. Would the 
constructor for example contain a method call to a method foo(), you 
would see some thing like getting the CallSite and executing "call" on it.

Why are we doing static method calls in the constructor here? Because in 
several cases the compiler optimizes the dynamic call away here. 
Basically you cannot provide a super constructor in Groovy, which means 
only the one statically defined does count. And as long as the given 
types are matching enough for the compiler to decide, we can create 
direct method calls.

So what does this mean for the object initialization performance in 
cases of Scripts so far? We eagerly initialize ClassInfo and MetaClass 
for each script. That means a lookup by reflection of the complete 
structure of the class and its super classes... which is cached, so the 
super class lookup will be much lower the next time. initializing the 
first meta class will also initialize the extension method lookup, which 
can also take a bit time... again, a one-time cost here.

The invokedynamic version will again use the jdk internals for the 
callsites, thus $getCallSiteArray is never called nor are the methods 
around this created. This saves bytecode size (faster verification), and 
in fact the two mentioned constructors above would look just like they 
would in Java. This of course still implies the meta class system init.

Now... the actual code of the script is to be found in the run method, 
which looks like this in old groovy:

>   // access flags 0x1
>   public run()Ljava/lang/Object;
>    L0
>     INVOKESTATIC x.$getCallSiteArray ()[Lorg/codehaus/groovy/runtime/callsite/CallSite;
>     ASTORE 1
>    L1
>     LINENUMBER 1 L1
>     BIPUSH 42
>     INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
>     ARETURN
>    L2
>     ACONST_NULL
>     ARETURN

again we see the call to get the callsite array. But otherwise the 
method should like this in Java too. Since we are not calling any actual 
methods and just return 42, we have again needless overhead in this. 
Accordingly the invokedynamic version looks like this:

>   // access flags 0x1
>   public run()Ljava/lang/Object;
>    L0
>     LINENUMBER 1 L0
>     BIPUSH 42
>     INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
>     ARETURN
>    L1
>    FRAME FULL [] [java/lang/Throwable]
>     NOP
>     ATHROW

Frames appear in this one, because the old groovy bytecode is java5 
compatible, while invokedynamic requires java7 and thus the new faster 
verifier with frames. The frames basically support the verifier to make 
it faster.... I never tested if that is actually the case.

To sum it up a little so far.... the invokedynamic version uses less 
bytecode and the new verifier 1685 vs 2140, both could execute that 
script without ever using the meta class system, if run is called 
directly and if not Script would initialize the meta class eagerly. Of 
course the later is for nil as soon as you have some method calls in 
there, which are not static.

Since we now have some basics, we should start talking about your test case.

First thing... I don´t think you load the class new every time. (Taken 
from my machine, with java8u25 and Groovy 2.4.6)
>    0         1           1      10.7m      11.4m        14.4m    240.0m     0.105ms 
   0.061ms
>    1    760081           1      11.4m      11.8m        28.3m    246.5m     0.000ms 
   0.000ms

I see here the first time class loading with 105 and a creation time 
with 61. This class loading time has to appear every time and must never 
be 0. But that is the case. So even if you create a new URLClassLoader 
without defining a root, it still has the system class loader as parent. 
Providing null as parent class loader fixes that problem. And then I got 
a more interesting:

>    0         1           1      10.6m      11.4m        14.4m    240.0m     0.213ms 
   0.063ms
>    1     11519        2227      16.2m      35.9m        24.2m    246.0m     0.060ms 
   0.022ms
>    2     27103        4055      19.4m      46.8m        40.7m    229.0m     0.051ms 
   0.019ms
>    3     42465           1      12.2m      34.4m         4.5m    235.0m     0.049ms 
   0.019ms
>    4     57606         580      13.3m      34.4m         9.0m    236.0m     0.048ms 
   0.018ms
>    5     73506        1918      15.8m      39.1m        21.3m    236.0m     0.047ms 
   0.018ms
>    6     89212        2482      16.8m      43.0m        25.8m    212.5m     0.047ms 
   0.018ms
>    7    104578           1      12.4m      41.7m         4.0m    232.0m     0.046ms 
   0.018ms

actually this was with java5 compatible code... with java8 standard I 
get something like this:

>    0         1           1      10.6m      11.4m        14.4m    240.0m     0.214ms 
   0.029ms
>    1     12117        2861      17.3m      38.4m        30.4m    246.0m     0.062ms 
   0.016ms
>    2     29143        1303      14.6m      36.6m        15.8m    236.0m     0.052ms 
   0.014ms
>    3     46633        4267      19.8m      48.3m        43.2m    235.0m     0.048ms 
   0.013ms
>    4     62634         900      14.0m      35.2m        11.4m    236.0m     0.048ms 
   0.013ms
>    5     80618        4358      20.1m      48.7m        43.5m    235.5m     0.046ms 
   0.013ms
>    6     97964        2336      16.6m      41.0m        25.3m    236.5m     0.046ms 
   0.012ms

This makes me actually wondering a bit as of why the creation time goes 
down, but not the load time. And after some thinking things become clear 
to me...when you load a class using loadClass, the class is not verified 
immediately. This happens lazy on actual static initialization or once 
you ask the class for its structure... like when using getMethods. So 
what load time tests here is purely the class loader performance, 
nothing else. A difference in load time would thus indicate solely that 
more or less classes are used. This also means the creation time here 
includes the verification time of the class, which I guess amounts to 
the .006 in difference between the two.

Now lets get further and change the simple java class with a run method 
to extend Groovy´s script class.. well this requires further changes, 
since now of course giving the groovy jar on the classpath will no 
longer enable the spawned URLClassloader to load groovy classes. 
Anyway... after fixing that:

>    0         1           1      13.4m      13.8m        24.0m    240.0m     1.290ms 
  90.612ms
>    1        12          12      39.3m      40.1m        64.3m    190.0m     0.985ms 
  82.673ms
>    2        23          23      65.0m      66.5m        56.0m    246.0m     1.024ms 
  88.072ms
>    3        34           5      29.3m      46.8m        27.6m    230.5m     0.976ms 
  88.245ms
>    4        48          19      54.8m      64.0m        39.2m    228.0m     0.929ms 
  83.359ms
>    5        60           1      19.6m      51.3m        13.2m    228.0m     0.892ms 
  84.574ms
>    6        73          14      42.6m      55.8m        36.8m    228.0m     0.859ms 
  82.042ms
...
>   46       593           1      21.8m      62.2m         7.9m    220.5m     0.783ms 
  76.878ms
>   47       607          15      47.0m      62.3m        41.8m    219.0m     0.787ms 
  76.695ms
>   48       621          29      78.3m      82.5m        44.5m    220.5m     0.782ms 
  76.563ms

nothing much more will happen. I guess having a fast SSD is paying off 
here, creation times would have been 1s+ otherwise. Anyway... as you can 
see by the load time, we load a ton more classes now and creation is 
much more expensive, since we now load part of the meta class system. 
Without any further changes, this would be the best Groovy can achieve 
without actually changing the way scripts are done. But as you can see 
as well, the memory behaviour is actually quite well. I would have liked 
to test the ClassValue version, but it crashes almost immediately after 
my changes.

Next step is then to use the simple "42" script:
...
>   46       596          17      52.0m      63.7m        36.9m    219.5m     0.840ms 
  76.407ms
>   47       609           1      21.7m      60.7m        11.5m    223.0m     0.835ms 
  76.536ms
>   48       621          13      43.0m      60.7m        27.0m    223.0m     0.830ms 
  76.434ms

we now load even more classes like CallSite and such, but creation times 
are about the same meaning the overhead here is almost zero in the end. 
Unsurprisingly the invokedynamic version behaves about the same. Maybe 
the load times are a bit less good... which indicates more classes being 
loaded internally in the JDK code to support invokedynamic.

Now a further step... I can change the script from being "42" to:

public class GroovyFilling {
   public Object run() {
     return 42;
   }
}


so basically the same as the java version... This means Script will no 
longer be used here!

>   46       638          28      74.9m      79.6m        50.7m    227.5m     0.626ms 
  71.473ms
>   47       650          10      35.4m      60.3m        41.1m    227.5m     0.631ms 
  71.630ms
>   48       665          25      68.4m      74.6m        47.4m    227.5m     0.626ms 
  71.512ms

well... we seem to load less classes, but we still get quite high 
creation times. looking at the bytecode I find this (for the indy 
version, but that does not really matter):

>   // access flags 0x1
>   public <init>()V
>     ALOAD 0
>     INVOKESPECIAL java/lang/Object.<init> ()V
>    L0
>     ALOAD 0
>     INVOKEVIRTUAL GroovyFilling.$getStaticMetaClass ()Lgroovy/lang/MetaClass;
>     ASTORE 1
>     ALOAD 1
>     ALOAD 0
>     SWAP
>     PUTFIELD GroovyFilling.metaClass : Lgroovy/lang/MetaClass;
>     ALOAD 1
>     POP
>    L1
>     RETURN

So we actually do an eager meta class creation here as well. The worst 
part here is... we get this even with static compilation... which 
absolutely is a bug.

So to improve this test here I guess trying to get rid of 
$getStaticMetaClass and making Script (maybe even GroovyObjectSupport) 
init meta classes lazy would be a gain. In the times of pre Groovy 1.8 
this would not have amassed to much gain, since we did dynamic calls all 
other the place. But since then we managed to reduce those calls quite a 
lot, so I see an actual potential to reduce creation times for simple 
cases. Of course in the more complex ones or in case in which the super 
class constructor cannot be chosen statically by the compiler, all bets 
are off again.

bye Jochen


Mime
View raw message