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

Feature/cacheHashCode #2513

Merged
merged 18 commits into from
Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Lombok contributors in alphabetical order:

Adam Juraszek <[email protected]>
Aleksandr Zhelezniak <[email protected]>
Andre Brait <[email protected]>
Bulgakov Alexander <[email protected]>
Caleb Brinkman <[email protected]>
Christian Nüssgens <[email protected]>
Expand Down
23 changes: 23 additions & 0 deletions src/core/lombok/EqualsAndHashCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@
* @return If {@code true}, always use direct field access instead of calling the getter method.
*/
boolean doNotUseGetters() default false;

/**
* Determines how the result of the {@code hashCode} method will be cached.
* <strong>default: {@link CacheStrategy#NEVER}</strong>
*
* @return The {@code hashCode} cache strategy to be used.
*/
CacheStrategy cacheStrategy() default CacheStrategy.NEVER;

/**
* Any annotations listed here are put on the generated parameter of {@code equals} and {@code canEqual}.
Expand Down Expand Up @@ -132,4 +140,19 @@
*/
int rank() default 0;
}

public enum CacheStrategy {
/**
* Never cache. Perform the calculation every time the method is called.
*/
NEVER,
/**
* Cache the result of the first invocation of {@code hashCode} and use it for subsequent invocations.
* This can improve performance if all fields used for calculating the {@code hashCode} are immutable
* and thus every invocation of {@code hashCode} will always return the same value.
* <strong>Do not use this if there's <em>any</em> chance that different invocations of {@code hashCode}
* might return different values.</strong>
*/
LAZY
}
}
85 changes: 79 additions & 6 deletions src/core/lombok/eclipse/handlers/HandleEqualsAndHashCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import lombok.AccessLevel;
import lombok.ConfigurationKeys;
import lombok.EqualsAndHashCode;
import lombok.EqualsAndHashCode.CacheStrategy;
import lombok.core.AST.Kind;
import lombok.core.handlers.HandlerUtil;
import lombok.core.handlers.InclusionExclusionUtils;
Expand All @@ -59,6 +60,8 @@
import org.eclipse.jdt.internal.compiler.ast.EqualExpression;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FalseLiteral;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldReference;
import org.eclipse.jdt.internal.compiler.ast.IfStatement;
import org.eclipse.jdt.internal.compiler.ast.InstanceOfExpression;
import org.eclipse.jdt.internal.compiler.ast.IntLiteral;
Expand Down Expand Up @@ -94,6 +97,9 @@
@ProviderFor(EclipseAnnotationHandler.class)
public class HandleEqualsAndHashCode extends EclipseAnnotationHandler<EqualsAndHashCode> {

private static final String HASH_CODE_CACHE_NAME = "$hashCodeCache";

private final char[] HASH_CODE_CACHE_NAME_ARR = HASH_CODE_CACHE_NAME.toCharArray();
private final char[] PRIME = "PRIME".toCharArray();
private final char[] RESULT = "result".toCharArray();

Expand All @@ -116,7 +122,9 @@ public class HandleEqualsAndHashCode extends EclipseAnnotationHandler<EqualsAndH
boolean doNotUseGetters = annotation.isExplicit("doNotUseGetters") || doNotUseGettersConfiguration == null ? ann.doNotUseGetters() : doNotUseGettersConfiguration;
FieldAccess fieldAccess = doNotUseGetters ? FieldAccess.PREFER_FIELD : FieldAccess.GETTER;

generateMethods(annotationNode.up(), annotationNode, members, callSuper, true, fieldAccess, onParam);
boolean cacheHashCode = ann.cacheStrategy() == CacheStrategy.LAZY;

generateMethods(annotationNode.up(), annotationNode, members, callSuper, true, cacheHashCode, fieldAccess, onParam);
}

