Skip to content
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

HV-1694 HV-1695 HV-1696 HV-1697 Reduce memory allocations #1020

Merged
merged 8 commits into from Feb 27, 2019

Conversation

gsmet
Copy link
Member

@gsmet gsmet commented Feb 21, 2019

@marko-bekhta could you take a look at this one?

We had some reports of higher memory allocation rate when validating unconstrained beans (compared to old 5.2) and I can see how that could be, considering how we restructured the code: we would create a context even if not required at all.

That being said, benchmarks don't really say it's faster but I can see at least less allocations which is good.

I also modified some of the benchmarks so that they allocate less things in the benchmark itself. It avoids polluting the results.

Better reviewed commit per commit.

Note that there are a lot more things we could do to improve memory allocation rate when bootstrapping the metadata but it's less pressing so maybe one day :) - one thing I noticed is that we should cache getAnnotatedType() and getAnnotations() in the org.hibernate.validator.internal.properties.javabean classes. Not totally sure it's worth the bandwidth though so enough for one day.

@gsmet gsmet changed the title HV-1694 Reduce memory allocation for unconstrained beans HV-1694 HV-1695 HV-1696 Reduce memory allocations Feb 21, 2019
@gsmet gsmet force-pushed the HV-1694 branch 3 times, most recently from efbddab to 3c49312 Compare February 21, 2019 15:07
@gsmet
Copy link
Member Author

gsmet commented Feb 21, 2019

@marko-bekhta I added one further improvements to ValidationContexts. This one makes a nice difference in the SimpleValidation.

I also added a hv-6.1 target so I would be interested if you could also get some numbers on your laptop for:

  • hv-5.2
  • hv-6.0
  • hv-6.1 (this is the latest alpha)
  • hv-current (mvn clean install engine before this one :))

for the SimpleValidation and the UnconstrainedBeanValidation benchmarks.

return beanMetaData;
}

beanMetaData = (BeanMetaData<T>) beanMetaDataCache.computeIfAbsent( beanClass,
bc -> createBeanMetaData( bc ) );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is interesting: this code creates a new instance of the lambda for each call so we end up creating a lot of lambda instances.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... so it looks like the ConcurrentReferenceHashMap that we are using for this cache is actually using the default impl of computeIfAbsent. And that default one doesn't have any synchronised blocks. So I wonder if maybe we should change this last line to something like:

BeanMetaDataImpl<T> beanMetaData = createBeanMetaData( beanClass );
beanMetaDataCache.putIfAbsent( beanClass, beanMetaData );

return beanMetaData;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. So I switched to that but I get the previous value from putIfAbsent and return that if there is one.

@gsmet
Copy link
Member Author

gsmet commented Feb 21, 2019

@gunnarmorling if you have some time, could you take a look at this one? Better reviewed commit per commit.

@gsmet gsmet changed the title HV-1694 HV-1695 HV-1696 Reduce memory allocations HV-1694 HV-1695 HV-1696 HV-1697 Reduce memory allocations Feb 21, 2019
Copy link
Member

@marko-bekhta marko-bekhta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvements! I've added a few comments/questions inline. Now - benchmarks!

@@ -245,7 +245,7 @@
<properties>
<validation-api.version>1.1.0.Final</validation-api.version>
<beanvalidation-impl.name>Hibernate Validator</beanvalidation-impl.name>
<beanvalidation-impl.version>5.4.2.Final</beanvalidation-impl.version>
<beanvalidation-impl.version>5.4.3.Final</beanvalidation-impl.version>
</properties>
<dependencies>
<dependency>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cannot leave a comment on a renamed file below. Not sure why SimpleComposingConstraintValidation got moved to bv2 sources ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's using BV 2 constraints :).

I had an issue when trying to build with BV 1.1 and couldn't find nice BV 1.1 constraints so I just decided to move it as it's more a test to validate recent performance improvements.

return Collections.emptySet();
}

BaseBeanValidationContext<T> validationContext = getValidationContextBuilder().forValidate( rootBeanClass, rootBeanMetaData, object );

