Skip to content

[fix][misc] Fix bugs in hash bucket algorithm implementation in Pulsar collections#21107

Open
lhotari wants to merge 2 commits intoapache:masterfrom
lhotari:lh-fix-bucket-index
Open

[fix][misc] Fix bugs in hash bucket algorithm implementation in Pulsar collections#21107
lhotari wants to merge 2 commits intoapache:masterfrom
lhotari:lh-fix-bucket-index

Conversation

@lhotari
Copy link
Member

@lhotari lhotari commented Sep 1, 2023

Fixes #21106
Fixes #21108

Motivation

  • Fix a bug in ConcurrentLongLongPairHashMap, ConcurrentLongPairSet and ConcurrentOpenHashMap where the hash bucket's storage index was incorrectly calculated.
    • the impact of the bug has been that the implementations have been rather unoptimal because of of frequent collisions in storing the item to the underlying array. Essentially the hash bucket algorithm hasn't been properly used.
  • Fix a bug in signSafeMod function which is duplicated in all classes. The buggy implementation didn't return correct results for negative input values.

Modifications

  • rename bucket to bucketIndex to indicate that it's the bucket index in the storage array that needs to be calculated.
  • calculate bucketIndex by multiplying the hash bucket by the item size (4 or 2).
  • fix the signSafeMod function which wasn't sign safe at all. Currently the logic is duplicated in all classes.

Documentation

  • doc
  • doc-required
  • doc-not-needed
  • doc-complete

…multiple elements of array

Fixes apache#21106

- Fix a bug in ConcurrentLongLongPairHashMap, ConcurrentLongPairSet and ConcurrentOpenHashMap where
  the hash bucket's storage index was incorrectly calculated.
  - the impact of the bug has been that the implementations have been rather unoptimal because of of frequent
    collisions in storing the item to the underlying array. Essentially the hash bucket algorithm hasn't been
    properly used.
@lhotari lhotari added the type/bug The PR fixed a bug or issue reported a bug label Sep 1, 2023
@lhotari lhotari added this to the 3.2.0 milestone Sep 1, 2023
@lhotari lhotari self-assigned this Sep 1, 2023
@github-actions github-actions bot added the doc-not-needed Your PR changes do not impact docs label Sep 1, 2023
@lhotari lhotari changed the title [fix][common] Fix bug in hash bucket algorithm implementation with Long pair keys [fix][misc] Fix bug in hash bucket algorithm implementation with Long pair keys in Pulsar collections Sep 1, 2023
@lhotari
Copy link
Member Author

lhotari commented Sep 1, 2023

The signSafeMod bug must have impacted also all other collection classes where the invalid implementation was used.

@lhotari lhotari changed the title [fix][misc] Fix bug in hash bucket algorithm implementation with Long pair keys in Pulsar collections [fix][misc] Fix bugs in hash bucket algorithm implementation in Pulsar collections Sep 1, 2023
Comment on lines +638 to +640
if (mod < 0) {
mod += divisor;
}
Copy link
Member

Choose a reason for hiding this comment

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

As max-1 > 0, we won't get a negative result with (int) n & (max - 1) even if n is negative. This is unnecessary.

static final int signSafeMod(long n, int max) {
return (int) n & (max - 1);
static final int signSafeMod(long dividend, int divisor) {
int mod = (int) (dividend % divisor);
Copy link
Member

Choose a reason for hiding this comment

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

Using & is faster than the %, which is a trick to speed up.

Comment on lines -325 to +327
int bucket = signSafeMod(keyHash, capacity);
int bucketIndex = signSafeMod(keyHash, capacity) * ITEM_SIZE;
Copy link
Member

@thetumbled thetumbled Sep 1, 2023

Choose a reason for hiding this comment

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

* 4 is equivalent to << 2, moving this logic out of signSafeMod method seems meaningless?

Copy link
Member Author

@lhotari lhotari Sep 1, 2023

Choose a reason for hiding this comment

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

The problem with the old code is that it is cryptic. In the original code, signSafeMod function's name is misleading. In the cases where the item takes 2 or 4 elements, there's a bitwise shift to left to multiply by 2 (<< 1) or 4 (<< 2), directly in the signSafeMod function. It's bad from maintainability perspective to have functions with names that don't match the implementation. I doubt that using bitwise operations to optimize the speed of execution is worthwhile in Java code. It should be left to the compiler to do such optimizations.

Copy link
Member

Choose a reason for hiding this comment

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

Change method name signSafeMod to hash may be better, right?
And i wonder whether the compiler will do such optimizations, could you verify this? thanks.
IMO, these util class adopt some kind of ideas from common util class like HashMap, which use & to avoid % too.

Copy link
Contributor

Choose a reason for hiding this comment

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

The modern Java compilers are smart enough to recognize when multiplication is equivalent to a bit shift and can optimize the multiplication operation to use a bit shift instead. It's better to focus on writing clear, readable code.

Comment on lines +601 to +608
static final int signSafeMod(long dividend, int divisor) {
int mod = (int) (dividend % divisor);

if (mod < 0) {
mod += divisor;
}

return mod;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we move to a Utility Class for computing the sign safe mod?
We have a similar Unitity Class in pulsar-client, not pulsar-client-api.
Is it better to move it to pulsar-common to reduce the duplicated code?

Copy link
Contributor

Choose a reason for hiding this comment

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

@lhotari And, could you please provide more context about

the impact of the bug has been that the implementations have been rather unoptimal because of of frequent collisions in storing the item to the underlying array. Essentially the hash bucket algorithm hasn't been properly used.

The HashMap in JDK is also used the same way to calculate the table index. Why % is better than & in this case to avoid the frequent collisions?

Here is the code snippet in JDK 17:

        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

@codelipenghui
Copy link
Contributor

I also left a comment on another related PR. Maybe we should consider to replace the customized map with ConcurrentHashMap.

@github-actions
Copy link

github-actions bot commented Oct 6, 2023

The pr had no activity for 30 days, mark with Stale label.

@github-actions github-actions bot added the Stale label Oct 6, 2023
@Technoboy- Technoboy- modified the milestones: 3.2.0, 3.3.0 Dec 22, 2023
@coderzc coderzc removed this from the 3.3.0 milestone May 8, 2024
@coderzc coderzc added this to the 3.4.0 milestone May 8, 2024
@lhotari lhotari modified the milestones: 4.0.0, 4.1.0 Oct 11, 2024
@lhotari lhotari removed this from the 4.1.0 milestone Jan 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-not-needed Your PR changes do not impact docs ready-to-test Stale type/bug The PR fixed a bug or issue reported a bug

Projects

None yet

5 participants