RFR: 8221723: Avoid storing zero to String.hash
Peter Levart
peter.levart at gmail.com
Tue Apr 2 10:56:30 UTC 2019
Hi Claes,
I also think that the variant shown below should be compatible with
existing VM code that "injects" hash value. No changes necessary, right?
Peter
On 4/2/19 12:53 PM, Peter Levart wrote:
> Hi Claes,
>
> On 4/2/19 10:29 AM, Claes Redestad wrote:
>> Hi Peter,
>>
>> On 2019-04-01 23:54, Peter Levart wrote:
>>>
>>> In addition, this might not be easy from another point of view. You
>>> all know that there's no synchronization performed when caching the
>>> hash value. This works, because 32 bits are always read or written
>>> atomically. If there was another byte to be read or written together
>>> with 32 bit value, it would be tricky to get the ordering right and
>>> still keep the performance.
>>
>> I won't do this for the current RFE, but it's an interesting topic for
>> a future one. :-)
>
> Right. And after I posted this message yesterday, it occurred to me
> that it is possible to get away without fences if this additional
> boolean flag is not called "hashCalculated" but "hashIsZero". Like in
> the following example:
>
> private int hash;
> private boolean hashIsZero;
>
> public int hashCode() {
> int h = hash;
> if (h == 0 && !hashIsZero) {
> h = isLatin1() ? StringLatin1.hashCode(value)
> : StringUTF16.hashCode(value);
> if (h == 0) {
> hashIsZero = true;
> } else {
> hash = h;
> }
> }
> return h;
> }
>
> The trick here is that the logic writes to either 'hashIsZero' or to
> 'hash' field but never to both, so there's no ordering to be performed...
>
> Regards, Peter
>
>>
>> Disclaimer: As I'm trying to be clever about concurrency I'm likely
>> missing something...
>>
>> ... but I think a couple of barriers would be enough to ensure
>> sufficient visibility/atomicity for this case, since the only
>> catastrophic situation would be if another thread observes hash = 0 and
>> hashCalculated = true when the real hash value is != 0.
>>
>> private boolean hashCalculated;
>> private static Unsafe U = Unsafe.getUnsafe();
>>
>> public int hashCode() {
>> int h = hash;
>> if (h == 0 && !hashCalculated) {
>> if (!hashCalculated) {
>> hash = h = isLatin1() ? StringLatin1.hashCode(value)
>> : StringUTF16.hashCode(value);
>> // Ensure the store of the hashCalculated value is not
>> // reordered with the store of the hash value
>> U.storeStoreFence();
>> hashCalculated = true;
>> } else {
>> // We could have lost a race where we read hash = 0,
>> // then another thread stored hash and hashCalculated,
>> // then we read hashCalculated = true when the real hash
>> // value is not 0.
>> // A re-read behind a load fence should be sufficient
>> U.loadLoadFence();
>> h = hash;
>> }
>> }
>> return h;
>> }
>>
>> This is performance neutral on the fast-path (hash is calculated and
>> non-zero), and the extra fences cost ~1.2ns/op on my machine for cases
>> where the calculated hash code is 0, which is more or less the same
>> overhead as the extra branches we have now. The benefit of course is
>> that for non-empty Strings with hash 0 then repeated hashCode()
>> invocation is now as fast as "".hashCode()
>>
>> (One performance risk is that the added calls to U.*Fence will mess up
>> inlining heuristics.)
>>
>> Thanks!
>>
>> /Claes
>
More information about the core-libs-dev
mailing list