Eliminate lambda indirection overhead in DynamoDB Enhanced Client
Motivation and Context
The DynamoDB Enhanced Client v2 shows significant performance regression compared to v1 DynamoDB Mapper, with 32-98% slower operations across all operations. One theory was that lambda allocation overhead in ResolvedImmutableAttribute.attributeGetterMethod() is a key bottleneck affecting all operations in the serialization hot path.
The change refactors how attribute values are extracted during serialization by replacing an inline lambda expression with a method reference to a dedicated instance method. Profiling shows this reduces allocations of iterator and builder objects, with LinkedHashMap$LinkedEntryIterator allocations dropping 44% and DefaultDynamoDbExtensionContext$Builder allocations dropping 30%. The method reference approach appears to allow the JVM to optimize the call path more effectively, though the exact mechanism linking the code change to these specific allocation reductions is not fully clear from profiling data.
The optimization shows the strongest impact on small objects, with Get TINY operations improving 47% compared to 7% for Get HUGE, suggesting the allocation overhead represents a larger proportion of total processing time for smaller payloads.
Change
Before:
Function<T, AttributeValue> getAttributeValueWithTransform = item -> {
R value = immutableAttribute.getter().apply(item); // Lambda indirection
return value == null ? nullAttributeValue() : attributeType.objectToAttributeValue(value);
};
After:
public Function<T, AttributeValue> attributeGetterMethod() {
return this::getAttributeValue;
}
AttributeValue getAttributeValue(T item) {
R value = getter.apply(item);
return value == null ? nullAttributeValue() : attributeType.objectToAttributeValue(value);
}
Results
running existing benchmarks for test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/enhanced/dynamodb
| Operation | Size | V1 (ops/s) | Before Fix | After Fix | Fix Improvement | Remaining Gap to V1 |
|---|---|---|---|---|---|---|
| Delete | TINY | 12,149,315 | 10,699,554 | 11,865,284 | +11% | -2% |
| Delete | SMALL | 12,198,550 | 10,826,259 | 11,094,164 | +2% | -9% |
| Delete | HUGE | 12,247,132 | 10,704,169 | 11,379,428 | +6% | -7% |
| Get | TINY | 7,243,909 | 4,561,782 | 6,720,756 | +47% | -7% |
| Get | SMALL | 4,920,953 | 3,015,931 | 3,583,527 | +19% | -27% |
| Get | HUGE | 554,562 | 289,412 | 311,014 | +7% | -44% |
| Put | TINY | 6,824,135 | 4,672,933 | 5,870,566 | +26% | -14% |
| Put | SMALL | 4,296,701 | 2,605,126 | 2,807,175 | +8% | -35% |
| Put | HUGE | 582,056 | 225,242 | 223,333 | -1% | -62% |
| Update | TINY | 4,539,823 | 2,288,870 | 2,632,113 | +15% | -42% |
| Update | SMALL | 2,862,813 | 242,741 | 242,506 | +0% | -92% |
| Update | HUGE | 551,769 | 54,629 | 54,374 | +0% | -90% |
JFR Profiling (Get TINY benchmark):
Allocation Impact:
| Object Type | Before | After | Change | % |
|---|---|---|---|---|
| LinkedHashMap$LinkedEntryIterator | 1,000 | 565 | -435 | -44% |
| DefaultDynamoDbExtensionContext$Builder | 1,390 | 974 | -416 | -30% |
| DefaultDynamoDbExtensionContext | 1,431 | 1,170 | -261 | -18% |
| EnhancedAttributeValue$InternalBuilder | 871 | 678 | -193 | -22% |
| GetItemEnhancedResponse | 320 | 170 | -150 | -47% |
| HashMap$Node[] | 1,865 | 1,627 | -238 | -13% |
| LinkedHashMap | 1,404 | 1,211 | -193 | -14% |
| HashMap | 871 | 543 | -328 | -38% |
| EnhancedAttributeValue | 664 | 836 | +172 | +26% |
| LinkedHashMap$Entry | 590 | 466 | -124 | -21% |
CPU Impact:
| Metric | Before | After | Change | % |
|---|---|---|---|---|
| Total CPU Samples | 2,128 | 2,037 | -91 | -4.3% |
Quality Gate failed
Failed conditions
C Reliability Rating on New Code (required ≥ A)
See analysis details on SonarQube Cloud
Catch issues before they fail your Quality Gate with our IDE extension
SonarQube for IDE