Repository: spring-projects/spring-framework

Issue #18542: Improve CacheOperation equals/hashCode implementation performance    [SPR-13970]
Created: 2016-02-22T19:45:36Z
Khaled Ayoubi opened SPR-13970 and commented
"CacheAspectSupport" tries to get a cached version of "CacheOperationMetadata" from "metadataCache" using "CacheOperationCacheKey" as a key
"CacheOperationCacheKey.equals" method calls "CacheOperation.equals" which calls "toString" which is pretty expensive, if I'm in large loop and hitting the cache, I can notice considerable degradation in performance

Affects: 4.2.4
Issue Links:

#20549 Inefficient equals logic in Spring Cache ("is duplicated by")
#21033 @Cacheable annotation lead to a huge memory allocation from the side of OperationCache equals/hashCode ("is duplicated by")

Duplicate issue #21033: @Cacheable annotation lead to a huge memory allocation from the side of OperationCache equals/hashCode [SPR-16490]
Created: 2018-02-13T15:19:15Z
Alexander Fedorov opened SPR-16490 and commented
Hello Team,
we have faced a very intensive CPU consuming. One of the reason is very high GC activity.
GC activity could be splitted into two points:

minor GC collections which should be done very quickly in parallel
major GC collections ( FullGC ) which should be a rare case and should not lead to a long delay from app ( because FullGC is stop the world in our case ).

Minor GC could be tuned with help of investigating of memory allocation events inside new TLAB.
Major GC could be tuned with help of investigating of memory allocation events outside TLAB.
We use JMC and JFR API for aggregating memory allocation events.
It looks like


Memory allocation events inside new TLAB
!allocation_inside_TLAB.jpg|thumbnail!


Memory allocation events outside TLAB
!allocation_outside_TLAB.jpg|thumbnail!


Where higlighted categories are:
spring.cache_interceptor  org.springframework.cache.interceptor
spring.TransactionInterceptor.invoke org.springframework.transaction.interceptor.TransactionInterceptor.invoke()

Hotspots looks like:
!allocation_inside_TLAB_hotspots.jpg|thumbnail!
Issue is here:
!decompiled.jpg|thumbnail!
protected StringBuilder getOperationDescription()
 {
   StringBuilder result = new StringBuilder(getClass().getSimpleName());
   result.append("[").append(this.name);
   result.append("] caches=").append(this.cacheNames);
   result.append(" | key='").append(this.key);
   result.append("' | keyGenerator='").append(this.keyGenerator);
   result.append("' | cacheManager='").append(this.cacheManager);
   result.append("' | cacheResolver='").append(this.cacheResolver);
   result.append("' | condition='").append(this.condition).append("'");
   return result;
 }

Our version is
Manifest-Version: 1.0
Created-By: 1.8.0_20 (Oracle Corporation)
Implementation-Title: spring-context
Implementation-Version: 4.1.3.RELEASE

But issue actual for latest version too.
I suggest that it could be implemented as below:
@Override
	public String toString() {
		if( toStringCachedResult == null ) {
			toStringCachedResult = getOperationDescription().toString().intern();
		}
		return toStringCachedResult;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;

		CacheOperation that = (CacheOperation) o;

		return toString().equals(that.toString());

	}



	@Override
	public int hashCode() {
		if(hashCodeCachedResult == 0 ) {
			hashCodeCachedResult = toString().hashCode();
		}
		return hashCodeCachedResult;
	}

	private int hashCodeCachedResult = 0;
	private String toStringCachedResult = null;

	/**
	 * Return an identifying description for this caching operation.
	 * <p>Available to subclasses, for inclusion in their {@code toString()} result.
	 */
	protected StringBuilder getOperationDescription() {
		StringBuilder result = new StringBuilder(getClass().getSimpleName());
		result./*append("[").*/append(this.name);
		result./*append("] caches=").*/append(this.cacheNames);
		result./*append(" | key='").*/append(this.key);
		result./*append("' | keyGenerator='").*/append(this.keyGenerator);
		result./*append("' | cacheManager='").*/append(this.cacheManager);
		result./*append("' | cacheResolver='").*/append(this.cacheResolver);
		result./*append("' | condition='").*/append(this.condition)/*.append("'")*/;
		return result;
	}

Main idea is caching and interning string objects for minimizing memory allocation from the side of method equals and compare int instead of String for hashCode.
Our cusomized version of [^CacheOperation.java] is attached.
Thank you!

Affects: 4.3.14
Attachments:

allocation_inside_TLAB_hotspots.jpg (66.51 kB)
allocation_inside_TLAB.jpg (20.01 kB)
allocation_outside_TLAB.jpg (20.90 kB)
CacheOperation.java (4.92 kB)
decompiled.jpg (110.30 kB)

Issue Links:

#20549 Inefficient equals logic in Spring Cache ("duplicates")
#18542 Improve CacheOperation equals/hashCode implementation performance    ("duplicates")
#21035 @Transactional annotation lead to a huge memory allocation during creation String representation of Method uses for logging only

Closing comment (2018-02-13T15:27:54Z): Juergen Hoeller commented
This looks like a duplicate of #18542 and #20549 which have been addressed in the 4.3.x line. So while your report looks entirely reasonable against 4.1.x, please double-check against 4.3.14 where no such patching should be necessary.