MIME-303 Base64OutputStream is two time slower than its Java8 counterpart
While looking and running mime4J benchmarks, I tried some alternative implementations for Base64 encoding / decoding.
Base64InputStream proved to be 20% faster than its Java8 counterpart.
However, throughtput went from 280 MB/s to 520 MB/s by switching to Java8 Base64.getMimeEncoder().wrap(...)
Tests pass and this speeds up message writing operations... Apache James can for instance benefit of it when composing JMAP messages with large attachments.
diff --git a/core/src/main/java/org/apache/james/mime4j/codec/Base64OutputStream.java b/core/src/main/java/org/apache/james/mime4j/codec/Base64OutputStream.java
index b5bb1ca..f213a81 100644
--- a/core/src/main/java/org/apache/james/mime4j/codec/Base64OutputStream.java
+++ b/core/src/main/java/org/apache/james/mime4j/codec/Base64OutputStream.java
@@ -19,11 +19,9 @@
package org.apache.james.mime4j.codec;
-import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.Base64;
/**
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite>
@@ -34,7 +32,7 @@
*
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
*/
-public class Base64OutputStream extends FilterOutputStream {
+public class Base64OutputStream extends OutputStream {
// Default line length per RFC 2045 section 6.8.
private static final int DEFAULT_LINE_LENGTH = 76;
@@ -52,39 +50,7 @@
't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', '+', '/' };
- // Byte used to pad output.
- private static final byte BASE64_PAD = '=';
-
- // This set contains all base64 characters including the pad character. Used
- // solely to check if a line separator contains any of these characters.
- private static final Set<Byte> BASE64_CHARS = new HashSet<Byte>();
-
- static {
- for (byte b : BASE64_TABLE) {
- BASE64_CHARS.add(b);
- }
- BASE64_CHARS.add(BASE64_PAD);
- }
-
- // Mask used to extract 6 bits
- private static final int MASK_6BITS = 0x3f;
-
- private static final int ENCODED_BUFFER_SIZE = 2048;
-
- private final byte[] singleByte = new byte[1];
-
- private final int lineLength;
- private final byte[] lineSeparator;
-
- private boolean closed = false;
-
- private final byte[] encoded;
- private int position = 0;
-
- private int data = 0;
- private int modulus = 0;
-
- private int linePosition = 0;
+ private final OutputStream delegate;
/**
* Creates a <code>Base64OutputStream</code> that writes the encoded data
@@ -134,187 +100,74 @@
* @param lineSeparator
* line separator to use.
*/
- public Base64OutputStream(OutputStream out, int lineLength,
- byte[] lineSeparator) {
- super(out);
-
- if (out == null)
- throw new IllegalArgumentException();
- if (lineLength < 0)
- throw new IllegalArgumentException();
- checkLineSeparator(lineSeparator);
-
- this.lineLength = lineLength;
- this.lineSeparator = new byte[lineSeparator.length];
- System.arraycopy(lineSeparator, 0, this.lineSeparator, 0,
- lineSeparator.length);
-
- this.encoded = new byte[ENCODED_BUFFER_SIZE];
+ public Base64OutputStream(OutputStream out, int lineLength, byte[] lineSeparator) {
+ ExtraCrlfOutputStream wrapped = new ExtraCrlfOutputStream(out, lineSeparator);
+ this.delegate = Base64.getMimeEncoder(lineLength, lineSeparator).wrap(wrapped);
}
@Override
- public final void write(final int b) throws IOException {
- if (closed)
- throw new IOException("Base64OutputStream has been closed");
-
- singleByte[0] = (byte) b;
- write0(singleByte, 0, 1);
+ public void write(int i) throws IOException {
+ delegate.write(i);
}
@Override
- public final void write(final byte[] buffer) throws IOException {
- if (closed)
- throw new IOException("Base64OutputStream has been closed");
-
- if (buffer == null)
- throw new NullPointerException();
-
- if (buffer.length == 0)
- return;
-
- write0(buffer, 0, buffer.length);
+ public void write(byte[] b) throws IOException {
+ delegate.write(b);
}
@Override
- public final void write(final byte[] buffer, final int offset,
- final int length) throws IOException {
- if (closed)
- throw new IOException("Base64OutputStream has been closed");
-
- if (buffer == null)
- throw new NullPointerException();
-
- if (offset < 0 || length < 0 || offset + length > buffer.length)
- throw new IndexOutOfBoundsException();
-
- if (length == 0)
- return;
-
- write0(buffer, offset, offset + length);
+ public void write(byte[] b, int off, int len) throws IOException {
+ delegate.write(b, off, len);
}
@Override
public void flush() throws IOException {
- if (closed)
- throw new IOException("Base64OutputStream has been closed");
-
- flush0();
+ delegate.flush();
}
@Override
public void close() throws IOException {
- if (closed)
- return;
-
- closed = true;
- close0();
+ delegate.close();
}
- private void write0(final byte[] buffer, final int from, final int to)
- throws IOException {
- for (int i = from; i < to; i++) {
- data = (data << 8) | (buffer[i] & 0xff);
+ private static class ExtraCrlfOutputStream extends OutputStream {
+ private final OutputStream delegate;
+ private final byte[] lineSeparator;
+ private boolean appendExtraCrlf;
- if (++modulus == 3) {
- modulus = 0;
-
- // write line separator if necessary
-
- if (lineLength > 0 && linePosition >= lineLength) {
- // writeLineSeparator() inlined for performance reasons
-
- linePosition = 0;
-
- if (encoded.length - position < lineSeparator.length)
- flush0();
-
- for (byte ls : lineSeparator)
- encoded[position++] = ls;
- }
-
- // encode data into 4 bytes
-
- if (encoded.length - position < 4)
- flush0();
-
- encoded[position++] = BASE64_TABLE[(data >> 18) & MASK_6BITS];
- encoded[position++] = BASE64_TABLE[(data >> 12) & MASK_6BITS];
- encoded[position++] = BASE64_TABLE[(data >> 6) & MASK_6BITS];
- encoded[position++] = BASE64_TABLE[data & MASK_6BITS];
-
- linePosition += 4;
- }
- }
- }
-
- private void flush0() throws IOException {
- if (position > 0) {
- out.write(encoded, 0, position);
- position = 0;
- }
- }
-
- private void close0() throws IOException {
- if (modulus != 0)
- writePad();
-
- // write line separator at the end of the encoded data
-
- if (lineLength > 0 && linePosition > 0) {
- writeLineSeparator();
+ private ExtraCrlfOutputStream(OutputStream delegate, byte[] lineSeparator) {
+ this.delegate = delegate;
+ this.lineSeparator = lineSeparator;
+ this.appendExtraCrlf = false;
}
- flush0();
- }
-
- private void writePad() throws IOException {
- // write line separator if necessary
-
- if (lineLength > 0 && linePosition >= lineLength) {
- writeLineSeparator();
+ @Override
+ public void write(int i) throws IOException {
+ delegate.write(i);
+ appendExtraCrlf = true;
}
- // encode data into 4 bytes
-
- if (encoded.length - position < 4)
- flush0();
-
- if (modulus == 1) {
- encoded[position++] = BASE64_TABLE[(data >> 2) & MASK_6BITS];
- encoded[position++] = BASE64_TABLE[(data << 4) & MASK_6BITS];
- encoded[position++] = BASE64_PAD;
- encoded[position++] = BASE64_PAD;
- } else {
- assert modulus == 2;
- encoded[position++] = BASE64_TABLE[(data >> 10) & MASK_6BITS];
- encoded[position++] = BASE64_TABLE[(data >> 4) & MASK_6BITS];
- encoded[position++] = BASE64_TABLE[(data << 2) & MASK_6BITS];
- encoded[position++] = BASE64_PAD;
+ @Override
+ public void write(byte[] b) throws IOException {
+ delegate.write(b);
+ appendExtraCrlf |= b.length > 0;
}
- linePosition += 4;
- }
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ delegate.write(b, off, len);
+ appendExtraCrlf |= len> 0;
+ }
- private void writeLineSeparator() throws IOException {
- linePosition = 0;
+ @Override
+ public void flush() throws IOException {
+ delegate.flush();
+ }
- if (encoded.length - position < lineSeparator.length)
- flush0();
-
- for (byte ls : lineSeparator)
- encoded[position++] = ls;
- }
-
- private void checkLineSeparator(byte[] lineSeparator) {
- if (lineSeparator.length > ENCODED_BUFFER_SIZE)
- throw new IllegalArgumentException("line separator length exceeds "
- + ENCODED_BUFFER_SIZE);
-
- for (byte b : lineSeparator) {
- if (BASE64_CHARS.contains(b)) {
- throw new IllegalArgumentException(
- "line separator must not contain base64 character '"
- + (char) (b & 0xff) + "'");
+ @Override
+ public void close() throws IOException {
+ if (appendExtraCrlf) {
+ delegate.write(lineSeparator);
}
}
}