if ( !validationContext.getRootBeanMetaData().hasConstraints() ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is probably redundant now as we already checked for constraints on line 158.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, sure it was supposed to be removed, missed it, will fix.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, there were a few other glitches. I fixed them all, hopefully.

return beanMetaData;
}

beanMetaData = (BeanMetaData<T>) beanMetaDataCache.computeIfAbsent( beanClass,
bc -> createBeanMetaData( bc ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... so it looks like the ConcurrentReferenceHashMap that we are using for this cache is actually using the default impl of computeIfAbsent. And that default one doesn't have any synchronised blocks. So I wonder if maybe we should change this last line to something like:

BeanMetaDataImpl<T> beanMetaData = createBeanMetaData( beanClass );
beanMetaDataCache.putIfAbsent( beanClass, beanMetaData );

return beanMetaData;

…Impl

It significantly reduces memory allocation as it avoids the creation of
a lambda instance.
@marko-bekhta
Copy link
Member

Here's the results.. Some of them are a bit unexpected (6.0 unconstrained):

hv-current:
# Run complete. Total time: 00:00:36

Benchmark                                                     Mode  Cnt     Score     Error  Units
SimpleValidation.testSimpleBeanValidation                    thrpt   20  3580.428 ± 250.562  ops/ms
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  7065.427 ± 107.262  ops/ms

6.1.alpha
# Run complete. Total time: 00:00:37

Benchmark                                                     Mode  Cnt     Score     Error  Units
SimpleValidation.testSimpleBeanValidation                    thrpt   20  3176.884 ± 219.034  ops/ms
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  7429.412 ± 114.166  ops/ms

6.0
# Run complete. Total time: 00:00:37

Benchmark                                                     Mode  Cnt     Score     Error  Units
SimpleValidation.testSimpleBeanValidation                    thrpt   20  2799.963 ± 76.101  ops/ms
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  9013.694 ± 95.510  ops/ms
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  7882.279 ± 104.074  ops/ms

5.4
# Run complete. Total time: 00:00:30

Benchmark                                                     Mode  Cnt     Score     Error  Units
SimpleValidation.testSimpleBeanValidation                    thrpt   20  334.601  ± 6.362    ops/ms
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  7126.291 ± 102.859  ops/ms

5.2
# Run complete. Total time: 00:00:31

Benchmark                                                     Mode  Cnt     Score     Error  Units
SimpleValidation.testSimpleBeanValidation                    thrpt   20  337.434  ± 10.004   ops/ms
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  7541.114 ± 123.081  ops/ms

@gsmet
Copy link
Member Author

gsmet commented Feb 21, 2019

Yeah I had the same sort of issues for the unconstrained benchmark. I don't know what's going on exactly.

@gsmet
Copy link
Member Author

gsmet commented Feb 21, 2019

I just run it again and I have:

hv-current

Result "org.hibernate.validator.performance.unconstrained.UnconstrainedBeanValidation.testUnconstrainedBeanValidation":
  8544.618 ±(99.9%) 90.401 ops/ms [Average]
  (min, avg, max) = (8373.998, 8544.618, 8684.439), stdev = 104.106
  CI (99.9%): [8454.217, 8635.019] (assumes normal distribution)

hv-6.0

Result "org.hibernate.validator.performance.unconstrained.UnconstrainedBeanValidation.testUnconstrainedBeanValidation":
  7794.353 ±(99.9%) 128.405 ops/ms [Average]
  (min, avg, max) = (7591.621, 7794.353, 8221.196), stdev = 147.872
  CI (99.9%): [7665.948, 7922.758] (assumes normal distribution)

Which JDK do you use to run the benchmarks? I use a good old JDK 8.

@gsmet
Copy link
Member Author

gsmet commented Feb 21, 2019

And with JDK 11.0.1, it's significantly slower, especially hv-current which then get slower than hv-6.0.

@marko-bekhta
Copy link
Member

Just tried it again with your latest change:
hv-current:

Result "org.hibernate.validator.performance.unconstrained.UnconstrainedBeanValidation.testUnconstrainedBeanValidation":
  6913.940 ±(99.9%) 170.225 ops/ms [Average]
  (min, avg, max) = (6747.430, 6913.940, 7360.837), stdev = 196.031
  CI (99.9%): [6743.715, 7084.165] (assumes normal distribution)

6.0:

Result Result "org.hibernate.validator.performance.unconstrained.UnconstrainedBeanValidation.testUnconstrainedBeanValidation":
  8164.377 ±(99.9%) 240.792 ops/ms [Average]
  (min, avg, max) = (7777.431, 8164.377, 8744.066), stdev = 277.296
  CI (99.9%): [7923.585, 8405.168] (assumes normal distribution)

And I'm on VM version: JDK 1.8.0_161, VM 25.161-b12

@gsmet
Copy link
Member Author

gsmet commented Feb 21, 2019

This is weird to say the least because it definitely does less work. Could you check what's going on with a profiler? Not sure it will give interesting results though :/.

Wondering if maybe the method was inlined and that it's now too big to be inlined.

@gsmet
Copy link
Member Author

gsmet commented Feb 22, 2019

@marko-bekhta I pushed an additional commit pursuing the idea of inlining.

It seems to improve things here (but tbh, it's not very stable).

@marko-bekhta
Copy link
Member

Yeah I tried these last changes as well. And I cannot explain the behavior. I run benchmarks on different env today. When I tried them with 1.8.0_111 they gave expected results, where hv-current is slightly better than hv-6.0. Then when I tried to reproduce the results from yesterday I though I'll update to latest build - 1.8.0_201. With this version it started to show slower results for hv-current. But when I tried to profile it with VisualVm the iteration results would become similar between hv-current and hv-6.0. In profiling results the only thing to notice is that constructor of ValidationContext is not called in hv-current (which is expected).

@gsmet
Copy link
Member Author

gsmet commented Feb 22, 2019

OK, so you have weird results even with this very last commit: 581be9a ?

@marko-bekhta
Copy link
Member

Yes

@gsmet
Copy link
Member Author

gsmet commented Feb 27, 2019

@marko-bekhta so I discussed with @barreiro from our performance team and it appears our usage of Random was flawed. When several threads access a Random instance, it can become quite slow. And the faster you are, the more constrained you get, explaining why we ended up with worst results.

I changed our code to use ThreadLocalRandom.

With 6.1:

Benchmark                                                     Mode  Cnt      Score     Error   Units
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  10662.005 ± 142.829  ops/ms

With current:

Result "org.hibernate.validator.performance.unconstrained.UnconstrainedBeanValidation.testUnconstrainedBeanValidation":
  231520.643 ±(99.9%) 1596.706 ops/ms [Average]
  (min, avg, max) = (227430.245, 231520.643, 234223.436), stdev = 1838.769
  CI (99.9%): [229923.937, 233117.349] (assumes normal distribution)

So the new code appears to be 20 times faster on my laptop.

Could you check you have a similar tendency?

@marko-bekhta
Copy link
Member

Hi @gsmet yes, I just saw that you've pushed new changes and run the benchmarks right away 😄 . My results are similar:

hv-curr
# Run complete. Total time: 00:00:41

Benchmark                                                     Mode  Cnt      Score      Error   Units
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  88921.833 ± 1115.665  ops/ms
Benchmark                                   Mode  Cnt     Score    Error   Units
SimpleValidation.testSimpleBeanValidation  thrpt   20  1867.365 ± 39.177  ops/ms


hv-6.0
# Run complete. Total time: 00:00:43

Benchmark                                                     Mode  Cnt      Score     Error   Units
UnconstrainedBeanValidation.testUnconstrainedBeanValidation  thrpt   20  13163.132 ± 258.242  ops/ms
Benchmark                                   Mode  Cnt     Score    Error   Units
SimpleValidation.testSimpleBeanValidation  thrpt   20  1032.561 ± 17.536  ops/ms

Nice that we have an explanation of that behavior.

@gsmet gsmet merged commit 077827f into hibernate:master Feb 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants