Skip to content

AVRO-3527: Optimize GenericData.hashCode#1742

Merged
dkulp merged 7 commits intoapache:masterfrom
clesaec:avro-3527_OptimHashcode
Jan 4, 2023
Merged

AVRO-3527: Optimize GenericData.hashCode#1742
dkulp merged 7 commits intoapache:masterfrom
clesaec:avro-3527_OptimHashcode

Conversation

@clesaec
Copy link
Contributor

@clesaec clesaec commented Jun 29, 2022

AVRO-3527 : Small optimization on hashcode to avoid whole record exploration.

@github-actions github-actions bot added the Java Pull Requests for Java binding label Jun 29, 2022
@KalleOlaviNiemitalo
Copy link
Contributor

I see, you use AtomicInteger just as a mutable boxed integer, and don't actually care about its atomicity with regard to other threads.

@clesaec
Copy link
Contributor Author

clesaec commented Jun 29, 2022

I see, you use AtomicInteger just as a mutable boxed integer, and don't actually care about its atomicity with regard to other threads.

Yes

@RyanSkraba RyanSkraba changed the title Avro 3527 optim hashcode AVRO-3527: Optimize GenericData.hashCode Jun 29, 2022
@nandorKollar
Copy link
Contributor

What's the advantage of using an AtomicInteger instead of a simple Integer in this context? The counter isn't part of the shared mutable state of GenericData, could you please mention a scenario why AtomicInteger is better than Integer in this case?

@KalleOlaviNiemitalo
Copy link
Contributor

Integer is not mutable so you'd need an instance of another class (perhaps an one-element array) to hold the Integer reference.

If the atomicity is too much, then perhaps it's clearest to just define a non-atomic mutable state class with an int field here.

@nandorKollar
Copy link
Contributor

Integer is not mutable so you'd need an instance of another class (perhaps an one-element array) to hold the Integer reference.

If the atomicity is too much, then perhaps it's clearest to just define a non-atomic mutable state class with an int field here.

I see, thanks for the explanation. Looks like the point here is on the mutable nature of AtomicInteger, and not to support concurrency, I sounds a bit misleading to me. How about using simply the 'int' primitive type for the counter?

@KalleOlaviNiemitalo
Copy link
Contributor

The difficulty is in recursive calls

hashCode = hashCodeAdd(hashCode, getField(o, f.name(), f.pos()), f.schema(), counter);

hashCode = hashCodeAdd(hashCode, e, elementType, counter);

The limit is set to 10 hashes. Suppose you have a record type R1 with two properties, each of which has record type R2, which in turn has seven string properties. Now when you call hashCode on the R1 instance, it should hash all 7 strings of the first R2 instance but only the first 3 strings of the second R2 instance. So the recursive hashCode call on the first R2 instance must somehow let the caller know how many values it hashed. Passing an AtomicInteger as a parameter achieves that because hashCode can decrement the value of the AtomicInteger and this change is visible to the caller. If you passed an int counter parameter instead, then you'd have to return the counter change to the caller in some other way; perhaps by returning an instance of a class rather than just the hash code as int.

That said -- it might be advantageous to replace the AtomicInteger with a class that contains both the counter and the hash code being computed. The hashCodeAdd method effectively multiplies hash codes by powers of 31:

/** Add the hash code for an object into an accumulated hash code. */
protected int hashCodeAdd(int hashCode, Object o, Schema s, AtomicInteger counter) {
return 31 * hashCode + hashCode(o, s, counter);
}

In the scenario with record types R1 and R2, the hash codes of the strings in the first R2 instance get multiplied with 31 raised to the powers of 6, 5, 4, 3, 2, and 1. The hash codes of the first three strings in the second R2 instance get multiplied with 31 raised to the powers of 2, 1, and 0. So two of these multipliers get reused. If there instead were a static class HashCodeCollector that had one int field for the hash code and another int field for the counter, and all the recursive calls fed the values to that, then it would be able to give a separate multiplier to the hash code of each value.

@clesaec
Copy link
Contributor Author

clesaec commented Nov 14, 2022

Indeed, AtomicInteger, atomicity is may be too much, and an array of size one (int[]) may be to blurry, i will create a specific private class for that.
For optimization with calculating once each power of 31; it means transform

31*(31*(31*(31*hash(f1) + hash(f2)) + hash(f3)) + hash(f4))
31*(31*hash(f'1) + hash(f'2)) + hash(f'3)

into
31*(31*(31*(31*hash(f1) + hash(f2)) + hash(f3) + hash(f'1)) + hash(f4) + hash(f'2)) + hash(f'3)
But, as one field, as f2, can also be itself an Array of record containing itself fields ... , it will lead to too complex code for saving at most 10 multiplications.

@KalleOlaviNiemitalo
Copy link
Contributor

I meant using a separate power of 31 for each hashed value could make the hash codes more unique, without increasing the number of multiplications.

@clesaec
Copy link
Contributor Author

clesaec commented Nov 14, 2022

So, in the example
31*(31*(31*(31*(31*(31*hash(f1) + hash(f2)) + hash(f3)) + hash(f4)) + hash(f'1)) + hash(f'2)) + hash(f'3)
?
Indeed, that sounds logic.

@clesaec
Copy link
Contributor Author

clesaec commented Nov 14, 2022

So, i put hashcode calculation logic in an internal class, so counter became a simple int and 31* is apply to each step and code is easier to read.

@dkulp dkulp merged commit 1f7bfc4 into apache:master Jan 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Java Pull Requests for Java binding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants