Improve performance of AsciiString.equals(Object).

Motivation:

The current implementation does byte by byte comparison, which we have seen
can be a performance bottleneck when the AsciiString is used as the key in
a Map.

Modifications:

Use sun.misc.Unsafe (on supporting platforms) to compare up to eight bytes at a time
and get closer to the performance of String.equals(Object).

Result:

Significant improvement (2x - 6x) in performance over the current implementation.

Benchmark                                             (size)   Mode   Samples        Score  Score error    Units
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual       10  thrpt        10 118843477.518 2347259.347    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual       50  thrpt        10 43910319.773   198376.996    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual      100  thrpt        10 26339969.001   159599.252    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual     1000  thrpt        10  2873119.030    20779.056    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual    10000  thrpt        10   306370.450     1933.303    ops/s
i.n.m.i.PlatformDependentBenchmark.arraysBytesEqual   100000  thrpt        10    25750.415      108.391    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual       10  thrpt        10 248077563.510  635320.093    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual       50  thrpt        10 128198943.138  614827.548    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual      100  thrpt        10 86195621.349  1063959.307    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual     1000  thrpt        10 16920264.598    61615.365    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual    10000  thrpt        10  1687454.747     6367.602    ops/s
i.n.m.i.PlatformDependentBenchmark.unsafeBytesEqual   100000  thrpt        10   153717.851      586.916    ops/s
This commit is contained in:
Jakob Buchgraber 2015-04-13 09:25:27 -07:00 committed by Scott Mitchell
parent 221a9f50d4
commit c2de195f87
5 changed files with 215 additions and 26 deletions

View File

