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);
             }
         }
     }