Add a compiler for SpEL

With these changes an optional compiler is added for SpEL
expressions. The compiler is off by default but can be enabled
via the SpEL parser configuration object or system property
(when SpEL is embedded and parser configuration is not possible).
Not all expressions are currently handled but the common
cases are and it is an extensible compilation framework.

Issue: SPR-10943
This commit is contained in:
Andy Clement
2014-06-04 10:09:02 -07:00
parent e254b558f0
commit 2eeb2e9235
47 changed files with 5964 additions and 90 deletions

View File

@@ -0,0 +1,471 @@
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.expression.spel;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelCompiler;
import static org.junit.Assert.*;
/**
* Checks the speed of compiled SpEL expressions.
* By default these tests are marked Ignore since they can fail on a busy machine because they
* compare relative performance of interpreted vs compiled.
*
* @author Andy Clement
* @since 4.1
*/
@Ignore
public class SpelCompilationPerformanceTests extends AbstractExpressionTests {
int count = 50000; // Number of evaluations that are timed in one run
int iterations = 10; // Number of times to repeat 'count' evaluations (for averaging)
private final static boolean noisyTests = true;
Expression expression;
public static class Payload {
Two[] DR = new Two[]{new Two()};
public Two[] getDR() {
return DR;
}
}
public static class Two {
Three DRFixedSection = new Three();
public Three getDRFixedSection() {
return DRFixedSection;
}
}
public static class Three {
double duration = 0.4d;
public double getDuration() {
return duration;
}
}
@Test
public void complexExpressionPerformance() throws Exception {
Payload payload = new Payload();
Expression expression = parser.parseExpression("DR[0].DRFixedSection.duration lt 0.1");
boolean b = false;
long iTotal = 0,cTotal = 0;
// warmup
for (int i=0;i<count;i++) {
b = expression.getValue(payload,Boolean.TYPE);
}
log("timing interpreted: ");
for (int iter=0;iter<iterations;iter++) {
long stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
b = expression.getValue(payload,Boolean.TYPE);
}
long etime = System.currentTimeMillis();
long interpretedSpeed = (etime - stime);
iTotal+=interpretedSpeed;
log(interpretedSpeed+"ms ");
}
logln();
compile(expression);
boolean bc = false;
expression.getValue(payload,Boolean.TYPE);
log("timing compiled: ");
for (int iter=0;iter<iterations;iter++) {
long stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
bc = expression.getValue(payload,Boolean.TYPE);
}
long etime = System.currentTimeMillis();
long compiledSpeed = (etime - stime);
cTotal+=compiledSpeed;
log(compiledSpeed+"ms ");
}
logln();
reportPerformance("complex expression",iTotal, cTotal);
// Verify the result
assertFalse(b);
// Verify the same result for compiled vs interpreted
assertEquals(b,bc);
// Verify if the input changes, the result changes
payload.DR[0].DRFixedSection.duration = 0.04d;
bc = expression.getValue(payload,Boolean.TYPE);
assertTrue(bc);
}
public static class HW {
public String hello() {
return "foobar";
}
}
@Test
public void compilingMethodReference() throws Exception {
long interpretedTotal = 0, compiledTotal = 0;
long stime,etime;
String interpretedResult = null,compiledResult = null;
HW testdata = new HW();
Expression expression = parser.parseExpression("hello()");
// warmup
for (int i=0;i<count;i++) {
interpretedResult = expression.getValue(testdata,String.class);
}
log("timing interpreted: ");
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
interpretedResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long interpretedSpeed = (etime - stime);
interpretedTotal+=interpretedSpeed;
log(interpretedSpeed+"ms ");
}
logln();
compile(expression);
log("timing compiled: ");
expression.getValue(testdata,String.class);
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
compiledResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long compiledSpeed = (etime - stime);
compiledTotal+=compiledSpeed;
log(compiledSpeed+"ms ");
}
logln();
assertEquals(interpretedResult,compiledResult);
reportPerformance("method reference", interpretedTotal, compiledTotal);
if (compiledTotal>=interpretedTotal) {
fail("Compiled version is slower than interpreted!");
}
}
public static class TestClass2 {
public String name = "Santa";
private String name2 = "foobar";
public String getName2() {
return name2;
}
public Foo foo = new Foo();
public static class Foo {
public Bar bar = new Bar();
Bar b = new Bar();
public Bar getBaz() {
return b;
}
public Bar bay() {
return b;
}
}
public static class Bar {
public String boo = "oranges";
}
}
@Test
public void compilingPropertyReferenceField() throws Exception {
long interpretedTotal = 0, compiledTotal = 0, stime, etime;
String interpretedResult = null, compiledResult = null;
TestClass2 testdata = new TestClass2();
Expression expression = parser.parseExpression("name");
// warmup
for (int i=0;i<count;i++) {
expression.getValue(testdata,String.class);
}
log("timing interpreted: ");
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
interpretedResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long interpretedSpeed = (etime - stime);
interpretedTotal+=interpretedSpeed;
log(interpretedSpeed+"ms ");
}
logln();
compile(expression);
log("timing compiled: ");
expression.getValue(testdata,String.class);
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
compiledResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long compiledSpeed = (etime - stime);
compiledTotal+=compiledSpeed;
log(compiledSpeed+"ms ");
}
logln();
assertEquals(interpretedResult,compiledResult);
reportPerformance("property reference (field)",interpretedTotal, compiledTotal);
}
@Test
public void compilingPropertyReferenceNestedField() throws Exception {
long interpretedTotal = 0, compiledTotal = 0, stime, etime;
String interpretedResult = null, compiledResult = null;
TestClass2 testdata = new TestClass2();
Expression expression = parser.parseExpression("foo.bar.boo");
// warmup
for (int i=0;i<count;i++) {
expression.getValue(testdata,String.class);
}
log("timing interpreted: ");
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
interpretedResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long interpretedSpeed = (etime - stime);
interpretedTotal+=interpretedSpeed;
log(interpretedSpeed+"ms ");
}
logln();
compile(expression);
log("timing compiled: ");
expression.getValue(testdata,String.class);
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
compiledResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long compiledSpeed = (etime - stime);
compiledTotal+=compiledSpeed;
log(compiledSpeed+"ms ");
}
logln();
assertEquals(interpretedResult,compiledResult);
reportPerformance("property reference (nested field)",interpretedTotal, compiledTotal);
}
@Test
public void compilingPropertyReferenceNestedMixedFieldGetter() throws Exception {
long interpretedTotal = 0, compiledTotal = 0, stime, etime;
String interpretedResult = null, compiledResult = null;
TestClass2 testdata = new TestClass2();
Expression expression = parser.parseExpression("foo.baz.boo");
// warmup
for (int i=0;i<count;i++) {
expression.getValue(testdata,String.class);
}
log("timing interpreted: ");
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
interpretedResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long interpretedSpeed = (etime - stime);
interpretedTotal+=interpretedSpeed;
log(interpretedSpeed+"ms ");
}
logln();
compile(expression);
log("timing compiled: ");
expression.getValue(testdata,String.class);
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
compiledResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long compiledSpeed = (etime - stime);
compiledTotal+=compiledSpeed;
log(compiledSpeed+"ms ");
}
logln();
assertEquals(interpretedResult,compiledResult);
reportPerformance("nested property reference (mixed field/getter)",interpretedTotal, compiledTotal);
}
@Test
public void compilingNestedMixedFieldPropertyReferenceMethodReference() throws Exception {
long interpretedTotal = 0, compiledTotal = 0, stime, etime;
String interpretedResult = null, compiledResult = null;
TestClass2 testdata = new TestClass2();
Expression expression = parser.parseExpression("foo.bay().boo");
// warmup
for (int i=0;i<count;i++) {
expression.getValue(testdata,String.class);
}
log("timing interpreted: ");
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
interpretedResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long interpretedSpeed = (etime - stime);
interpretedTotal+=interpretedSpeed;
log(interpretedSpeed+"ms ");
}
logln();
compile(expression);
log("timing compiled: ");
expression.getValue(testdata,String.class);
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
compiledResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long compiledSpeed = (etime - stime);
compiledTotal+=compiledSpeed;
log(compiledSpeed+"ms ");
}
logln();
assertEquals(interpretedResult,compiledResult);
reportPerformance("nested reference (mixed field/method)",interpretedTotal, compiledTotal);
}
@Test
public void compilingPropertyReferenceGetter() throws Exception {
long interpretedTotal = 0, compiledTotal = 0, stime, etime;
String interpretedResult = null, compiledResult = null;
TestClass2 testdata = new TestClass2();
Expression expression = parser.parseExpression("name2");
// warmup
for (int i=0;i<count;i++) {
expression.getValue(testdata,String.class);
}
log("timing interpreted: ");
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
interpretedResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long interpretedSpeed = (etime - stime);
interpretedTotal+=interpretedSpeed;
log(interpretedSpeed+"ms ");
}
logln();
compile(expression);
log("timing compiled: ");
expression.getValue(testdata,String.class);
for (int iter=0;iter<iterations;iter++) {
stime = System.currentTimeMillis();
for (int i=0;i<count;i++) {
compiledResult = expression.getValue(testdata,String.class);
}
etime = System.currentTimeMillis();
long compiledSpeed = (etime - stime);
compiledTotal+=compiledSpeed;
log(compiledSpeed+"ms ");
}
logln();
assertEquals(interpretedResult,compiledResult);
reportPerformance("property reference (getter)", interpretedTotal, compiledTotal);
if (compiledTotal>=interpretedTotal) {
fail("Compiled version is slower than interpreted!");
}
}
// ---
private void reportPerformance(String title, long interpretedTotal, long compiledTotal) {
double averageInterpreted = interpretedTotal/(iterations);
double averageCompiled = compiledTotal/(iterations);
double ratio = (averageCompiled/averageInterpreted)*100.0d;
logln(">>"+title+": average for "+count+": compiled="+averageCompiled+"ms interpreted="+averageInterpreted+"ms: compiled takes "+((int)ratio)+"% of the interpreted time");
if (averageCompiled>averageInterpreted) {
fail("Compiled version took longer than interpreted! CompiledSpeed=~"+averageCompiled+
"ms InterpretedSpeed="+averageInterpreted+"ms");
}
logln();
}
private void log(String message) {
if (noisyTests) {
System.out.print(message);
}
}
private void logln(String... message) {
if (noisyTests) {
if (message!=null && message.length>0) {
System.out.println(message[0]);
} else {
System.out.println();
}
}
}
private void compile(Expression expression) {
assertTrue(SpelCompiler.compile(expression));
}
}