@ -15,6 +15,8 @@
package io.netty.util;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import java.nio.ByteBuffer;
@ -600,24 +602,12 @@ public class ByteString {
if (!(obj instanceof ByteString)) {
return false;
}
if (this == obj) {
return true;
}
ByteString that = (ByteString) obj;
if (length() != that.length() || hashCode() != that.hashCode()) {
return false;
}
final int end = value.length;
for (int i = 0, j = 0; i < end; i++, j++) {
if (value[i] != that.value[j]) {
return false;
}
}
return true;
ByteString other = (ByteString) obj;
return hashCode() == other.hashCode() &&
PlatformDependent.equals(array(), 0, array().length, other.array(), 0, other.array().length);
}
/**

View File

@ -42,7 +42,6 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility that detects various properties specific to the current runtime
* environment, such as Java version and the availability of the
@ -72,7 +71,7 @@ public final class PlatformDependent {
HAS_UNSAFE && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);
private static final long MAX_DIRECT_MEMORY = maxDirectMemory0();
private static final long ARRAY_BASE_OFFSET = arrayBaseOffset0();
private static final long ARRAY_BASE_OFFSET = PlatformDependent0.arrayBaseOffset();
private static final boolean HAS_JAVASSIST = hasJavassist0();
@ -350,6 +349,24 @@ public final class PlatformDependent {
PlatformDependent0.copyMemory(null, srcAddr, dst, ARRAY_BASE_OFFSET + dstIndex, length);
}
/**
* Compare two {@code byte} arrays for equality. For performance reasons no bounds checking on the
* parameters is performed.
*
* @param bytes1 the first byte array.
* @param startPos1 the position (inclusive) to start comparing in {@code bytes1}.
* @param endPos1 the position (exclusive) to stop comparing in {@code bytes1}.
* @param bytes2 the second byte array.
* @param startPos2 the position (inclusive) to start comparing in {@code bytes2}.
* @param endPos2 the position (exclusive) to stop comparing in {@code bytes2}.
*/
public static boolean equals(byte[] bytes1, int startPos1, int endPos1, byte[] bytes2, int startPos2, int endPos2) {
if (!hasUnsafe() || !PlatformDependent0.unalignedAccess()) {
return safeEquals(bytes1, startPos1, endPos1, bytes2, startPos2, endPos2);
}
return PlatformDependent0.equals(bytes1, startPos1, endPos1, bytes2, startPos2, endPos2);
}
/**
* Create a new optimized {@link AtomicReferenceFieldUpdater} or {@code null} if it
* could not be created. Because of this the caller need to check for {@code null} and if {@code null} is returned
@ -622,14 +639,6 @@ public final class PlatformDependent {
}
}
private static long arrayBaseOffset0() {
if (!hasUnsafe()) {
return -1;
}
return PlatformDependent0.arrayBaseOffset();
}
private static long maxDirectMemory0() {
long maxDirectMemory = 0;
try {
@ -847,6 +856,21 @@ public final class PlatformDependent {
return PlatformDependent0.addressSize();
}
private static boolean safeEquals(byte[] bytes1, int startPos1, int endPos1,
byte[] bytes2, int startPos2, int endPos2) {
final int len1 = endPos1 - startPos1;
final int len2 = endPos2 - startPos2;
if (len1 != len2) {
return false;
}
for (int i = 0; i < len1; i++) {
if (bytes1[startPos1 + i] != bytes2[startPos2 + i]) {
return false;
}
}
return true;
}
private PlatformDependent() {
// only static method supported
}

View File

@ -39,6 +39,7 @@ final class PlatformDependent0 {
private static final Unsafe UNSAFE;
private static final boolean BIG_ENDIAN = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN;
private static final long ADDRESS_FIELD_OFFSET;
private static final long ARRAY_BASE_OFFSET;
/**
* Limits the number of bytes to copy per {@link Unsafe#copyMemory(long, long, long)} to allow safepoint polling
@ -112,9 +113,11 @@ final class PlatformDependent0 {
if (unsafe == null) {
ADDRESS_FIELD_OFFSET = -1;
ARRAY_BASE_OFFSET = -1;
UNALIGNED = false;
} else {
ADDRESS_FIELD_OFFSET = objectFieldOffset(addressField);
ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
boolean unaligned;
try {
Class<?> bitsClass = Class.forName("java.nio.Bits", false, ClassLoader.getSystemClassLoader());
@ -137,6 +140,10 @@ final class PlatformDependent0 {
return UNSAFE != null;
}
static boolean unalignedAccess() {
return UNALIGNED;
}
static void throwException(Throwable t) {
UNSAFE.throwException(t);
}
@ -152,7 +159,7 @@ final class PlatformDependent0 {
}
static long arrayBaseOffset() {
return UNSAFE.arrayBaseOffset(byte[].class);
return ARRAY_BASE_OFFSET;
}
static Object getObject(Object object, long fieldOffset) {
@ -311,6 +318,37 @@ final class PlatformDependent0 {
}
}
static boolean equals(byte[] bytes1, int startPos1, int endPos1, byte[] bytes2, int startPos2, int endPos2) {
final int len1 = endPos1 - startPos1;
final int len2 = endPos2 - startPos2;
if (len1 != len2) {
return false;
}
if (len1 == 0) {
return true;
}
final long baseOffset1 = ARRAY_BASE_OFFSET + startPos1;
final long baseOffset2 = ARRAY_BASE_OFFSET + startPos2;
int remainingBytes = len1 & 7;
for (int i = len1 - 8; i >= remainingBytes; i -= 8) {
if (UNSAFE.getLong(bytes1, baseOffset1 + i) != UNSAFE.getLong(bytes2, baseOffset2 + i)) {
return false;
}
}
if (remainingBytes >= 4) {
remainingBytes -= 4;
if (UNSAFE.getInt(bytes1, baseOffset1 + remainingBytes) !=
UNSAFE.getInt(bytes2, baseOffset2 + remainingBytes)) {
return false;
}
}
if (remainingBytes >= 2) {
return UNSAFE.getChar(bytes1, baseOffset1) == UNSAFE.getChar(bytes2, baseOffset2) &&
(remainingBytes == 2 || bytes1[startPos1 + 2] == bytes2[startPos2 + 2]);
}
return bytes1[startPos1] == bytes2[startPos2];
}
static <U, W> AtomicReferenceFieldUpdater<U, W> newAtomicReferenceFieldUpdater(
Class<U> tclass, String fieldName) throws Exception {
return new UnsafeAtomicReferenceFieldUpdater<U, W>(UNSAFE, tclass, fieldName);

View File

@ -0,0 +1,73 @@
/*
* Copyright 2015 The Netty Project
*
* The Netty Project 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 io.netty.util.internal;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
public class PlatformDependentTest {
@Test
public void testEquals() {
byte[] bytes1 = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
byte[] bytes2 = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
assertNotSame(bytes1, bytes2);
assertTrue(PlatformDependent.equals(bytes1, 0, bytes1.length, bytes2, 0, bytes2.length));
assertTrue(PlatformDependent.equals(bytes1, 2, bytes1.length, bytes2, 2, bytes2.length));
bytes1 = new byte[] {1, 2, 3, 4, 5, 6};
bytes2 = new byte[] {1, 2, 3, 4, 5, 6, 7};
assertNotSame(bytes1, bytes2);
assertFalse(PlatformDependent.equals(bytes1, 0, bytes1.length, bytes2, 0, bytes2.length));
assertTrue(PlatformDependent.equals(bytes2, 0, 6, bytes1, 0, 6));
bytes1 = new byte[] {1, 2, 3, 4};
bytes2 = new byte[] {1, 2, 3, 5};
assertFalse(PlatformDependent.equals(bytes1, 0, bytes1.length, bytes2, 0, bytes2.length));
assertTrue(PlatformDependent.equals(bytes1, 0, 3, bytes2, 0, 3));
bytes1 = new byte[] {1, 2, 3, 4};
bytes2 = new byte[] {1, 3, 3, 4};
assertFalse(PlatformDependent.equals(bytes1, 0, bytes1.length, bytes2, 0, bytes2.length));
assertTrue(PlatformDependent.equals(bytes1, 2, bytes1.length, bytes2, 2, bytes2.length));
bytes1 = new byte[0];
bytes2 = new byte[0];
assertNotSame(bytes1, bytes2);
assertTrue(PlatformDependent.equals(bytes1, 0, 0, bytes2, 0, 0));
bytes1 = new byte[100];
bytes2 = new byte[100];
for (int i = 0; i < 100; i++) {
bytes1[i] = (byte) i;
bytes2[i] = (byte) i;
}
assertTrue(PlatformDependent.equals(bytes1, 0, bytes1.length, bytes2, 0, bytes2.length));
bytes1[50] = 0;
assertFalse(PlatformDependent.equals(bytes1, 0, bytes1.length, bytes2, 0, bytes2.length));
assertTrue(PlatformDependent.equals(bytes1, 51, bytes1.length, bytes2, 51, bytes2.length));
assertTrue(PlatformDependent.equals(bytes1, 0, 50, bytes2, 0, 50));
bytes1 = new byte[]{1, 2, 3, 4, 5};
bytes2 = new byte[]{3, 4, 5};
assertFalse(PlatformDependent.equals(bytes1, 0, bytes1.length, bytes2, 0, bytes2.length));
assertTrue(PlatformDependent.equals(bytes1, 2, bytes1.length, bytes2, 0, bytes2.length));
assertTrue(PlatformDependent.equals(bytes2, 0, bytes2.length, bytes1, 2, bytes1.length));
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2015 The Netty Project
*
* The Netty Project 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 io.netty.microbench.internal;
import io.netty.microbench.util.AbstractMicrobenchmark;
import io.netty.util.internal.PlatformDependent;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import java.util.Arrays;
@Threads(1)
@State(Scope.Benchmark)
public class PlatformDependentBenchmark extends AbstractMicrobenchmark {
@Param({ "10", "50", "100", "1000", "10000", "100000" })
private String size;
private byte[] bytes1;
private byte[] bytes2;
@Setup(Level.Trial)
public void setup() {
int size = Integer.parseInt(this.size);
bytes1 = new byte[size];
bytes2 = new byte[size];
for (int i = 0; i < size; i++) {
bytes1[i] = (byte) i;
bytes2[i] = (byte) i;
}
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
public boolean unsafeBytesEqual() {
return PlatformDependent.equals(bytes1, 0, bytes1.length, bytes2, 0, bytes2.length);
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
public boolean arraysBytesEqual() {
return Arrays.equals(bytes1, bytes2);
}
}