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.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
@ -109,7 +114,6 @@ public final class Base64 {
public static ByteBuf encode(
ByteBuf src, int off, int len, boolean breakLines, Base64Dialect dialect, ByteBufAllocator allocator) {
if (src == null) {
throw new NullPointerException("src");
}
@ -117,17 +121,18 @@ public final class Base64 {
throw new NullPointerException("dialect");
}
int len43 = len * 4 / 3;
int len43 = (len << 2) / 3;
ByteBuf dest = allocator.buffer(
len43 +
(len % 3 > 0 ? 4 : 0) + // Account for padding
(breakLines ? len43 / MAX_LINE_LENGTH : 0)).order(src.order()); // New lines
byte[] alphabet = alphabet(dialect);
int d = 0;
int e = 0;
int len2 = len - 2;
int lineLength = 0;
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;
@ -139,7 +144,7 @@ public final class Base64 {
} // end for: each piece of array
if (d < len) {
encode3to4(src, d + off, len - d, dest, e, dialect);
encode3to4(src, d + off, len - d, dest, e, alphabet);
e += 4;
} // end if: some padding needed
@ -152,11 +157,7 @@ public final class Base64 {
}
private static void encode3to4(
ByteBuf src, int srcOffset, int numSigBytes,
ByteBuf dest, int destOffset, Base64Dialect dialect) {
byte[] ALPHABET = alphabet(dialect);
ByteBuf src, int srcOffset, int numSigBytes, ByteBuf dest, int destOffset, byte[] alphabet) {
// 1 2 3
// 01234567890123456789012345678901 Bit position
// --------000000001111111122222222 Array position from threeBytes
@ -168,30 +169,110 @@ public final class Base64 {
// significant bytes passed in the array.
// 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.
int inBuff =
(numSigBytes > 0? src.getByte(srcOffset) << 24 >>> 8 : 0) |
(numSigBytes > 1? src.getByte(srcOffset + 1) << 24 >>> 16 : 0) |
(numSigBytes > 2? src.getByte(srcOffset + 2) << 24 >>> 24 : 0);
if (src.order() == ByteOrder.BIG_ENDIAN) {
final int inBuff;
switch (numSigBytes) {
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) {
case 3:
dest.setByte(destOffset , ALPHABET[inBuff >>> 18 ]);
dest.setByte(destOffset + 1, ALPHABET[inBuff >>> 12 & 0x3f]);
dest.setByte(destOffset + 2, ALPHABET[inBuff >>> 6 & 0x3f]);
dest.setByte(destOffset + 3, ALPHABET[inBuff & 0x3f]);
break;
case 2:
dest.setByte(destOffset , ALPHABET[inBuff >>> 18 ]);
dest.setByte(destOffset + 1, ALPHABET[inBuff >>> 12 & 0x3f]);
dest.setByte(destOffset + 2, ALPHABET[inBuff >>> 6 & 0x3f]);
dest.setByte(destOffset + 3, EQUALS_SIGN);
break;
case 1:
dest.setByte(destOffset , ALPHABET[inBuff >>> 18 ]);
dest.setByte(destOffset + 1, ALPHABET[inBuff >>> 12 & 0x3f]);
dest.setByte(destOffset + 2, EQUALS_SIGN);
dest.setByte(destOffset + 3, EQUALS_SIGN);
break;
case 3:
dest.setInt(destOffset, alphabet[inBuff >>> 18 ] << 24 |
alphabet[inBuff >>> 12 & 0x3f] << 16 |
alphabet[inBuff >>> 6 & 0x3f] << 8 |
alphabet[inBuff & 0x3f]);
break;
case 2:
dest.setInt(destOffset, alphabet[inBuff >>> 18 ] << 24 |
alphabet[inBuff >>> 12 & 0x3f] << 16 |
alphabet[inBuff >>> 6 & 0x3f] << 8 |
EQUALS_SIGN);
break;
case 1:
dest.setInt(destOffset, alphabet[inBuff >>> 18 ] << 24 |
alphabet[inBuff >>> 12 & 0x3f] << 16 |
EQUALS_SIGN << 8 |
EQUALS_SIGN);
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) {
if (src == null) {
throw new NullPointerException("src");
}
@ -222,7 +302,6 @@ public final class Base64 {
public static ByteBuf decode(
ByteBuf src, int off, int len, Base64Dialect dialect, ByteBufAllocator allocator) {
if (src == null) {
throw new NullPointerException("src");
}
@ -230,85 +309,96 @@ public final class Base64 {
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;
ByteBuf dest = allocator.buffer(len34).order(src.order()); // Upper limit on size of output
int outBuffPosn = 0;
private static final class Decoder implements ByteProcessor {
private final byte[] b4 = new byte[4];
private int b4Posn;
private byte sbiCrop;
private byte sbiDecode;
private byte[] decodabet;
private int outBuffPosn;
private ByteBuf dest;
byte[] b4 = new byte[4];
int b4Posn = 0;
int i;
byte sbiCrop;
byte sbiDecode;
for (i = off; i < off + len; i ++) {
sbiCrop = (byte) (src.getByte(i) & 0x7f); // Only the low seven bits
sbiDecode = DECODABET[sbiCrop];
ByteBuf decode(ByteBuf src, int off, int len, ByteBufAllocator allocator, Base64Dialect dialect) {
int len34 = (len * 3) >>> 2;
dest = allocator.buffer(len34).order(src.order()); // Upper limit on size of output
decodabet = decodabet(dialect);
try {
src.forEachByte(off, len, this);
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 >= EQUALS_SIGN_ENC) { // Equals sign or better
b4[b4Posn ++] = sbiCrop;
if (b4Posn > 3) { // Quartet built
outBuffPosn += decode4to3(
b4, 0, dest, outBuffPosn, dialect);
outBuffPosn += decode4to3(b4, dest, outBuffPosn, decodabet);
b4Posn = 0;
// If that was the equals sign, break out of 'for' loop
if (sbiCrop == EQUALS_SIGN) {
break;
return false;
}
}
}
} else {
throw new IllegalArgumentException(
"bad Base64 input character at " + i + ": " +
src.getUnsignedByte(i) + " (decimal)");
return true;
}
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(
byte[] src, int srcOffset,
ByteBuf dest, int destOffset, Base64Dialect dialect) {
if (src[3] == EQUALS_SIGN) {
// Example: DkL=
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
int outBuff;
final int outBuff;
try {
outBuff =
(DECODABET[src[srcOffset ]] & 0xFF) << 18 |
(DECODABET[src[srcOffset + 1]] & 0xFF) << 12 |
(DECODABET[src[srcOffset + 2]] & 0xFF) << 6 |
DECODABET[src[srcOffset + 3]] & 0xFF;
outBuff = (decodabet[src[0]] & 0xff) << 18 |
(decodabet[src[1]] & 0xff) << 12 |
(decodabet[src[2]] & 0xff) << 6 |
decodabet[src[3]] & 0xff;
} catch (IndexOutOfBoundsException ignored) {
throw new IllegalArgumentException("not encoded in Base64");
}
dest.setByte(destOffset , (byte) (outBuff >> 16));
dest.setByte(destOffset + 1, (byte) (outBuff >> 8));
dest.setByte(destOffset + 2, (byte) outBuff);
// Just directly set it as medium
if (dest.order() == ByteOrder.BIG_ENDIAN) {
dest.setMedium(destOffset, outBuff);
} else {
dest.setMedium(destOffset, ByteBufUtil.swapMedium(outBuff));
}
return 3;
}
}

View File

@ -18,10 +18,12 @@ package io.netty.handler.codec.base64;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.PlatformDependent;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.nio.ByteOrder;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@ -116,4 +118,42 @@ public class Base64Test {
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();
}
}
}