Improve performance of Base64.decode and encode methods.

Motivation:

The decode and encode method uses getByte(...) and setByte(...) in loops which can be very expensive because of bounds / reference-count checking. Beside this it also slows-down a lot when paranoid leak-detection is enabled as it will track each access.

Modifications:

- Pack bytes into int / short and so reduce operations on the ByteBuf
- Use ByteProcessor to reduce getByte calls.

Result:

Better performance in general. Also when you run the build with -Pleak the handler module will build in 1/4 of the time it took before.
This commit is contained in:
Norman Maurer 2017-02-17 14:35:39 +01:00
parent 0623c6c533
commit 7feb92959e
2 changed files with 217 additions and 87 deletions

View File

@ -21,6 +21,11 @@ package io.netty.handler.codec.base64;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.netty.util.ByteProcessor;
import io.netty.util.internal.PlatformDependent;
import java.nio.ByteOrder;
/** /**
* Utility class for {@link ByteBuf} that encodes and decodes to and from * Utility class for {@link ByteBuf} that encodes and decodes to and from
@ -109,7 +114,6 @@ public final class Base64 {
public static ByteBuf encode( public static ByteBuf encode(
ByteBuf src, int off, int len, boolean breakLines, Base64Dialect dialect, ByteBufAllocator allocator) { ByteBuf src, int off, int len, boolean breakLines, Base64Dialect dialect, ByteBufAllocator allocator) {
if (src == null) { if (src == null) {
throw new NullPointerException("src"); throw new NullPointerException("src");
} }
@ -117,17 +121,18 @@ public final class Base64 {
throw new NullPointerException("dialect"); throw new NullPointerException("dialect");
} }
int len43 = len * 4 / 3; int len43 = (len << 2) / 3;
ByteBuf dest = allocator.buffer( ByteBuf dest = allocator.buffer(
len43 + len43 +
(len % 3 > 0 ? 4 : 0) + // Account for padding (len % 3 > 0 ? 4 : 0) + // Account for padding
(breakLines ? len43 / MAX_LINE_LENGTH : 0)).order(src.order()); // New lines (breakLines ? len43 / MAX_LINE_LENGTH : 0)).order(src.order()); // New lines
byte[] alphabet = alphabet(dialect);
int d = 0; int d = 0;
int e = 0; int e = 0;
int len2 = len - 2; int len2 = len - 2;
int lineLength = 0; int lineLength = 0;
for (; d < len2; d += 3, e += 4) { for (; d < len2; d += 3, e += 4) {
encode3to4(src, d + off, 3, dest, e, dialect); encode3to4(src, d + off, 3, dest, e, alphabet);
lineLength += 4; lineLength += 4;
@ -139,7 +144,7 @@ public final class Base64 {
} // end for: each piece of array } // end for: each piece of array
if (d < len) { if (d < len) {
encode3to4(src, d + off, len - d, dest, e, dialect); encode3to4(src, d + off, len - d, dest, e, alphabet);
e += 4; e += 4;
} // end if: some padding needed } // end if: some padding needed
@ -152,11 +157,7 @@ public final class Base64 {
} }
private static void encode3to4( private static void encode3to4(
ByteBuf src, int srcOffset, int numSigBytes, ByteBuf src, int srcOffset, int numSigBytes, ByteBuf dest, int destOffset, byte[] alphabet) {
ByteBuf dest, int destOffset, Base64Dialect dialect) {
byte[] ALPHABET = alphabet(dialect);
// 1 2 3 // 1 2 3
// 01234567890123456789012345678901 Bit position // 01234567890123456789012345678901 Bit position
// --------000000001111111122222222 Array position from threeBytes // --------000000001111111122222222 Array position from threeBytes
@ -168,30 +169,110 @@ public final class Base64 {
// significant bytes passed in the array. // significant bytes passed in the array.
// We have to shift left 24 in order to flush out the 1's that appear // We have to shift left 24 in order to flush out the 1's that appear
// when Java treats a value as negative that is cast from a byte to an int. // when Java treats a value as negative that is cast from a byte to an int.
int inBuff = if (src.order() == ByteOrder.BIG_ENDIAN) {
(numSigBytes > 0? src.getByte(srcOffset) << 24 >>> 8 : 0) | final int inBuff;
(numSigBytes > 1? src.getByte(srcOffset + 1) << 24 >>> 16 : 0) | switch (numSigBytes) {
(numSigBytes > 2? src.getByte(srcOffset + 2) << 24 >>> 24 : 0); case 1:
inBuff = toInt(src.getByte(srcOffset));
break;
case 2:
inBuff = toIntBE(src.getShort(srcOffset));
break;
default:
inBuff = numSigBytes <= 0 ? 0 : toIntBE(src.getMedium(srcOffset));
break;
}
encode3to4BigEndian(inBuff, numSigBytes, dest, destOffset, alphabet);
} else {
final int inBuff;
switch (numSigBytes) {
case 1:
inBuff = toInt(src.getByte(srcOffset));
break;
case 2:
inBuff = toIntLE(src.getShort(srcOffset));
break;
default:
inBuff = numSigBytes <= 0 ? 0 : toIntLE(src.getMedium(srcOffset));
break;
}
encode3to4LittleEndian(inBuff, numSigBytes, dest, destOffset, alphabet);
}
}
private static int toInt(byte value) {
return (value & 0xff) << 16;
}
private static int toIntBE(short value) {
return (value & 0xff00) << 8 | (value & 0xff) << 8;
}
private static int toIntLE(short value) {
return (value & 0xff) << 16 | (value & 0xff00);
}
private static int toIntBE(int mediumValue) {
return (mediumValue & 0xff0000) | (mediumValue & 0xff00) | (mediumValue & 0xff);
}
private static int toIntLE(int mediumValue) {
return (mediumValue & 0xff) << 16 | (mediumValue & 0xff00) | (mediumValue & 0xff0000) >>> 16;
}
private static void encode3to4BigEndian(
int inBuff, int numSigBytes, ByteBuf dest, int destOffset, byte[] alphabet) {
// Packing bytes into an int to reduce bound and reference count checking.
switch (numSigBytes) { switch (numSigBytes) {
case 3: case 3:
dest.setByte(destOffset , ALPHABET[inBuff >>> 18 ]); dest.setInt(destOffset, alphabet[inBuff >>> 18 ] << 24 |
dest.setByte(destOffset + 1, ALPHABET[inBuff >>> 12 & 0x3f]); alphabet[inBuff >>> 12 & 0x3f] << 16 |
dest.setByte(destOffset + 2, ALPHABET[inBuff >>> 6 & 0x3f]); alphabet[inBuff >>> 6 & 0x3f] << 8 |
dest.setByte(destOffset + 3, ALPHABET[inBuff & 0x3f]); alphabet[inBuff & 0x3f]);
break; break;
case 2: case 2:
dest.setByte(destOffset , ALPHABET[inBuff >>> 18 ]); dest.setInt(destOffset, alphabet[inBuff >>> 18 ] << 24 |
dest.setByte(destOffset + 1, ALPHABET[inBuff >>> 12 & 0x3f]); alphabet[inBuff >>> 12 & 0x3f] << 16 |
dest.setByte(destOffset + 2, ALPHABET[inBuff >>> 6 & 0x3f]); alphabet[inBuff >>> 6 & 0x3f] << 8 |
dest.setByte(destOffset + 3, EQUALS_SIGN); EQUALS_SIGN);
break; break;
case 1: case 1:
dest.setByte(destOffset , ALPHABET[inBuff >>> 18 ]); dest.setInt(destOffset, alphabet[inBuff >>> 18 ] << 24 |
dest.setByte(destOffset + 1, ALPHABET[inBuff >>> 12 & 0x3f]); alphabet[inBuff >>> 12 & 0x3f] << 16 |
dest.setByte(destOffset + 2, EQUALS_SIGN); EQUALS_SIGN << 8 |
dest.setByte(destOffset + 3, EQUALS_SIGN); EQUALS_SIGN);
break; break;
default:
// NOOP
break;
}
}
private static void encode3to4LittleEndian(
int inBuff, int numSigBytes, ByteBuf dest, int destOffset, byte[] alphabet) {
// Packing bytes into an int to reduce bound and reference count checking.
switch (numSigBytes) {
case 3:
dest.setInt(destOffset, alphabet[inBuff >>> 18 ] |
alphabet[inBuff >>> 12 & 0x3f] << 8 |
alphabet[inBuff >>> 6 & 0x3f] << 16 |
alphabet[inBuff & 0x3f] << 24);
break;
case 2:
dest.setInt(destOffset, alphabet[inBuff >>> 18 ] |
alphabet[inBuff >>> 12 & 0x3f] << 8 |
alphabet[inBuff >>> 6 & 0x3f] << 16 |
EQUALS_SIGN << 24);
break;
case 1:
dest.setInt(destOffset, alphabet[inBuff >>> 18 ] |
alphabet[inBuff >>> 12 & 0x3f] << 8 |
EQUALS_SIGN << 16 |
EQUALS_SIGN << 24);
break;
default:
// NOOP
break;
} }
} }
@ -200,7 +281,6 @@ public final class Base64 {
} }
public static ByteBuf decode(ByteBuf src, Base64Dialect dialect) { public static ByteBuf decode(ByteBuf src, Base64Dialect dialect) {
if (src == null) { if (src == null) {
throw new NullPointerException("src"); throw new NullPointerException("src");
} }
@ -222,7 +302,6 @@ public final class Base64 {
public static ByteBuf decode( public static ByteBuf decode(
ByteBuf src, int off, int len, Base64Dialect dialect, ByteBufAllocator allocator) { ByteBuf src, int off, int len, Base64Dialect dialect, ByteBufAllocator allocator) {
if (src == null) { if (src == null) {
throw new NullPointerException("src"); throw new NullPointerException("src");
} }
@ -230,85 +309,96 @@ public final class Base64 {
throw new NullPointerException("dialect"); throw new NullPointerException("dialect");
} }
byte[] DECODABET = decodabet(dialect); // Using a ByteProcessor to reduce bound and reference count checking.
return new Decoder().decode(src, off, len, allocator, dialect);
}
int len34 = len * 3 / 4; private static final class Decoder implements ByteProcessor {
ByteBuf dest = allocator.buffer(len34).order(src.order()); // Upper limit on size of output private final byte[] b4 = new byte[4];
int outBuffPosn = 0; private int b4Posn;
private byte sbiCrop;
private byte sbiDecode;
private byte[] decodabet;
private int outBuffPosn;
private ByteBuf dest;
byte[] b4 = new byte[4]; ByteBuf decode(ByteBuf src, int off, int len, ByteBufAllocator allocator, Base64Dialect dialect) {
int b4Posn = 0; int len34 = (len * 3) >>> 2;
int i; dest = allocator.buffer(len34).order(src.order()); // Upper limit on size of output
byte sbiCrop;
byte sbiDecode; decodabet = decodabet(dialect);
for (i = off; i < off + len; i ++) { try {
sbiCrop = (byte) (src.getByte(i) & 0x7f); // Only the low seven bits src.forEachByte(off, len, this);
sbiDecode = DECODABET[sbiCrop]; return dest.slice(0, outBuffPosn);
} catch (Throwable cause) {
dest.release();
PlatformDependent.throwException(cause);
return null;
}
}
@Override
public boolean process(byte value) throws Exception {
sbiCrop = (byte) (value & 0x7f); // Only the low seven bits
sbiDecode = decodabet[sbiCrop];
if (sbiDecode >= WHITE_SPACE_ENC) { // White space, Equals sign or better if (sbiDecode >= WHITE_SPACE_ENC) { // White space, Equals sign or better
if (sbiDecode >= EQUALS_SIGN_ENC) { // Equals sign or better if (sbiDecode >= EQUALS_SIGN_ENC) { // Equals sign or better
b4[b4Posn ++] = sbiCrop; b4[b4Posn ++] = sbiCrop;
if (b4Posn > 3) { // Quartet built if (b4Posn > 3) { // Quartet built
outBuffPosn += decode4to3( outBuffPosn += decode4to3(b4, dest, outBuffPosn, decodabet);
b4, 0, dest, outBuffPosn, dialect);
b4Posn = 0; b4Posn = 0;
// If that was the equals sign, break out of 'for' loop // If that was the equals sign, break out of 'for' loop
if (sbiCrop == EQUALS_SIGN) { if (sbiCrop == EQUALS_SIGN) {
break; return false;
} }
} }
} }
} else { return true;
throw new IllegalArgumentException(
"bad Base64 input character at " + i + ": " +
src.getUnsignedByte(i) + " (decimal)");
} }
throw new IllegalArgumentException(
"invalid bad Base64 input character: " + (short) (value & 0xFF) + " (decimal)");
} }
return dest.slice(0, outBuffPosn); private static int decode4to3(byte[] src, ByteBuf dest, int destOffset, byte[] decodabet) {
} if (src[2] == EQUALS_SIGN) {
// Example: Dk==
dest.setByte(destOffset, (byte) ((decodabet[src[0]] & 0xff) << 2 | (decodabet[src[1]] & 0xff) >>> 4));
return 1;
}
private static int decode4to3( if (src[3] == EQUALS_SIGN) {
byte[] src, int srcOffset, // Example: DkL=
ByteBuf dest, int destOffset, Base64Dialect dialect) { int outBuff = (decodabet[src[0]] & 0xff) << 18 |
(decodabet[src[1]] & 0xff) << 12 |
(decodabet[src[2]] & 0xff) << 6;
byte[] DECODABET = decodabet(dialect); // Packing bytes into a short to reduce bound and reference count checking.
if (dest.order() == ByteOrder.BIG_ENDIAN) {
dest.setShort(destOffset, (short) ((outBuff & 0xff0000) >>> 8 | (outBuff & 0xff00) >>> 8));
} else {
dest.setShort(destOffset, (short) ((outBuff & 0xff0000) >>> 16 | (outBuff & 0xff00)));
}
return 2;
}
if (src[srcOffset + 2] == EQUALS_SIGN) {
// Example: Dk==
int outBuff =
(DECODABET[src[srcOffset ]] & 0xFF) << 18 |
(DECODABET[src[srcOffset + 1]] & 0xFF) << 12;
dest.setByte(destOffset, (byte) (outBuff >>> 16));
return 1;
} else if (src[srcOffset + 3] == EQUALS_SIGN) {
// Example: DkL=
int outBuff =
(DECODABET[src[srcOffset ]] & 0xFF) << 18 |
(DECODABET[src[srcOffset + 1]] & 0xFF) << 12 |
(DECODABET[src[srcOffset + 2]] & 0xFF) << 6;
dest.setByte(destOffset , (byte) (outBuff >>> 16));
dest.setByte(destOffset + 1, (byte) (outBuff >>> 8));
return 2;
} else {
// Example: DkLE // Example: DkLE
int outBuff; final int outBuff;
try { try {
outBuff = outBuff = (decodabet[src[0]] & 0xff) << 18 |
(DECODABET[src[srcOffset ]] & 0xFF) << 18 | (decodabet[src[1]] & 0xff) << 12 |
(DECODABET[src[srcOffset + 1]] & 0xFF) << 12 | (decodabet[src[2]] & 0xff) << 6 |
(DECODABET[src[srcOffset + 2]] & 0xFF) << 6 | decodabet[src[3]] & 0xff;
DECODABET[src[srcOffset + 3]] & 0xFF;
} catch (IndexOutOfBoundsException ignored) { } catch (IndexOutOfBoundsException ignored) {
throw new IllegalArgumentException("not encoded in Base64"); throw new IllegalArgumentException("not encoded in Base64");
} }
// Just directly set it as medium
dest.setByte(destOffset , (byte) (outBuff >> 16)); if (dest.order() == ByteOrder.BIG_ENDIAN) {
dest.setByte(destOffset + 1, (byte) (outBuff >> 8)); dest.setMedium(destOffset, outBuff);
dest.setByte(destOffset + 2, (byte) outBuff); } else {
dest.setMedium(destOffset, ByteBufUtil.swapMedium(outBuff));
}
return 3; return 3;
} }
} }

View File

@ -18,10 +18,12 @@ package io.netty.handler.codec.base64;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import io.netty.util.internal.PlatformDependent;
import org.junit.Test; import org.junit.Test;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.nio.ByteOrder;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
@ -116,4 +118,42 @@ public class Base64Test {
encoded.release(); encoded.release();
} }
} }
@Test
public void testEncodeDecodeBE() {
testEncodeDecode(ByteOrder.BIG_ENDIAN);
}
@Test
public void testEncodeDecodeLE() {
testEncodeDecode(ByteOrder.LITTLE_ENDIAN);
}
private static void testEncodeDecode(ByteOrder order) {
testEncodeDecode(64, order);
testEncodeDecode(128, order);
testEncodeDecode(512, order);
testEncodeDecode(1024, order);
testEncodeDecode(4096, order);
testEncodeDecode(8192, order);
testEncodeDecode(16384, order);
}
private static void testEncodeDecode(int size, ByteOrder order) {
byte[] bytes = new byte[size];
PlatformDependent.threadLocalRandom().nextBytes(bytes);
ByteBuf src = Unpooled.wrappedBuffer(bytes).order(order);
ByteBuf encoded = Base64.encode(src);
ByteBuf decoded = Base64.decode(encoded);
ByteBuf expectedBuf = Unpooled.wrappedBuffer(bytes);
try {
assertEquals(expectedBuf, decoded);
} finally {
src.release();
encoded.release();
decoded.release();
expectedBuf.release();
}
}
} }