Skip to content

Implement native method to calculate hashcode for valuetypes#23502

Draft
AditiS11 wants to merge 5 commits intoeclipse-openj9:masterfrom
AditiS11:valuetypeHashCode
Draft

Implement native method to calculate hashcode for valuetypes#23502
AditiS11 wants to merge 5 commits intoeclipse-openj9:masterfrom
AditiS11:valuetypeHashCode

Conversation

@AditiS11
Copy link
Copy Markdown

@AditiS11 AditiS11 commented Mar 12, 2026

-Add native method to calculate hashcode for valuetypes.
-Modify GC path to return/store 0 for value types,
to eliminate hashcode calculation during object copying.
-Ensure hashcode calculation is only triggered by the VM.
-Enable ValueTypeTests:testValueClassHashCode

Fixes: #15768

This change follows the approach described in 15768(comment)

@AditiS11
Copy link
Copy Markdown
Author

With these changes the following OpenJDK tests are passing:

  • valhalla/valuetypes/Reflection.java
  • valhalla/valuetypes/StreamTest.java
  • valhalla/valuetypes/RecursiveValueClass.java

@theresa-m theresa-m self-requested a review March 12, 2026 16:30
@theresa-m theresa-m added comp:vm project:valhalla Used to track Project Valhalla related work labels Mar 12, 2026
@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch 2 times, most recently from 29d6177 to 6f419cf Compare March 13, 2026 11:42
Copy link
Copy Markdown
Contributor

@theresa-m theresa-m left a comment

Choose a reason for hiding this comment

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

First pass.