public void generateEqualsAndHashCodeForType(EclipseNode typeNode, EclipseNode errorNode) {
Expand All @@ -130,11 +138,11 @@ public void generateEqualsAndHashCodeForType(EclipseNode typeNode, EclipseNode e
Boolean doNotUseGettersConfiguration = typeNode.getAst().readConfiguration(ConfigurationKeys.EQUALS_AND_HASH_CODE_DO_NOT_USE_GETTERS);
FieldAccess access = doNotUseGettersConfiguration == null || !doNotUseGettersConfiguration ? FieldAccess.GETTER : FieldAccess.PREFER_FIELD;

generateMethods(typeNode, errorNode, members, null, false, access, new ArrayList<Annotation>());
generateMethods(typeNode, errorNode, members, null, false, false, access, new ArrayList<Annotation>());
}

public void generateMethods(EclipseNode typeNode, EclipseNode errorNode, List<Included<EclipseNode, EqualsAndHashCode.Include>> members,
Boolean callSuper, boolean whineIfExists, FieldAccess fieldAccess, List<Annotation> onParam) {
Boolean callSuper, boolean whineIfExists, boolean cacheHashCode, FieldAccess fieldAccess, List<Annotation> onParam) {

TypeDeclaration typeDecl = null;

Expand Down Expand Up @@ -222,12 +230,34 @@ public void generateMethods(EclipseNode typeNode, EclipseNode errorNode, List<In
injectMethod(typeNode, canEqualMethod);
}

MethodDeclaration hashCodeMethod = createHashCode(typeNode, members, callSuper, errorNode.get(), fieldAccess);
if (cacheHashCode){
if (fieldExists(HASH_CODE_CACHE_NAME, typeNode) != MemberExistsResult.NOT_EXISTS) {
String msg = String.format("Not caching the result of hashCode: A field named %s already exists.", HASH_CODE_CACHE_NAME);
errorNode.addWarning(msg);
cacheHashCode = false;
} else {
createHashCodeCacheField(typeNode, errorNode.get());
}
}

MethodDeclaration hashCodeMethod = createHashCode(typeNode, members, callSuper, cacheHashCode, errorNode.get(), fieldAccess);
hashCodeMethod.traverse(new SetGeneratedByVisitor(errorNode.get()), ((TypeDeclaration)typeNode.get()).scope);
injectMethod(typeNode, hashCodeMethod);
}

private void createHashCodeCacheField(EclipseNode typeNode, ASTNode source) {
FieldDeclaration hashCodeCacheDecl = new FieldDeclaration(HASH_CODE_CACHE_NAME_ARR, 0, -1);
hashCodeCacheDecl.modifiers = ClassFileConstants.AccPrivate | ClassFileConstants.AccTransient;
hashCodeCacheDecl.bits |= Eclipse.ECLIPSE_DO_NOT_TOUCH_FLAG;
hashCodeCacheDecl.type = TypeReference.baseTypeReference(TypeIds.T_int, 0);
hashCodeCacheDecl.declarationSourceEnd = -1;
hashCodeCacheDecl.initialization = makeIntLiteral("0".toCharArray(), source);
injectFieldAndMarkGenerated(typeNode, hashCodeCacheDecl);
setGeneratedBy(hashCodeCacheDecl, source);
setGeneratedBy(hashCodeCacheDecl.type, source);
}

public MethodDeclaration createHashCode(EclipseNode type, Collection<Included<EclipseNode, EqualsAndHashCode.Include>> members, boolean callSuper, ASTNode source, FieldAccess fieldAccess) {
public MethodDeclaration createHashCode(EclipseNode type, Collection<Included<EclipseNode, EqualsAndHashCode.Include>> members, boolean callSuper, boolean cacheHashCode, ASTNode source, FieldAccess fieldAccess) {
int pS = source.sourceStart, pE = source.sourceEnd;
long p = (long) pS << 32 | pE;

Expand All @@ -238,7 +268,10 @@ public MethodDeclaration createHashCode(EclipseNode type, Collection<Included<Ec
method.returnType = TypeReference.baseTypeReference(TypeIds.T_int, 0);
setGeneratedBy(method.returnType, source);
Annotation overrideAnnotation = makeMarkerAnnotation(TypeConstants.JAVA_LANG_OVERRIDE, source);
if (getCheckerFrameworkVersion(type).generateSideEffectFree()) {
CheckerFrameworkVersion checkerFramework = getCheckerFrameworkVersion(type);
if (cacheHashCode && checkerFramework.generatePure()) {
method.annotations = new Annotation[] { overrideAnnotation, generateNamedAnnotation(source, CheckerFrameworkVersion.NAME__PURE) };
} else if (checkerFramework.generateSideEffectFree()) {
method.annotations = new Annotation[] { overrideAnnotation, generateNamedAnnotation(source, CheckerFrameworkVersion.NAME__SIDE_EFFECT_FREE) };
} else {
method.annotations = new Annotation[] { overrideAnnotation };
Expand All @@ -262,6 +295,22 @@ public MethodDeclaration createHashCode(EclipseNode type, Collection<Included<Ec
}
}

/* if (this.$hashCodeCache != 0) return this.$hashCodeCache; */ {
if (cacheHashCode) {
FieldReference hashCodeCacheRef = new FieldReference(HASH_CODE_CACHE_NAME_ARR, p);
hashCodeCacheRef.receiver = new ThisReference(pS, pE);
setGeneratedBy(hashCodeCacheRef, source);
setGeneratedBy(hashCodeCacheRef.receiver, source);
EqualExpression cacheNotZero = new EqualExpression(hashCodeCacheRef, makeIntLiteral("0".toCharArray(), source), OperatorIds.NOT_EQUAL);
setGeneratedBy(cacheNotZero, source);
ReturnStatement returnCache = new ReturnStatement(hashCodeCacheRef, pS, pE);
setGeneratedBy(returnCache, source);
IfStatement ifStatement = new IfStatement(cacheNotZero, returnCache, pS, pE);
setGeneratedBy(ifStatement, source);
statements.add(ifStatement);
}
}

/* final int PRIME = X; */ {
/* Without members, PRIME isn't used, as that would trigger a 'local variable not used' warning. */
if (!isEmpty) {
Expand Down Expand Up @@ -391,6 +440,30 @@ public MethodDeclaration createHashCode(EclipseNode type, Collection<Included<Ec
}
}

/* this.$hashCodeCache = result != 0 ? result : Integer.MIN_VALUE; */ {
if (cacheHashCode) {
FieldReference hashCodeCacheRef = new FieldReference(HASH_CODE_CACHE_NAME_ARR, p);
hashCodeCacheRef.receiver = new ThisReference(pS, pE);
setGeneratedBy(hashCodeCacheRef, source);
setGeneratedBy(hashCodeCacheRef.receiver, source);
SingleNameReference resultRef = new SingleNameReference(RESULT, p);
setGeneratedBy(resultRef, source);
EqualExpression resultNotZero = new EqualExpression(resultRef, makeIntLiteral("0".toCharArray(), source), OperatorIds.NOT_EQUAL);
setGeneratedBy(resultNotZero, source);
FieldReference integerMinValue = new FieldReference("MIN_VALUE".toCharArray(), p);
integerMinValue.receiver = generateQualifiedNameRef(source, TypeConstants.JAVA_LANG_INTEGER);
setGeneratedBy(integerMinValue, source);
resultRef = new SingleNameReference(RESULT, p);
setGeneratedBy(resultRef, source);
ConditionalExpression notZeroOrIntegerMin = new ConditionalExpression(resultNotZero, resultRef, integerMinValue);
setGeneratedBy(notZeroOrIntegerMin, source);
Assignment cacheResult = new Assignment(hashCodeCacheRef, notZeroOrIntegerMin, pE);
cacheResult.sourceStart = pS; cacheResult.statementEnd = cacheResult.sourceEnd = pE;
setGeneratedBy(cacheResult, source);
statements.add(cacheResult);
}
}

/* return result; */ {
SingleNameReference resultRef = new SingleNameReference(RESULT, p);
setGeneratedBy(resultRef, source);
Expand Down
64 changes: 57 additions & 7 deletions src/core/lombok/javac/handlers/HandleEqualsAndHashCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@
import com.sun.tools.javac.tree.JCTree.JCBinary;
import com.sun.tools.javac.tree.JCTree.JCBlock;
import com.sun.tools.javac.tree.JCTree.JCClassDecl;
import com.sun.tools.javac.tree.JCTree.JCConditional;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCExpressionStatement;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.JCTree.JCModifiers;
Expand All @@ -56,6 +59,7 @@

import lombok.ConfigurationKeys;
import lombok.EqualsAndHashCode;
import lombok.EqualsAndHashCode.CacheStrategy;
import lombok.core.AST.Kind;
import lombok.core.AnnotationValues;
import lombok.core.configuration.CallSuperType;
Expand All @@ -76,11 +80,13 @@
public class HandleEqualsAndHashCode extends JavacAnnotationHandler<EqualsAndHashCode> {
private static final String RESULT_NAME = "result";
private static final String PRIME_NAME = "PRIME";
private static final String HASH_CODE_CACHE_NAME = "$hashCodeCache";

@Override public void handle(AnnotationValues<EqualsAndHashCode> annotation, JCAnnotation ast, JavacNode annotationNode) {
handleFlagUsage(annotationNode, ConfigurationKeys.EQUALS_AND_HASH_CODE_FLAG_USAGE, "@EqualsAndHashCode");

deleteAnnotationIfNeccessary(annotationNode, EqualsAndHashCode.class);
deleteImportFromCompilationUnit(annotationNode, CacheStrategy.class.getName());
EqualsAndHashCode ann = annotation.getInstance();
java.util.List<Included<JavacNode, EqualsAndHashCode.Include>> members = InclusionExclusionUtils.handleEqualsAndHashCodeMarking(annotationNode.up(), annotation, annotationNode);
JavacNode typeNode = annotationNode.up();
Expand All @@ -92,8 +98,10 @@ public class HandleEqualsAndHashCode extends JavacAnnotationHandler<EqualsAndHas
Boolean doNotUseGettersConfiguration = annotationNode.getAst().readConfiguration(ConfigurationKeys.EQUALS_AND_HASH_CODE_DO_NOT_USE_GETTERS);
boolean doNotUseGetters = annotation.isExplicit("doNotUseGetters") || doNotUseGettersConfiguration == null ? ann.doNotUseGetters() : doNotUseGettersConfiguration;
FieldAccess fieldAccess = doNotUseGetters ? FieldAccess.PREFER_FIELD : FieldAccess.GETTER;

generateMethods(typeNode, annotationNode, members, callSuper, true, fieldAccess, onParam);

boolean cacheHashCode = ann.cacheStrategy() == CacheStrategy.LAZY;

generateMethods(typeNode, annotationNode, members, callSuper, true, cacheHashCode, fieldAccess, onParam);
}

public void generateEqualsAndHashCodeForType(JavacNode typeNode, JavacNode source) {
Expand All @@ -107,11 +115,11 @@ public void generateEqualsAndHashCodeForType(JavacNode typeNode, JavacNode sourc

java.util.List<Included<JavacNode, EqualsAndHashCode.Include>> members = InclusionExclusionUtils.handleEqualsAndHashCodeMarking(typeNode, null, null);

generateMethods(typeNode, source, members, null, false, access, List.<JCAnnotation>nil());
generateMethods(typeNode, source, members, null, false, false, access, List.<JCAnnotation>nil());
}

public void generateMethods(JavacNode typeNode, JavacNode source, java.util.List<Included<JavacNode, EqualsAndHashCode.Include>> members,
Boolean callSuper, boolean whineIfExists, FieldAccess fieldAccess, List<JCAnnotation> onParam) {
Boolean callSuper, boolean whineIfExists, boolean cacheHashCode, FieldAccess fieldAccess, List<JCAnnotation> onParam) {

boolean notAClass = true;
if (typeNode.get() instanceof JCClassDecl) {
Expand Down Expand Up @@ -196,17 +204,39 @@ public void generateMethods(JavacNode typeNode, JavacNode source, java.util.List
injectMethod(typeNode, canEqualMethod);
}

JCMethodDecl hashCodeMethod = createHashCode(typeNode, members, callSuper, fieldAccess, source.get());
if (cacheHashCode){
if (fieldExists(HASH_CODE_CACHE_NAME, typeNode) != MemberExistsResult.NOT_EXISTS) {
String msg = String.format("Not caching the result of hashCode: A field named %s already exists.", HASH_CODE_CACHE_NAME);
source.addWarning(msg);
cacheHashCode = false;
} else {
createHashCodeCacheField(typeNode, source.get());
}
}

JCMethodDecl hashCodeMethod = createHashCode(typeNode, members, callSuper, cacheHashCode, fieldAccess, source.get());
injectMethod(typeNode, hashCodeMethod);
}

private void createHashCodeCacheField(JavacNode typeNode, JCTree source) {
JavacTreeMaker maker = typeNode.getTreeMaker();
JCModifiers mods = maker.Modifiers(Flags.PRIVATE | Flags.TRANSIENT);
JCVariableDecl hashCodeCacheField = maker.VarDef(mods, typeNode.toName(HASH_CODE_CACHE_NAME), maker.TypeIdent(CTC_INT), maker.Literal(CTC_INT, 0));
injectFieldAndMarkGenerated(typeNode, hashCodeCacheField);
recursiveSetGeneratedBy(hashCodeCacheField, source, typeNode.getContext());
}

public JCMethodDecl createHashCode(JavacNode typeNode, java.util.List<Included<JavacNode, EqualsAndHashCode.Include>> members, boolean callSuper, FieldAccess fieldAccess, JCTree source) {
public JCMethodDecl createHashCode(JavacNode typeNode, java.util.List<Included<JavacNode, EqualsAndHashCode.Include>> members, boolean callSuper, boolean cacheHashCode, FieldAccess fieldAccess, JCTree source) {
JavacTreeMaker maker = typeNode.getTreeMaker();

JCAnnotation overrideAnnotation = maker.Annotation(genJavaLangTypeRef(typeNode, "Override"), List.<JCExpression>nil());
List<JCAnnotation> annsOnMethod = List.of(overrideAnnotation);
CheckerFrameworkVersion checkerFramework = getCheckerFrameworkVersion(typeNode);
if (checkerFramework.generateSideEffectFree()) annsOnMethod = annsOnMethod.prepend(maker.Annotation(genTypeRef(typeNode, CheckerFrameworkVersion.NAME__SIDE_EFFECT_FREE), List.<JCExpression>nil()));
if (cacheHashCode && checkerFramework.generatePure()) {
annsOnMethod = annsOnMethod.prepend(maker.Annotation(genTypeRef(typeNode, CheckerFrameworkVersion.NAME__PURE), List.<JCExpression>nil()));
} else if (checkerFramework.generateSideEffectFree()) {
annsOnMethod = annsOnMethod.prepend(maker.Annotation(genTypeRef(typeNode, CheckerFrameworkVersion.NAME__SIDE_EFFECT_FREE), List.<JCExpression>nil()));
}
JCModifiers mods = maker.Modifiers(Flags.PUBLIC, annsOnMethod);
JCExpression returnType = maker.TypeIdent(CTC_INT);
ListBuffer<JCStatement> statements = new ListBuffer<JCStatement>();
Expand All @@ -217,6 +247,15 @@ public JCMethodDecl createHashCode(JavacNode typeNode, java.util.List<Included<J

boolean isEmpty = members.isEmpty();

/* if (this.$hashCodeCache != 0) return this.$hashCodeCache; */ {
if (cacheHashCode) {
JCIdent receiver = maker.Ident(typeNode.toName("this"));
JCFieldAccess cacheHashCodeFieldAccess = maker.Select(receiver, typeNode.toName(HASH_CODE_CACHE_NAME));
JCExpression cacheNotZero = maker.Binary(CTC_NOT_EQUAL, cacheHashCodeFieldAccess, maker.Literal(CTC_INT, 0));
statements.append(maker.If(cacheNotZero, maker.Return(cacheHashCodeFieldAccess), null));
}
}

/* final int PRIME = X; */ {
if (!isEmpty) {
statements.append(maker.VarDef(maker.Modifiers(finalFlag), primeName, maker.TypeIdent(CTC_INT), maker.Literal(HandlerUtil.primeForHashcode())));
Expand Down Expand Up @@ -306,6 +345,17 @@ public JCMethodDecl createHashCode(JavacNode typeNode, java.util.List<Included<J
}
}

/* this.$hashCodeCache = result != 0 ? result : Integer.MIN_VALUE; */ {
if (cacheHashCode) {
JCIdent receiver = maker.Ident(typeNode.toName("this"));
JCFieldAccess cacheHashCodeFieldAccess = maker.Select(receiver, typeNode.toName(HASH_CODE_CACHE_NAME));
JCExpression resultNotZero = maker.Binary(CTC_NOT_EQUAL, maker.Ident(resultName), maker.Literal(CTC_INT, 0));
JCExpression integerMinValue = genJavaLangTypeRef(typeNode, "Integer", "MIN_VALUE");
JCConditional notZeroOrIntegerMin = maker.Conditional(resultNotZero, maker.Ident(resultName), integerMinValue);
statements.append(maker.Exec(maker.Assign(cacheHashCodeFieldAccess, notZeroOrIntegerMin)));
}
}

/* return result; */ {
statements.append(maker.Return(maker.Ident(resultName)));
}
Expand Down
Loading