This is an automated email from the ASF dual-hosted git repository.
junegunn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hbase.git
The following commit(s) were added to refs/heads/master by this push:
new 244e022236f HBASE-29995 Improve existing hash implementations by
reading 4 bytes at once (#7934)
244e022236f is described below
commit 244e022236fbc54f3539d1c6325a1b2b0127723a
Author: Jinhyuk Kim <[email protected]>
AuthorDate: Thu Jun 11 12:05:06 2026 +0900
HBASE-29995 Improve existing hash implementations by reading 4 bytes at
once (#7934)
Signed-off-by: Junegunn Choi <[email protected]>
---
.../apache/hadoop/hbase/util/ByteArrayHashKey.java | 5 +
.../java/org/apache/hadoop/hbase/util/HashKey.java | 8 +
.../org/apache/hadoop/hbase/util/JenkinsHash.java | 15 +-
.../hadoop/hbase/util/LittleEndianBytes.java | 165 +++++++++++++++++++++
.../org/apache/hadoop/hbase/util/MurmurHash.java | 10 +-
.../org/apache/hadoop/hbase/util/MurmurHash3.java | 4 +-
.../apache/hadoop/hbase/util/RowBloomHashKey.java | 5 +
.../hadoop/hbase/util/RowColBloomHashKey.java | 159 +++++++++++++++-----
.../org/apache/hadoop/hbase/util/UnsafeAccess.java | 42 ++++++
.../hadoop/hbase/util/TestLittleEndianBytes.java} | 22 +--
.../hbase/util/TestLittleEndianBytesBase.java | 139 +++++++++++++++++
.../hbase/util/TestLittleEndianBytesWoUnsafe.java} | 41 +++--
.../hadoop/hbase/util/TestRowColBloomHashKey.java | 119 +++++++++++++++
13 files changed, 632 insertions(+), 102 deletions(-)
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteArrayHashKey.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteArrayHashKey.java
index 63ffabfa90f..19d56e7264b 100644
---
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteArrayHashKey.java
+++
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteArrayHashKey.java
@@ -44,4 +44,9 @@ public class ByteArrayHashKey extends HashKey<byte[]> {
public int length() {
return this.length;
}
+
+ @Override
+ public int getIntLE(int pos) {
+ return LittleEndianBytes.toInt(t, getAbsolutePos(pos));
+ }
}
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/HashKey.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/HashKey.java
index 9f12acb0b67..b9eb465a804 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/HashKey.java
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/HashKey.java
@@ -35,4 +35,12 @@ public abstract class HashKey<T> {
/** Returns The number of bytes in this HashKey */
public abstract int length();
+
+ /**
+ * Returns the little-endian 32-bit int value starting at the given position
in this
+ * {@code HashKey}.
+ * @param pos the starting offset of the 4-byte little-endian int
+ * @return the 32-bit value decoded in little-endian order
+ */
+ public abstract int getIntLE(int pos);
}
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/JenkinsHash.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/JenkinsHash.java
index ffe79698572..74cd053aef7 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/JenkinsHash.java
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/JenkinsHash.java
@@ -104,18 +104,9 @@ public class JenkinsHash extends Hash {
a = b = c = 0xdeadbeef + length + initval;
int offset = 0;
for (; length > 12; offset += 12, length -= 12) {
- a += (hashKey.get(offset) & BYTE_MASK);
- a += ((hashKey.get(offset + 1) & BYTE_MASK) << 8);
- a += ((hashKey.get(offset + 2) & BYTE_MASK) << 16);
- a += ((hashKey.get(offset + 3) & BYTE_MASK) << 24);
- b += (hashKey.get(offset + 4) & BYTE_MASK);
- b += ((hashKey.get(offset + 5) & BYTE_MASK) << 8);
- b += ((hashKey.get(offset + 6) & BYTE_MASK) << 16);
- b += ((hashKey.get(offset + 7) & BYTE_MASK) << 24);
- c += (hashKey.get(offset + 8) & BYTE_MASK);
- c += ((hashKey.get(offset + 9) & BYTE_MASK) << 8);
- c += ((hashKey.get(offset + 10) & BYTE_MASK) << 16);
- c += ((hashKey.get(offset + 11) & BYTE_MASK) << 24);
+ a += hashKey.getIntLE(offset);
+ b += hashKey.getIntLE(offset + 4);
+ c += hashKey.getIntLE(offset + 8);
/*
* mix -- mix 3 32-bit values reversibly. This is reversible, so any
information in (a,b,c)
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/LittleEndianBytes.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/LittleEndianBytes.java
new file mode 100644
index 00000000000..4b501ddc80b
--- /dev/null
+++
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/LittleEndianBytes.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.util;
+
+import java.nio.ByteBuffer;
+import org.apache.hadoop.hbase.ByteBufferExtendedCell;
+import org.apache.hadoop.hbase.Cell;
+import org.apache.hadoop.hbase.unsafe.HBasePlatformDependent;
+import org.apache.yetus.audience.InterfaceAudience;
+
+/**
+ * Utility methods for reading and writing little-endian integers from byte[]
and ByteBuffer. Used
+ * by hashing components to perform fast, low-level LE conversions with
optional Unsafe
+ * acceleration.
+ */
[email protected]
+public final class LittleEndianBytes {
+ final static boolean UNSAFE_UNALIGNED = HBasePlatformDependent.unaligned();
+
+ static abstract class Converter {
+ abstract int toInt(byte[] bytes, int offset);
+
+ abstract int toInt(ByteBuffer buffer, int offset);
+
+ abstract int putInt(byte[] bytes, int offset, int val);
+ }
+
+ static class ConverterHolder {
+ static final String UNSAFE_CONVERTER_NAME =
+ ConverterHolder.class.getName() + "$UnsafeConverter";
+ static final Converter BEST_CONVERTER = getBestConverter();
+
+ static Converter getBestConverter() {
+ try {
+ Class<? extends Converter> theClass =
+ Class.forName(UNSAFE_CONVERTER_NAME).asSubclass(Converter.class);
+ return theClass.getConstructor().newInstance();
+ } catch (Throwable t) {
+ return PureJavaConverter.INSTANCE;
+ }
+ }
+
+ static final class PureJavaConverter extends Converter {
+ static final PureJavaConverter INSTANCE = new PureJavaConverter();
+
+ private PureJavaConverter() {
+ }
+
+ @Override
+ int toInt(byte[] bytes, int offset) {
+ return (bytes[offset] & 0xFF) | ((bytes[offset + 1] & 0xFF) << 8)
+ | ((bytes[offset + 2] & 0xFF) << 16) | ((bytes[offset + 3] & 0xFF)
<< 24);
+ }
+
+ @Override
+ int toInt(ByteBuffer buffer, int offset) {
+ return (buffer.get(offset) & 0xFF) | ((buffer.get(offset + 1) & 0xFF)
<< 8)
+ | ((buffer.get(offset + 2) & 0xFF) << 16) | ((buffer.get(offset + 3)
& 0xFF) << 24);
+ }
+
+ @Override
+ int putInt(byte[] bytes, int offset, int val) {
+ bytes[offset] = (byte) val;
+ bytes[offset + 1] = (byte) (val >>> 8);
+ bytes[offset + 2] = (byte) (val >>> 16);
+ bytes[offset + 3] = (byte) (val >>> 24);
+ return offset + Bytes.SIZEOF_INT;
+ }
+ }
+
+ static final class UnsafeConverter extends Converter {
+ static final UnsafeConverter INSTANCE = new UnsafeConverter();
+
+ public UnsafeConverter() {
+ }
+
+ static {
+ if (!UNSAFE_UNALIGNED) {
+ throw new Error();
+ }
+ }
+
+ @Override
+ int toInt(byte[] bytes, int offset) {
+ return UnsafeAccess.toIntLE(bytes, offset);
+ }
+
+ @Override
+ int toInt(ByteBuffer buffer, int offset) {
+ return UnsafeAccess.toIntLE(buffer, offset);
+ }
+
+ @Override
+ int putInt(byte[] bytes, int offset, int val) {
+ return UnsafeAccess.putIntLE(bytes, offset, val);
+ }
+ }
+ }
+
+ /*
+ * Writes an int in little-endian order. Caller must ensure bounds; no
checks are performed.
+ */
+ public static void putInt(byte[] bytes, int offset, int val) {
+ assert offset >= 0 && bytes.length - offset >= Bytes.SIZEOF_INT;
+ ConverterHolder.BEST_CONVERTER.putInt(bytes, offset, val);
+ }
+
+ /*
+ * Reads an int in little-endian order. Caller must ensure bounds; no checks
are performed.
+ */
+ public static int toInt(byte[] bytes, int offset) {
+ assert offset >= 0 && bytes.length - offset >= Bytes.SIZEOF_INT;
+ return ConverterHolder.BEST_CONVERTER.toInt(bytes, offset);
+ }
+
+ /*
+ * Reads an int in little-endian order from ByteBuffer. Caller must ensure
bounds; no checks are
+ * performed.
+ */
+ public static int toInt(ByteBuffer buffer, int offset) {
+ assert offset >= 0 && buffer.capacity() - offset >= Bytes.SIZEOF_INT;
+ return ConverterHolder.BEST_CONVERTER.toInt(buffer, offset);
+ }
+
+ /*
+ * Reads an int in little-endian order from the row portion of the Cell, at
the given offset.
+ */
+ public static int getRowAsInt(Cell cell, int offset) {
+ if (cell instanceof ByteBufferExtendedCell) {
+ ByteBufferExtendedCell bbCell = (ByteBufferExtendedCell) cell;
+ return toInt(bbCell.getRowByteBuffer(), bbCell.getRowPosition() +
offset);
+ }
+ return toInt(cell.getRowArray(), cell.getRowOffset() + offset);
+ }
+
+ /*
+ * Reads an int in little-endian order from the qualifier portion of the
Cell, at the given
+ * offset.
+ */
+ public static int getQualifierAsInt(Cell cell, int offset) {
+ if (cell instanceof ByteBufferExtendedCell) {
+ ByteBufferExtendedCell bbCell = (ByteBufferExtendedCell) cell;
+ return toInt(bbCell.getQualifierByteBuffer(),
bbCell.getQualifierPosition() + offset);
+ }
+ return toInt(cell.getQualifierArray(), cell.getQualifierOffset() + offset);
+ }
+
+ private LittleEndianBytes() {
+ }
+}
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/MurmurHash.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/MurmurHash.java
index 0df3276c61a..c6b7de78f4b 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/MurmurHash.java
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/MurmurHash.java
@@ -47,15 +47,7 @@ public class MurmurHash extends Hash {
int len_4 = length >> 2;
for (int i = 0; i < len_4; i++) {
- int i_4 = (i << 2);
- int k = hashKey.get(i_4 + 3);
- k = k << 8;
- k = k | (hashKey.get(i_4 + 2) & 0xff);
- k = k << 8;
- k = k | (hashKey.get(i_4 + 1) & 0xff);
- k = k << 8;
- // noinspection PointlessArithmeticExpression
- k = k | (hashKey.get(i_4 + 0) & 0xff);
+ int k = hashKey.getIntLE(i << 2);
k *= m;
k ^= k >>> r;
k *= m;
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/MurmurHash3.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/MurmurHash3.java
index 115f8121fe8..20563edf143 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/MurmurHash3.java
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/MurmurHash3.java
@@ -48,9 +48,7 @@ public class MurmurHash3 extends Hash {
int roundedEnd = (length & 0xfffffffc); // round down to 4 byte block
for (int i = 0; i < roundedEnd; i += 4) {
- // little endian load order
- int k1 = (hashKey.get(i) & 0xff) | ((hashKey.get(i + 1) & 0xff) << 8)
- | ((hashKey.get(i + 2) & 0xff) << 16) | (hashKey.get(i + 3) << 24);
+ int k1 = hashKey.getIntLE(i);
k1 *= c1;
k1 = (k1 << 15) | (k1 >>> 17); // ROTL32(k1,15);
k1 *= c2;
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RowBloomHashKey.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RowBloomHashKey.java
index 10b9dd8ac0e..cbfab7f9c6e 100644
---
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RowBloomHashKey.java
+++
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RowBloomHashKey.java
@@ -37,4 +37,9 @@ public class RowBloomHashKey extends CellHashKey {
public int length() {
return this.t.getRowLength();
}
+
+ @Override
+ public int getIntLE(int offset) {
+ return LittleEndianBytes.getRowAsInt(t, offset);
+ }
}
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RowColBloomHashKey.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RowColBloomHashKey.java
index 68162f89e19..b034119d304 100644
---
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RowColBloomHashKey.java
+++
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RowColBloomHashKey.java
@@ -23,15 +23,15 @@ import org.apache.hadoop.hbase.PrivateCellUtil;
import org.apache.yetus.audience.InterfaceAudience;
/**
- * An hash key for ROWCOL bloom. This assumes the cells to be serialized in
the Keyvalue
+ * A hash key for ROWCOL bloom. This assumes the cells to be serialized in the
Keyvalue
* serialization format with Empty column family. Note that the byte
representing the family length
* is considered to be 0
*/
@InterfaceAudience.Private
public class RowColBloomHashKey extends CellHashKey {
-
private final int rowLength;
private final int qualLength;
+ private final int totalLength;
public RowColBloomHashKey(Cell cell) {
super(cell);
@@ -39,50 +39,133 @@ public class RowColBloomHashKey extends CellHashKey {
// We don't consider the family length for ROWCOL bloom. So subtract the
famLen from the
// length calculation. Timestamp and type are of no relevance here
qualLength = cell.getQualifierLength();
+ // ROWCOL Bloom byte layout:
+ // <2B RK length> <RK> <1B CF length> <CQ> <8B TS> <1B TYPE>
+ totalLength = KeyValue.ROW_LENGTH_SIZE + rowLength +
KeyValue.FAMILY_LENGTH_SIZE + qualLength
+ + KeyValue.TIMESTAMP_TYPE_SIZE;
}
@Override
public byte get(int offset) {
- // For ROW_COL blooms we use bytes
- // <RK length> (2 bytes) , <RK>, 0 (one byte CF length), <CQ>, <TS> (8
btes), <TYPE> ( 1 byte)
- if (offset < Bytes.SIZEOF_SHORT) {
- // assign locally
- int rowlen = rowLength;
- byte b = (byte) rowlen;
- if (offset == 0) {
- rowlen >>= 8;
- b = (byte) rowlen;
- }
- return b;
- }
- int refLen = Bytes.SIZEOF_SHORT + rowLength;
- if (offset < refLen) {
- return PrivateCellUtil.getRowByte(t, offset - Bytes.SIZEOF_SHORT);
- }
- if (offset == refLen) {
- // The fam length should return 0 assuming there is no column family.
- // Because for ROWCOL blooms family is not considered
- return 0;
+ return (byte) assembleCrossingLE(offset, Bytes.SIZEOF_BYTE);
+ }
+
+ @Override
+ public int length() {
+ return totalLength;
+ }
+
+ @Override
+ public int getIntLE(int offset) {
+ // Handle fast path that can return the row key as int directly
+ // Compute rowkey section range.
+ final int rowEnd = KeyValue.ROW_LENGTH_SIZE + rowLength;
+ if (offset >= KeyValue.ROW_LENGTH_SIZE && offset + Bytes.SIZEOF_INT <=
rowEnd) {
+ return LittleEndianBytes.getRowAsInt(t, offset -
KeyValue.ROW_LENGTH_SIZE);
}
- refLen += qualLength + Bytes.SIZEOF_BYTE;
- // skip the family len because actual cells may have family also
- if (offset < refLen) {
- return PrivateCellUtil.getQualifierByte(t,
- offset - (Bytes.SIZEOF_SHORT + rowLength + Bytes.SIZEOF_BYTE));
+
+ // Compute qualifier section range.
+ final int qualStart = rowEnd + KeyValue.FAMILY_LENGTH_SIZE;
+ final int qualEnd = qualStart + qualLength;
+ if (offset >= qualStart && offset + Bytes.SIZEOF_INT <= qualEnd) {
+ return LittleEndianBytes.getQualifierAsInt(t, offset - qualStart);
}
- // TODO : check if ts and type can be removed
- refLen += KeyValue.TIMESTAMP_SIZE;
- if (offset < refLen) {
- return LATEST_TS[offset - (Bytes.SIZEOF_SHORT + rowLength + qualLength +
Bytes.SIZEOF_BYTE)];
+
+ // Compute timestamp section range.
+ final int tsEnd = qualEnd + KeyValue.TIMESTAMP_SIZE;
+ if (offset >= qualEnd && offset + Bytes.SIZEOF_INT <= tsEnd) {
+ return LittleEndianBytes.toInt(LATEST_TS, offset - qualEnd);
}
- return MAX_TYPE;
+
+ return (int) assembleCrossingLE(offset, Bytes.SIZEOF_INT);
}
- @Override
- public int length() {
- // For ROW_COL blooms we use bytes
- // <RK length> (2 bytes) , <RK>, 0 (one byte CF length), <CQ>, <TS> (8
btes), <TYPE> ( 1 byte)
- return KeyValue.ROW_LENGTH_SIZE + this.t.getRowLength() +
KeyValue.FAMILY_LENGTH_SIZE
- + this.t.getQualifierLength() + KeyValue.TIMESTAMP_TYPE_SIZE;
+ private long assembleCrossingLE(int offset, int wordBytes) {
+ final int rowEnd = KeyValue.ROW_LENGTH_SIZE + rowLength;
+ final int qualStart = rowEnd + KeyValue.FAMILY_LENGTH_SIZE;
+ final int qualEnd = qualStart + qualLength;
+ final int tsEnd = qualEnd + KeyValue.TIMESTAMP_SIZE;
+
+ long result = 0L;
+ int pos = offset;
+ int remaining = wordBytes;
+
+ while (remaining > 0) {
+ // 1) row length field [0,2)
+ if (pos < KeyValue.ROW_LENGTH_SIZE) {
+ if (pos == 0 && remaining >= KeyValue.ROW_LENGTH_SIZE) {
+ result |= (rowLength >>> 8) & 0xFF;
+ result |= (rowLength & 0xFF) << 8;
+ pos += 2;
+ remaining -= 2;
+ } else if (pos == 0) {
+ result |= (rowLength >>> 8) & 0xFF;
+ pos += 1;
+ remaining -= 1;
+ } else {
+ result |= rowLength & 0xFF;
+ pos += 1;
+ remaining -= 1;
+ }
+ continue;
+ }
+
+ // 2) row bytes [2, rowEnd)
+ if (pos < rowEnd) {
+ final int take = Math.min(rowEnd - pos, remaining);
+ final int rOffset = pos - KeyValue.ROW_LENGTH_SIZE;
+ for (int i = 0; i < take; i++) {
+ final int shift = (wordBytes - remaining) * 8;
+ final byte b = PrivateCellUtil.getRowByte(t, rOffset + i);
+ result |= ((long) b & 0xFF) << shift;
+ remaining -= 1;
+ }
+ pos += take;
+ continue;
+ }
+
+ // 3) family length byte (always 0)
+ if (pos == rowEnd) {
+ pos += 1;
+ remaining -= 1;
+ continue;
+ }
+
+ // 4) qualifier bytes [qualStart, qualEnd)
+ if (pos < qualEnd) {
+ final int take = Math.min(qualEnd - pos, remaining);
+ final int qOffset = pos - qualStart;
+ for (int i = 0; i < take; i++) {
+ final int shift = (wordBytes - remaining) * 8;
+ final int b = PrivateCellUtil.getQualifierByte(t, qOffset + i) &
0xFF;
+ result |= ((long) b) << shift;
+ remaining -= 1;
+ }
+ pos += take;
+ continue;
+ }
+
+ // 5) timestamp bytes [qualEnd, tsEnd) -> LATEST_TS
+ if (pos < tsEnd) {
+ final int take = Math.min(tsEnd - pos, remaining);
+ final int tsOff = pos - qualEnd;
+ for (int i = 0; i < take; i++) {
+ final int shift = (wordBytes - remaining) * 8;
+ final int b = LATEST_TS[tsOff + i] & 0xFF;
+ result |= ((long) b) << shift;
+ remaining -= 1;
+ }
+ pos += take;
+ continue;
+ }
+
+ // 6) type byte at typePos -> MAX_TYPE
+ final int shift = (wordBytes - remaining) * 8;
+ result |= ((long) MAX_TYPE & 0xFF) << shift;
+ pos += 1;
+ remaining -= 1;
+ }
+
+ return result;
}
}
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/UnsafeAccess.java
b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/UnsafeAccess.java
index 3aa8a6ec123..83fcc3fc1c6 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/UnsafeAccess.java
+++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/UnsafeAccess.java
@@ -81,6 +81,21 @@ public final class UnsafeAccess {
}
}
+ /**
+ * Converts a byte array to an int value considering it was written in
little-endian format.
+ * @param bytes byte array
+ * @param offset offset into array
+ * @return the int value
+ */
+ public static int toIntLE(byte[] bytes, int offset) {
+ if (LITTLE_ENDIAN) {
+ return HBasePlatformDependent.getInt(bytes, offset +
BYTE_ARRAY_BASE_OFFSET);
+ } else {
+ return Integer
+ .reverseBytes(HBasePlatformDependent.getInt(bytes, offset +
BYTE_ARRAY_BASE_OFFSET));
+ }
+ }
+
/**
* Converts a byte array to a long value considering it was written in
big-endian format.
* @param bytes byte array
@@ -127,6 +142,21 @@ public final class UnsafeAccess {
return offset + Bytes.SIZEOF_INT;
}
+ /**
+ * Put an int value out to the specified byte array position in
little-endian format.
+ * @param bytes the byte array
+ * @param offset position in the array
+ * @param val int to write out
+ * @return incremented offset
+ */
+ public static int putIntLE(byte[] bytes, int offset, int val) {
+ if (!LITTLE_ENDIAN) {
+ val = Integer.reverseBytes(val);
+ }
+ HBasePlatformDependent.putInt(bytes, offset + BYTE_ARRAY_BASE_OFFSET, val);
+ return offset + Bytes.SIZEOF_INT;
+ }
+
/**
* Put a long value out to the specified byte array position in big-endian
format.
* @param bytes the byte array
@@ -191,6 +221,18 @@ public final class UnsafeAccess {
return getAsInt(buf, offset);
}
+ /**
+ * Reads an int value at the given buffer's offset considering it was
written in little-endian
+ * format.
+ * @return int value at offset
+ */
+ public static int toIntLE(ByteBuffer buf, int offset) {
+ if (LITTLE_ENDIAN) {
+ return getAsInt(buf, offset);
+ }
+ return Integer.reverseBytes(getAsInt(buf, offset));
+ }
+
/**
* Reads a int value at the given Object's offset considering it was written
in big-endian format.
* @return int value at offset
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/HashKey.java
b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytes.java
similarity index 65%
copy from hbase-common/src/main/java/org/apache/hadoop/hbase/util/HashKey.java
copy to
hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytes.java
index 9f12acb0b67..e1c9c4f5552 100644
--- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/HashKey.java
+++
b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytes.java
@@ -17,22 +17,12 @@
*/
package org.apache.hadoop.hbase.util;
-import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.jupiter.api.Tag;
-/**
- * Used to calculate the hash {@link Hash} algorithms for Bloomfilters.
- */
[email protected]
-public abstract class HashKey<T> {
- protected final T t;
-
- public HashKey(T t) {
- this.t = t;
- }
-
- /** Return The byte at the given position in this HashKey */
- public abstract byte get(int pos);
+@Tag(MiscTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestLittleEndianBytes extends TestLittleEndianBytesBase {
- /** Returns The number of bytes in this HashKey */
- public abstract int length();
}
diff --git
a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytesBase.java
b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytesBase.java
new file mode 100644
index 00000000000..6764dd3447a
--- /dev/null
+++
b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytesBase.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.ByteBuffer;
+import org.apache.hadoop.hbase.ByteBufferExtendedCell;
+import org.apache.hadoop.hbase.ByteBufferKeyValue;
+import org.apache.hadoop.hbase.Cell;
+import org.apache.hadoop.hbase.KeyValue;
+import org.junit.jupiter.api.Test;
+
+public abstract class TestLittleEndianBytesBase {
+
+ @Test
+ public void testToInt() {
+ byte[] b = generateByteArray(32);
+
+ for (int i = 0; i <= b.length - Integer.BYTES; i++) {
+ int expected = readIntLE(b, i);
+ assertEquals(expected, LittleEndianBytes.toInt(b, i));
+ }
+ }
+
+ @Test
+ public void testByteBufferToInt() {
+ byte[] b = generateByteArray(32);
+ ByteBuffer buf = ByteBuffer.wrap(b);
+
+ for (int i = 0; i <= b.length - Integer.BYTES; i++) {
+ int expected = readIntLE(b, i);
+ assertEquals(expected, LittleEndianBytes.toInt(buf, i));
+ }
+ }
+
+ @Test
+ public void testPutInt() {
+ byte[] b = new byte[16];
+
+ int offset = 5;
+ int value = 0x12345678;
+ LittleEndianBytes.putInt(b, offset, value);
+ int expected = readIntLE(b, offset);
+ assertEquals(value, expected);
+
+ offset += Integer.BYTES;
+ value = 0x9ABCDEF0;
+ LittleEndianBytes.putInt(b, offset, value);
+ expected = readIntLE(b, offset);
+ assertEquals(value, expected);
+ }
+
+ @Test
+ public void testGetRowAsIntFromByteBufferExtendedCell() {
+ Cell bbCell = createByteBufferExtendedCell();
+ byte[] row = bbCell.getRowArray();
+
+ for (int i = bbCell.getRowOffset(); i <= bbCell.getRowLength() -
Integer.BYTES; i++) {
+ int expected = readIntLE(row, i);
+ assertEquals(expected, LittleEndianBytes.getRowAsInt(bbCell, i));
+ }
+ }
+
+ @Test
+ public void testGetRowAsIntFromCell() {
+ KeyValue cell = createCell();
+ byte[] row = cell.getRowArray();
+
+ for (int i = 0; i <= cell.getRowLength() - Integer.BYTES; i++) {
+ int expected = readIntLE(row, cell.getRowOffset() + i);
+ assertEquals(expected, LittleEndianBytes.getRowAsInt(cell, i));
+ }
+ }
+
+ @Test
+ public void testGetQualifierAsIntFromByteBufferExtendedCell() {
+ Cell bbCell = createByteBufferExtendedCell();
+ byte[] qual = bbCell.getQualifierArray();
+
+ for (int i = 0; i <= bbCell.getQualifierLength() - Integer.BYTES; i++) {
+ int expected = readIntLE(qual, bbCell.getQualifierOffset() + i);
+ assertEquals(expected, LittleEndianBytes.getQualifierAsInt(bbCell, i));
+ }
+ }
+
+ @Test
+ public void testGetQualifierAsIntFromCell() {
+ KeyValue cell = createCell();
+ byte[] qual = cell.getQualifierArray();
+
+ for (int i = 0; i <= cell.getQualifierLength() - Integer.BYTES; i++) {
+ int expected = readIntLE(qual, cell.getQualifierOffset() + i);
+ assertEquals(expected, LittleEndianBytes.getQualifierAsInt(cell, i));
+ }
+ }
+
+ private static KeyValue createCell() {
+ byte[] row = Bytes.toBytes("row_key_for_test_12345");
+ byte[] family = Bytes.toBytes("f");
+ byte[] qualifier = Bytes.toBytes("qualifier_12345");
+ byte[] value = Bytes.toBytes(123456789);
+ return new KeyValue(row, family, qualifier, value);
+ }
+
+ private static ByteBufferExtendedCell createByteBufferExtendedCell() {
+ KeyValue kv = createCell();
+ ByteBuffer buffer = ByteBuffer.wrap(kv.getBuffer());
+ return new ByteBufferKeyValue(buffer, 0, buffer.remaining());
+ }
+
+ private static byte[] generateByteArray(int size) {
+ byte[] b = new byte[size];
+ for (int i = 0; i < b.length; i++) {
+ b[i] = (byte) (i * 3 + 7);
+ }
+ return b;
+ }
+
+ private static int readIntLE(byte[] b, int off) {
+ return (b[off] & 0xFF) | ((b[off + 1] & 0xFF) << 8) | ((b[off + 2] & 0xFF)
<< 16)
+ | ((b[off + 3] & 0xFF) << 24);
+ }
+}
diff --git
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteArrayHashKey.java
b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytesWoUnsafe.java
similarity index 51%
copy from
hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteArrayHashKey.java
copy to
hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytesWoUnsafe.java
index 63ffabfa90f..dc82e4a4087 100644
---
a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteArrayHashKey.java
+++
b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestLittleEndianBytesWoUnsafe.java
@@ -17,31 +17,24 @@
*/
package org.apache.hadoop.hbase.util;
-import org.apache.yetus.audience.InterfaceAudience;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.Mockito.mockStatic;
[email protected]
-public class ByteArrayHashKey extends HashKey<byte[]> {
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.hadoop.hbase.unsafe.HBasePlatformDependent;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.mockito.MockedStatic;
- private final int offset;
- private final int length;
-
- public ByteArrayHashKey(byte[] t, int offset, int length) {
- super(t);
- this.offset = offset;
- this.length = length;
- }
-
- @Override
- public byte get(int pos) {
- return t[getAbsolutePos(pos)];
- }
-
- private int getAbsolutePos(int pos) {
- return this.offset + pos;
- }
-
- @Override
- public int length() {
- return this.length;
+@Tag(MiscTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestLittleEndianBytesWoUnsafe extends TestLittleEndianBytesBase {
+ @BeforeAll
+ public static void disableUnsafe() {
+ try (MockedStatic<HBasePlatformDependent> mocked =
mockStatic(HBasePlatformDependent.class)) {
+ mocked.when(HBasePlatformDependent::unaligned).thenReturn(false);
+ assertFalse(LittleEndianBytes.UNSAFE_UNALIGNED);
+ }
}
}
diff --git
a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestRowColBloomHashKey.java
b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestRowColBloomHashKey.java
new file mode 100644
index 00000000000..e2621cdb259
--- /dev/null
+++
b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestRowColBloomHashKey.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.hbase.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.hadoop.hbase.HConstants;
+import org.apache.hadoop.hbase.KeyValue;
+import org.apache.hadoop.hbase.PrivateCellUtil;
+import org.apache.hadoop.hbase.testclassification.MiscTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+@Tag(MiscTests.TAG)
+@Tag(SmallTests.TAG)
+public class TestRowColBloomHashKey {
+
+ private KeyValue kv;
+ private RowColBloomHashKey hashKey;
+
+ @BeforeEach
+ public void setup() {
+ byte[] row = Bytes.toBytes("row_key_test");
+ byte[] family = Bytes.toBytes("family");
+ byte[] qualifier = Bytes.toBytes("qualifier");
+ byte[] value = Bytes.toBytes("1234567890");
+ kv = new KeyValue(row, family, qualifier, value);
+ hashKey = new RowColBloomHashKey(kv);
+ }
+
+ @Test
+ public void testGet() {
+ final int rowLen = kv.getRowLength();
+ final int qualLen = kv.getQualifierLength();
+
+ // Expected virtual layout:
+ // [rowLen(2 bytes)][row bytes][famLen(1 byte, always 0)][qualifier bytes]
+ // [timestamp(8 bytes, HConstants.LATEST_TIMESTAMP)][type(1 byte,
KeyValue.Type.Maximum)]
+ final int expectedLength = KeyValue.ROW_LENGTH_SIZE + rowLen +
KeyValue.FAMILY_LENGTH_SIZE
+ + qualLen + KeyValue.TIMESTAMP_TYPE_SIZE;
+ assertEquals(expectedLength, hashKey.length());
+
+ int offset = 0;
+
+ // 1) Row length field: MSB then LSB
+ int msb = hashKey.get(offset++) & 0xFF;
+ int lsb = hashKey.get(offset++) & 0xFF;
+ int decodedRowLen = (msb << 8) | lsb;
+ assertEquals(rowLen, decodedRowLen);
+
+ // 2) Row bytes
+ for (int i = 0; i < rowLen; i++) {
+ int expected = PrivateCellUtil.getRowByte(kv, i) & 0xFF;
+ int actual = hashKey.get(offset++) & 0xFF;
+ assertEquals(expected, actual, "row byte mismatch at i=" + i);
+ }
+
+ // 3) Family length byte
+ assertEquals(0, hashKey.get(offset++) & 0xFF);
+
+ // 4) Qualifier bytes
+ for (int i = 0; i < qualLen; i++) {
+ int expected = PrivateCellUtil.getQualifierByte(kv, i) & 0xFF;
+ int actual = hashKey.get(offset++) & 0xFF;
+ assertEquals(expected, actual, "qualifier byte mismatch at i=" + i);
+ }
+
+ // 5) Timestamp bytes: should match HConstants.LATEST_TIMESTAMP in
big-endian
+ // RowColBloomHashKey uses LATEST_TS byte[] from CellHashKey which
corresponds to latest
+ // timestamp.
+ long ts = HConstants.LATEST_TIMESTAMP;
+ for (int i = 0; i < KeyValue.TIMESTAMP_SIZE; i++) {
+ // KeyValue timestamp serialization is big-endian
+ int expected = (int) ((ts >>> (8 * (KeyValue.TIMESTAMP_SIZE - 1 - i))) &
0xFF);
+ int actual = hashKey.get(offset++) & 0xFF;
+ assertEquals(expected, actual, "timestamp byte mismatch at i=" + i);
+ }
+
+ // 6) Type byte: should be Maximum
+ assertEquals(KeyValue.Type.Maximum.getCode(), hashKey.get(offset++));
+
+ // consumed exactly all bytes
+ assertEquals(hashKey.length(), offset);
+ }
+
+ @Test
+ public void testGetIntLE() {
+ for (int i = 0; i <= hashKey.length() - Bytes.SIZEOF_INT; i++) {
+ int expected = expectedIntLEFromGet(i);
+ int actual = hashKey.getIntLE(i);
+ assertEquals(expected, actual, "sequential mismatch at offset=" + i);
+ }
+ }
+
+ private int expectedIntLEFromGet(int offset) {
+ int b0 = hashKey.get(offset) & 0xFF;
+ int b1 = hashKey.get(offset + 1) & 0xFF;
+ int b2 = hashKey.get(offset + 2) & 0xFF;
+ int b3 = hashKey.get(offset + 3) & 0xFF;
+ return (b0) | (b1 << 8) | (b2 << 16) | (b3 << 24);
+ }
+}