/* https://github.com/eclipse-openj9/openj9/issues/15768 */
@Test(priority=1, enabled = false)
@Test(priority=1)
static public void testValueClassHashCode() throws Throwable {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are there any tests for value classes with identity fields (here or in OpenJDK)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No, I don't think so. I will add another test similar to 15768. Will open another PR for that.
Verified 15768 with the current changes locally, and the behaviour matches the ri.

@@ -70,7 +70,6 @@ final class J9VMInternals {

/*[IF INLINE-TYPES]*/
static boolean positiveOnlyHashcodes = positiveOnlyHashcodes();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This field and method positiveOnlyHashcodes() can also be removed.

private:
protected:
public:
struct ValueTypeHashQueueEntry {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This struct and the new helper methods can be under private in this file.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I've encountered compilers that don't handle private types properly in all situations.

Comment thread runtime/oti/ObjectHash.hpp Outdated
* Use inlineObjectHashCode for reference fields.
* Use a queue to iterate through the fields of a valuetype.
*
* @param vm a java VM
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

currentThread is missing in this comment.

nit: spaces over tabs to separate the field descriptions.

Comment thread runtime/oti/ObjectHash.hpp Outdated
{
I_32 hashValue = 0;
J9Class *clazz = J9OBJECT_CLAZZ(currentThread, ((uintptr_t)objectPointer));
if ((NULL != clazz) && J9_IS_J9CLASS_VALUETYPE(clazz)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In what scenario could clazz be null?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

These were just some defensive checks which I added while debugging some crashes earlier. Will remove it.

Comment thread runtime/oti/ObjectHash.hpp Outdated
UDATA queueSize = initialQueueSize;
ValueTypeHashQueueEntry *queue = (ValueTypeHashQueueEntry *)j9mem_allocate_memory(sizeof(ValueTypeHashQueueEntry) * queueSize, OMRMEM_CATEGORY_VM);
if (NULL == queue) {
return 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Failure to allocate memory should result in an out of memory exception.

Comment thread runtime/oti/ObjectHash.hpp Outdated
ValueTypeHashQueueEntry entry = queue[head++];
J9ROMFieldOffsetWalkState state;
J9ROMFieldOffsetWalkResult *result = vm->internalVMFunctions->fieldOffsetsStartDo(vm, entry.clazz->romClass, VM_VMHelpers::getSuperclass(entry.clazz), &state, J9VM_FIELD_OFFSET_WALK_INCLUDE_INSTANCE, entry.clazz->flattenedClassCache);
while ((NULL != result) && (NULL != result->field)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why would result->field be null?

Copy link
Copy Markdown
Author

@AditiS11 AditiS11 Mar 17, 2026

Choose a reason for hiding this comment

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

fieldOffsetsNextDo returns a pointer to the result structure, but sets result->field to NULL to indicate the end of field traversal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see. result->field should be sufficient then, result will not be null.

#endif /* defined(J9VM_OPT_VALHALLA_FLATTENABLE_VALUE_TYPES) */
{
j9object_t fieldObject = objectAccessBarrier.inlineMixedObjectReadObject(currentThread, entry.objectPointer, fieldOffset);
if (NULL != fieldObject) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why would this be null?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fieldObject could be null if the reference fields are uninitialized.

Comment thread runtime/oti/ObjectHash.hpp Outdated
result = vm->internalVMFunctions->fieldOffsetsNextDo(&state);
}
}
const U_32 MUL1 = 0x85ebca6b;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you create a helper method to avoid this duplicated code between here and inlineConvertValueToHash ?

case 'L': {
U_32 datum = 0;
#if defined(J9VM_OPT_VALHALLA_FLATTENABLE_VALUE_TYPES)
if (vm->internalVMFunctions->isFlattenableFieldFlattened(entry.clazz, result->field)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be better to handle the L case as either value type fields or instance fields, not flattened fields and non-flattened fields. This will avoid unnecessary searches in the flattened class cache here and hopefully avoid some duplicated code.

Copy link
Copy Markdown
Author

@AditiS11 AditiS11 Mar 17, 2026

Choose a reason for hiding this comment

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

I tried restructuring the L case, but I got a segfault when reading the field as an object reference. Reading a flattened value field using inlineMixedObjectReadObject() results in an invalid object pointer.
I think we need to read the reference and use J9OBJECT_CLAZZ(currentThread, fieldObject) to determine whether it is a value type or a regular instance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh right. It's fine as is then.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You could check J9ROMFIELD_IS_NULL_RESTRICTED like in isSubstitutable.

@AditiS11 AditiS11 marked this pull request as draft March 16, 2026 12:06
@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch 3 times, most recently from 59a45e3 to 437659e Compare March 18, 2026 11:17
@AditiS11 AditiS11 marked this pull request as ready for review March 18, 2026 12:22
Copy link
Copy Markdown
Contributor

@theresa-m theresa-m left a comment

Choose a reason for hiding this comment

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

What is the behavior of the ri if hashCode is called prior to all strict instance fields being set in ? The easiest way to test this out is to use the @NullRestricted annotation which will mark the instance field as strict.

return EXECUTE_BYTECODE;
}

/* java.lang.J9VMInternals: positiveOnlyHashcodes() */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove extra line 3161.

Comment thread runtime/vm/BytecodeInterpreter.hpp Outdated
/* Caller has null-checked obj already */
_pc += 3;
*(I_32*)_sp = VM_ObjectHash::inlineObjectHashCode(_vm, obj);
I_32 hashValue = VM_ObjectHash::inlineObjectHashCode(_vm, obj);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since oom isn't a factor for non-valhalla builds I suggest keeping these additions under a build flag. Also in ObjectHash.hpp.

* Data members
*/
private:
struct ValueTypeHashQueueEntry {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This struct and most of the new helper methods should be under value type compiler flags.

Comment thread runtime/oti/ObjectHash.hpp Outdated
case 'S': { /* short */
U_32 datum = objectAccessBarrier.inlineMixedObjectReadU32(currentThread, entry.objectPointer, fieldOffset);
hashValue = mix(hashValue, datum);
numBytesHashed+=4;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

numBytesHashed += 4;

queue[tail].startOffset = fieldOffset;
tail++;
break;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should there be an else here?

Comment thread runtime/oti/ObjectHash.hpp Outdated
break;
}
default:
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should result in an error.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I initially thought of using Assert_VM_unreachable(), but I think it can't be used here.
Could you please suggest on what error to throw here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Use Assert_VM_unreachable() (or similar) seems reasonable; reaching line 324 means the signature is invalid - which should have been detected long before we get here.

Copy link
Copy Markdown
Contributor

@theresa-m theresa-m Mar 20, 2026

Choose a reason for hiding this comment

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

I think you can add a comment like https://github.com/eclipse-openj9/openj9/blob/master/runtime/oti/ObjectHash.hpp#L121 instead. edit: didn't see Keith's answer before posting this.

};
protected:
public:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This line does not need to be removed.

Comment thread runtime/gc_glue_java/ObjectModel.hpp Outdated
/**
* Determine the basic hash code for the specified object. This may modify the object. For example, it may
* set the HAS_BEEN_HASHED bit in the object's header. Object must not be NULL.
* Does not calculate hashcode for valuetypes. Returns 0.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please spell "hash code" consistently.
Why is zero the right answer for value types?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Followed this as per the discussion in #15768 (comment).
Valuetype hash calculation is not performed during GC as objects may still be moving.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's not how I read that comment. Zero might signal an object in transit, but when the hash code is computed later it need not be zero. Where is the code that computes that (real) hash code, if not this?

Comment thread runtime/gc_glue_java/ObjectModel.hpp Outdated
Comment on lines +380 to +383
#if defined(J9VM_OPT_VALHALLA_VALUE_TYPES)
if (isValueType) {
return 0;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this only for those two GC modes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is for all the GC modes. Sorry about this, I have changed it.

Comment thread runtime/gc_glue_java/ObjectModel.hpp Outdated
Comment on lines +413 to +418
#if defined(J9VM_OPT_VALHALLA_VALUE_TYPES)
J9Class *objectClass = J9GC_J9OBJECT_CLAZZ(objectPtr, this);
if (J9_IS_J9CLASS_VALUETYPE(objectClass)) {
*hashCodePointer = 0;
} else
#endif /* defined(J9VM_OPT_VALHALLA_VALUE_TYPES) */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Again, shouldn't this be independent of the GC policy?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The entire function body is under the if defined (OMR_GC_MODRON_COMPACTION) || defined (J9VM_GC_GENERATIONAL) flag. Hence I added my change following the same structure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just to clarify: this ifdef is an ugly check of condition "object can be moved by GC", so there are two cases obviously:

  • objects can be moved
  • objects always stay at the same place never change location.

private:
protected:
public:
struct ValueTypeHashQueueEntry {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I've encountered compilers that don't handle private types properly in all situations.

* @param[in] tail pointer to one index past the last active element
* @return pointer to resized queue, or NULL if allocation fails.
*/
static VMINLINE ValueTypeHashQueueEntry*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Space before *, please.

Comment thread runtime/oti/ObjectHash.hpp Outdated
Comment on lines +242 to +247
case 'Z': /* boolean */
case 'B': /* byte */
case 'C': /* char */
case 'I': /* int */
case 'F': /* float */
case 'S': { /* short */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Our coding standard says case labels should indented the same as switch.
Suggest 'S' and the comment should be closer:

				case 'S': /* short */ {

Comment thread runtime/oti/ObjectHash.hpp Outdated
{
PORT_ACCESS_FROM_JAVAVM(vm);
MM_ObjectAccessBarrierAPI objectAccessBarrier(currentThread);
I_32 hashValue = getSalt(vm, (UDATA) objectPointer);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please format casts consistently: others don't have the space.

Comment thread runtime/oti/ObjectHash.hpp Outdated
queue[tail].objectPointer = entry.objectPointer;
queue[tail].clazz = flatClazz;
queue[tail].startOffset = fieldOffset;
tail++;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use +=; here and line 315.

Comment thread runtime/oti/ObjectHash.hpp Outdated
result = vm->internalVMFunctions->fieldOffsetsNextDo(&state);
}
if ((NULL != queue) && (head < tail)) {
entry = queue[head++];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The increment should be separate:

				entry = queue[head];
				head += 1;

Comment thread runtime/oti/ObjectHash.hpp Outdated

hashValue = finalizeMurmur3Hash(hashValue, numBytesHashed);
done:
if (NULL!= queue) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing space before !=.

@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch from 437659e to c5f3c7e Compare March 20, 2026 18:20
@AditiS11
Copy link
Copy Markdown
Author

What is the behavior of the ri if hashCode is called prior to all strict instance fields being set in ? The easiest way to test this out is to use the @NullRestricted annotation which will mark the instance field as strict.

It throws a compiler error: reference to hashCode() may only appear after an explicit constructor invocation

@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch 2 times, most recently from 75c5db8 to 9aeaac9 Compare March 23, 2026 12:23
Copy link
Copy Markdown
Contributor

@theresa-m theresa-m left a comment

Choose a reason for hiding this comment

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

I suggest summarizing how hashcodes are updated for value types or link #15768 (comment) in the description so it is easy to find.

Comment thread runtime/vm/BytecodeInterpreter.hpp Outdated
/* Caller has null-checked obj already */
_pc += 3;
*(I_32*)_sp = VM_ObjectHash::inlineObjectHashCode(_vm, obj);
I_32 hashValue = VM_ObjectHash::inlineObjectHashCode(_vm, obj);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fields should be declared at the top of the method.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is C++ code; hashValue is a local which should be declared like this as it gets its first (better - only) value.

*
* @param vm a java VM
* @param objectPointer a valid object reference
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment should be updated to show that the oom could be returned.

Comment thread runtime/gc_glue_java/ObjectModel.hpp Outdated
} else
#endif /* defined(J9VM_OPT_VALHALLA_VALUE_TYPES) */
{
*hashCodePointer = convertValueToHash(vm, (uintptr_t)objectPtr);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please fix the indentation here (only tabs before *).

Comment thread runtime/oti/ObjectHash.hpp Outdated

while (true) {
J9ROMFieldOffsetWalkState state;
J9ROMFieldOffsetWalkResult *result = vm->internalVMFunctions->fieldOffsetsStartDo(vm, entry.clazz->romClass, VM_VMHelpers::getSuperclass(entry.clazz), &state, J9VM_FIELD_OFFSET_WALK_INCLUDE_INSTANCE, entry.clazz->flattenedClassCache);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This line is much too long:

			J9ROMFieldOffsetWalkResult *result = vm->internalVMFunctions->fieldOffsetsStartDo(
					vm, entry.clazz->romClass, VM_VMHelpers::getSuperclass(entry.clazz), &state,
					J9VM_FIELD_OFFSET_WALK_INCLUDE_INSTANCE, entry.clazz->flattenedClassCache);

Comment thread runtime/oti/ObjectHash.hpp Outdated
Comment on lines +305 to +308
if (NULL == queue) {
hashValue = -1;
goto done;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please correct the indentation here.

Comment thread runtime/oti/ObjectHash.hpp Outdated
break;
}
default:
/* Invalid field signature. Should assert but we are in util. */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please correct the indentation here.

Comment thread runtime/oti/ObjectHash.hpp Outdated
}
return hashValue;
}
#endif /* if defined(J9VM_OPT_VALHALLA_VALUE_TYPES) */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please remove "if" (only the condition should appear here).

Comment thread runtime/oti/ObjectHash.hpp Outdated
* @param objectPointer a valid valuetype object
* @param clazz class of the valuetye object
* @param startOffset offset to start reading fields from
* @return the calculated hash code or -1 if an OOM occurred
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

-1 is a perfectly reasonable hash value (you might expect that for an Integer with the value -1: some other mechanism is needed to distinguish OOM.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

In the current design, the hash slot stores 0 for value types until a hash is computed by the VM. Since 0 already represents the “hash not computed” case for value types, would it make sense to also return 0 from this function if an OOM occurs?

I think the alternative would be to add an flag (like oomOccurred parameter) to the function signature to check for this case, but that would require updating all the callers of this function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I only see one call to this function.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The OOM condition is currently handled in BytecodeInterpreter.hpp. When inlineObjectHashCode() returns -1, it is treated as an indication that an OOM occurred and the appropriate exception is set.

If we introduce an additional parameter such as oomOccurred to propagate this information, I think the function signatures would need to change, all calls of these functions should be updated as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the objection? We need function signatures that handle all reasonable scenarios.

Comment thread runtime/gc_glue_java/ObjectModel.hpp Outdated
if (J9_IS_J9CLASS_VALUETYPE(objectClass)) {
return 0;
}
#endif /* defined(J9VM_OPT_VALHALLA_VALUE_TYPES) */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is not good enough in the case you want to store hash code in VT object eventually.
If you look to the logic below, you can see:

  • If object has been moved (has hash slot inflated already and "has been moved" flag set) read hash code from the object directly, no calculation. For VT object it means read code from the object, check for 0 (has not been stored yet, this is first hash code request after move), calculate code and store it in the object. If hash is not 0 (has been stored already) use this code.
  • In the "else" case object has not been moved yet, so hash code should be calculated any way, but "has been hashed" flag must be set (atomically) in order to inflate hash slot for VT object when it is going to be moved. We are not going to be able to calculate hash code for VT object at that time and keep it zero, but this code can be updated next time moved VT object hash is requested.
    Current code does not set has been hashed" flag, so slot is not going to be inflated during move.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you for the explanation, I have updated the code to set the HAS_BEEN_HASHED flag for value types before returning 0.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assuming new code does what it is intended, it should be better structured. Current implementation looks ugly. My first thought is can VT check be integrated in convertValueToHash() and computeObjectAddressToHash() instead to be explicit at the caller (ex. adding extra parameter to control desired behaviour). If, by some reason it is inconvenient, we should create local wrapper functions in this class to hide the check.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also please point me where inflated hash slot for VT object is updated by VM? I am not objecting this design point but this function is a good candidate to do this.

Copy link
Copy Markdown
Author

@AditiS11 AditiS11 Mar 26, 2026

Choose a reason for hiding this comment

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

Also please point me where inflated hash slot for VT object is updated by VM? I am not objecting this design point but this function is a good candidate to do this.

https://github.com/eclipse-openj9/openj9/pull/23502/changes#diff-9aa50703bc7320e2410d7093b1f619ded056a3aeed5f75e2477603d7a5772712R543-R550

In inlineObjectHashCode() the VM first reads the value from the backfill offset. If the value is 0, it indicates that the hash has not yet been computed, so inlineConvertValueObjectToHash() is called to calculate the hash for valuetypes. The computed hash is then written back to the backfill slot.

When VM_VMHelpers::mustCallWriteAccessBarrier(vm) is true, the VM delegates to j9gc_objaccess_getObjectHashCode(). In the GC path, the function currently returns 0 for value types when the hash has not yet been calculated.

My understanding is that in this case the VM should detect the 0 value and then trigger the value type hash calculation, which is missing in the current implementation. Please correct me if I am misunderstanding this flow.

Also, since you mentioned that this function could be a good place to update the inflated hash slot for value types, I am not entirely sure how that would be done here. My understanding is that the value type hash calculation currently happens in the VM (inlineConvertValueObjectToHash()), while getObjectHashCode() is invoked through the GC access barrier path.

@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch from 111a450 to 01beea1 Compare March 24, 2026 12:37
#if defined(J9VM_OPT_VALHALLA_VALUE_TYPES)
J9Class *objectClass = J9GC_J9OBJECT_CLAZZ(objectPtr, this);
if (J9_IS_J9CLASS_VALUETYPE(objectClass)) {
*hashCodePointer = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This point needs extra clarification regarding VT objects, but general idea is:

  • There is "Pre-hash" optimization: customer can specify list of classes instances of which are going to be allocated with inflated and filled hash slot from the beginning.
  • This function is used to provide has slot initialization for this optimization and called from object allocation path.
  • I guess VT object hashing support might want to use this feature. So, I am not sure returning 0 is a right answer.
  • Yes, this hash slot can be updated later on using "update after VT object move" general logic, but I think real hash code can be calculated and stored right here.

@AditiS11 AditiS11 marked this pull request as draft March 25, 2026 11:37
@dmitripivkine
Copy link
Copy Markdown
Contributor

Sorry, I probably missed something, I was under impression that during VT hash code calculation if visited sub-object has hash code stored in the slot already, this code can be used instead of walking this object again. Is it right? If so, where it is implemented in the code? Just want to double check.

@AditiS11
Copy link
Copy Markdown
Author

Sorry, I probably missed something, I was under impression that during VT hash code calculation if visited sub-object has hash code stored in the slot already, this code can be used instead of walking this object again. Is it right? If so, where it is implemented in the code? Just want to double check.

https://github.com/eclipse-openj9/openj9/pull/23502/changes#diff-9aa50703bc7320e2410d7093b1f619ded056a3aeed5f75e2477603d7a5772712R269-R329
The current implementation relies on inlineObjectHashCode() for reference fields. This helper checks whether the object already has a hash stored in its hash slot and returns it if present; otherwise it computes and stores a new one.

For value types, the hash slot is not checked, and the current implementation walks through all the fields to compute the hash.

Aditi Srinivas M added 3 commits April 9, 2026 08:32
-Add native method to calculate hashcode for valuetypes.
-Modify GC path to return/store 0 for value types,
to eliminate hashcode calculation during object copying.
-Ensure hashcode calculation is only triggered by the VM
-Enable ValueTypeTests:testValueClassHashCode
Fixes: eclipse-openj9#15768

Signed-off-by: Aditi Srinivas M <Aditi.Srini@ibm.com>
-Add function to skip valuetype hash calculation in getObjectHashCode()
-Add the oomOccurred parameter to handle OOM.
-Modify getObjectHashCode() to udate the hash slot for valuetypes.
-Update initializeHashSlot() to calculate the actual hash for valuetypes.
@AditiS11 AditiS11 marked this pull request as draft April 9, 2026 16:07
@dmitripivkine
Copy link
Copy Markdown
Contributor

@AditiS11 @theresa-m
My concern is complicate logic structure hard to read and follow.
I am suggesting even before apply this PR logic completely eliminate computeObjectAddressToHash chain (including inlinedCompute... and use convertValueToHash() chain of functions instead. And do it in separate PR before add VT changes.
If we look close to computeObjectAddressToHash chain, it ends by calling inlineConvertValueToHash inside inlineComputeObjectAddressToHash. I think all this can be replaced by calling convertValueToHash() instead.
With this changed, logic of this PR became much simpler and cleaner.
If you like I can do this prerequisite change myself, please let me know if you prefer this.

@dmitripivkine
Copy link
Copy Markdown
Contributor

Sorry, my previous suggestion might be not so good as I thought. It is cleaning code and simplification indeed, but I have missed the fact that VT hash calculation requires object to work with. It means using "convert" path where object address is casted to the value upfront is not optimum direction to move.

@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch from 8d9b5a1 to d80a039 Compare April 13, 2026 14:17
-Handle OOM when objectHashCode() is called
-Add convertValueToHashForObjectWithForwardedHeader which accepts class as a parameter
@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch from d80a039 to fb1f64b Compare April 13, 2026 14:20
Comment thread runtime/gc_glue_java/ObjectModel.hpp Outdated
}

MMINLINE I_32
convertValueToHashForObject(J9JavaVM *vm, UDATA value, j9object_t object)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, I was not clear. There is no need to have value as parameter, use cast (uintptr_t)object instead. So, signature can be (J9JavaVM *vm, j9object_t object).

Comment thread runtime/gc_glue_java/ObjectModel.hpp Outdated
}

MMINLINE I_32
convertValueToHashForObjectWithForwardedHeader(J9JavaVM *vm, UDATA value, J9Class *clazz)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You can use the same convertValueToHashForObject name for function. Compiler can recognize different signature and take it.
If you want to mention Forwarded Header in the name, it can be convertValueToHashForForwardedHeader or convertValueToHashForObjectFromForwardedHeader

@AditiS11
Copy link
Copy Markdown
Author

AditiS11 commented Apr 14, 2026

I just had a thought about the current implementation.
Does it make sense to have a separate hash code path for valuetypes like the current implementation (valueHashCode) instead of modifying inlineObjectHashCode to handle both identity and value types.
I am not sure what implication it will have in the places where objectHashCode(which will handle only identity types) is called.
If we have a separate path for value bjects, we will not have to modify the definition of inlineObjectHashCode to handle OOM.
I initially stared off from a Draft PR and hence went ahead with the approach of modifying inlineObjectHashCode to handle both identity and value types.

Comment thread runtime/gc_glue_java/ObjectModel.hpp Outdated
return result;
}

MMINLINE I_32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This file uses OMR recommended (native) declaration types. int32_t should be used here and below instead of I_32.

Comment thread runtime/oti/ObjectHash.hpp Outdated
static VMINLINE I_32
inlineConvertValueObjectToHash(J9JavaVM *vm, J9VMThread *currentThread, j9object_t objectPointer, J9Class *clazz, bool *oomOccurred)
{
return convertValueObjectAtOffsetToHash(vm, currentThread, objectPointer, clazz, J9VMTHREAD_OBJECT_HEADER_SIZE(currentThread), oomOccurred);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason to have convertValueObjectAtOffsetToHash separated into a different function?

Copy link
Copy Markdown
Author

@AditiS11 AditiS11 Apr 15, 2026

Choose a reason for hiding this comment

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

Tried following the existing pattern.
For identity types inlineConvertValueToHash has the main logic and inlineComputeObjectAddressToHash is a wrapper around it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see. I think this makes some sense inlineComputeObjectAddressToHash since its used as an api for the gc but is not necessary for this function.

Comment thread runtime/oti/ObjectHash.hpp Outdated
* @return the calculated hash code or 0 if oom occurred
*/
static VMINLINE I_32
convertObjectToHash(J9JavaVM *vm, J9VMThread *currentThread, j9object_t objectPointer, bool *oomOccurred)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be more consistent to call this inlineConvertObjectToHash


#if defined(J9VM_OPT_VALHALLA_VALUE_TYPES)
I_32
convertValueTypeToHash(J9JavaVM *vm, J9VMThread *currentThread, j9object_t objectPointer, J9Class *clazz, jboolean *oomOccurred)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not just call convertObjectToHash ?

@hangshao0
Copy link
Copy Markdown
Contributor

I just had a thought about the current implementation.
Does it make sense to have a separate hash code path for valuetypes like the current implementation (valueHashCode) instead of modifying inlineObjectHashCode to handle both identity and value types.
I am not sure what implication it will have in the places where objectHashCode(which will handle only identity types) is called.
If we have a separate path for value bjects, we will not have to modify the definition of inlineObjectHashCode to handle OOM.
I initially stared off from a Draft PR and hence went ahead with the approach of modifying inlineObjectHashCode to handle both identity and value types.

If you mean sth like the following, it makes sense to me (only to deal with OOM for value type).

I_32
objectHashCode(J9JavaVM *vm, j9object_t objectPointer, BOOLEAN* OOMOccurred)
{
    if (objectPointer is value type) {
          /* Value type version to calculate hashcode which could be OOM. */
    } else 
         return VM_ObjectHash::inlineObjectHashCode(vm, objectPointer);
    }
}

@AditiS11 AditiS11 marked this pull request as ready for review April 16, 2026 15:23
Comment thread runtime/jcl/common/mgmtthread.c Outdated
info->lockedMonitors.arr_safe[i].clazz =
vmfns->j9jni_createLocalRef(env, J9VM_J9CLASS_TO_HEAPCLASS(J9OBJECT_CLAZZ((J9VMThread *)env, object)));
#if defined(J9VM_OPT_VALHALLA_VALUE_TYPES)
jboolean oomOccurred = JNI_FALSE;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this is c code new variables should be declared directly after the for loop at 2077.

Comment thread runtime/jcl/common/mgmtthread.c Outdated
info->lockedMonitors.arr_safe[i].identityHashCode = hashValue;
} else {
exc = J9VMCONSTANTPOOL_JAVALANGOUTOFMEMORYERROR;
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of breaking here this should be reorganized to fall through to 2100, releasing allocated memory.

Comment thread runtime/jvmti/jvmtiObject.c Outdated

obj = *((j9object_t *)object);
#if defined(J9VM_OPT_VALHALLA_VALUE_TYPES)
jboolean oomOccurred = JNI_FALSE;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

These should be initialized after line 88.

Fast_java_lang_J9VMInternals_identityHashCode(J9VMThread *currentThread, j9object_t objectPointer)
{
return VM_ObjectHash::inlineObjectHashCode(currentThread->javaVM, objectPointer);
I_32 hashValue = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: hashValue is only needed for the value type case, the non value type case can directly return the result.

Comment thread runtime/vm/montable.c Outdated
if (JNI_FALSE == oomOccurred) {
hash = (UDATA)(U_32)hashValue;
} else {
hash = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of returning 0 here which may make a class hard to find in the hash table I would suggest as assertion. Assert_VM_false(oomOccurred).

Comment thread runtime/util/ObjectHash.cpp Outdated
#if defined(J9VM_OPT_VALHALLA_VALUE_TYPES)
bool oomOccurredBool = false;
I_32 result = VM_ObjectHash::inlineObjectHashCode(vm, objectPointer, &oomOccurredBool);
if (NULL != oomOccurred) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are there any scenarios where oomOccured is expected to be null? If not this can be removed.

#else /* defined(J9VM_OPT_VALHALLA_VALUE_TYPES) */
I_32 result = VM_ObjectHash::inlineObjectHashCode(vm, objectPointer);
#endif /* defined(J9VM_OPT_VALHALLA_VALUE_TYPES) */
return result;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this compile? There are two return statements for the J9VM_OPT_VALHALLA_VALUE_TYPES case.

if (J9_ARE_ANY_BITS_SET(flags, OBJECT_HEADER_HAS_BEEN_MOVED_IN_CLASS)) {
UDATA hashSlotOffset = fieldClazz->backfillOffset;
I_32 storedHash = objectAccessBarrier.inlineMixedObjectReadI32(currentThread, fieldObject, hashSlotOffset);
if (0 != storedHash) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the stored hash is 0 shouldn't this object be added to the queue and is hash code calculated? I assume this would be a value type.

Comment thread runtime/oti/ObjectHash.hpp Outdated
break;
}
} else if (J9_IS_J9CLASS_VALUETYPE(fieldClazz)) {
if (NULL == queue) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would recommend splitting out the queue creation code into a separate method since this one is already quite long and this code is repeated.

U_32 datum = 0;
#if defined(J9VM_OPT_VALHALLA_FLATTENABLE_VALUE_TYPES)
if (J9ROMFIELD_IS_NULL_RESTRICTED(result->field)) {
if (vm->internalVMFunctions->isFlattenableFieldFlattened(entry.clazz, result->field)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like you are missing the case for a null-restricted field that is not flattened.

@AditiS11 AditiS11 marked this pull request as draft April 21, 2026 10:35
@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch 3 times, most recently from d754d28 to 2116a54 Compare April 22, 2026 13:27
- Overload the function convertValueToHashForObjecti() to take the
J9Class structure from forwarded header
-Remove inlineConvertValueObjectToHash()
@AditiS11 AditiS11 force-pushed the valuetypeHashCode branch from 2116a54 to 147d3ad Compare April 23, 2026 14:52
if (J9_IS_J9CLASS_VALUETYPE(clazz)) {
J9VMThread *currentThread = vm->internalVMFunctions->currentVMThread(vm);
jboolean oomOccurred = JNI_FALSE;
_gcEnv.movedObjectHashCodeCache.originalHashCode = convertValueTypeToHash(vm, currentThread, objectPtr, clazz, &oomOccurred);
Copy link
Copy Markdown
Contributor

@dmitripivkine dmitripivkine Apr 23, 2026

Choose a reason for hiding this comment

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

This code is much better now, and it is good enough.
However I wonder can jboolean oomOccurred notion to be excluded from GC code?
It is not belong here. And it is not used other than function can return 0 in the case of native OOM.
So, is it possible to create another wrapper function in VM hash code with (vm, currentThread, objectPtr, clazz) signature to hide OOM but return 0 instead?
I presume this oomOccurred parameter is necessary for some cases.

AditiS11 pushed a commit to AditiS11/openj9 that referenced this pull request Apr 23, 2026
-Add test to verify hash code behaviour on value types
with null-restricted fields. Handles both flattened
and non-flattened cases.
-Add test to verify hashcode behaviour on value types
with identity fields.
-Add test to verify reading hash code from object backfill
when objects are moved.
-Add test to validate value type hash code with null reference
fields.
Related: eclipse-openj9#23502

Signed-off-by: Aditi Srinivas M <Aditi.Srini@ibm.com>
AditiS11 pushed a commit to AditiS11/openj9 that referenced this pull request Apr 23, 2026
-Add test to verify hash code behaviour on value types
with null-restricted fields. Handles both flattened
and non-flattened cases.
-Add test to verify hashcode behaviour on value types
with identity fields.
-Add test to verify reading hash code from object backfill
when objects are moved.
-Add test to validate value type hash code with null reference
fields.
Related: eclipse-openj9#23502

Signed-off-by: Aditi Srinivas M <Aditi.Srini@ibm.com>
AditiS11 pushed a commit to AditiS11/openj9 that referenced this pull request Apr 23, 2026
- Add test for hash code on value types with null-restricted fields
covering flattened and non-flattened cases.
- Add test for hash code on value types with identity fields.
- Add test to validate reading hash code from object backfill after move.
- Add test for hash code on value types with null reference fields.
Related: eclipse-openj9#23502

Signed-off-by: Aditi Srinivas M <Aditi.Srini@ibm.com>
AditiS11 pushed a commit to AditiS11/openj9 that referenced this pull request Apr 23, 2026
- Add test for hash code on value types with null-restricted fields
covering flattened and non-flattened cases.
- Add test for hash code on value types with identity fields.
- Add test to validate reading hash code from object backfill after move.
- Add test for hash code on value types with null reference fields.
Related: eclipse-openj9#23502

Signed-off-by: Aditi Srinivas M <Aditi.Srini@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:vm project:valhalla Used to track Project Valhalla related work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create native function to compute hashcodes for value types

5 participants