Inefficient byte[] to String constructor

by Mikhail Vorontsov

Everything written in this post is related to Java 6 only.

A String constructor was added in Java 6 in order to facilitate conversion from byte[] to String using a provided Charset:

1
public String(byte bytes[], int offset, int length, Charset charset)
public String(byte bytes[], int offset, int length, Charset charset)

It looks harmless from the first sight, but it has a potential problem inside: it makes a temporary “defensive” copy of a provided byte[] inside StringCoding.decode(Charset cs, byte[] ba, int off, int len) method. It may be needed in a very few applications, but most applications will pay unfair price for this method call.

Real problem happens when you have a long source byte[]. This will cause a short-lived temporary copy of that array to be created on each constructor call, thus causing minor garbage collections to happen very often. Generally, time of minor GC depends on eden generation size. It is quite quick for under 1Gb heaps, but starts to grow rather quickly after that. As a result, calling this constructor with long source byte[] in a large application my seriously increase time spent in GC.

That’s an extract from Java 6 src.zip file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String:
public String(byte bytes[], int offset, int length, Charset charset) {
    if (charset == null)
        throw new NullPointerException("charset");
    checkBounds(bytes, offset, length);
    char[] v = StringCoding.decode(charset, bytes, offset, length);
    this.offset = 0;
    this.count = v.length;
    this.value = v;
}
 
StringCoding:
static char[] decode(Charset cs, byte[] ba, int off, int len) {
    StringDecoder sd = new StringDecoder(cs, cs.name());
    byte[] b = Arrays.copyOf(ba, ba.length);
    return sd.decode(b, off, len);
}
String:
public String(byte bytes[], int offset, int length, Charset charset) {
    if (charset == null)
        throw new NullPointerException("charset");
    checkBounds(bytes, offset, length);
    char[] v = StringCoding.decode(charset, bytes, offset, length);
    this.offset = 0;
    this.count = v.length;
    this.value = v;
}

StringCoding:
static char[] decode(Charset cs, byte[] ba, int off, int len) {
    StringDecoder sd = new StringDecoder(cs, cs.name());
    byte[] b = Arrays.copyOf(ba, ba.length);
    return sd.decode(b, off, len);
}

Try running the following program with various -Xmx settings. Use -verbose:gc to see GC information. On my machine, average minor GC time with -Xmx500M was about 0.004 sec; with -Xmx2500M – 0.02 sec (yes, 5 times increase in available heap means 5 times increase in minor GC time in case of permanently full heap).

After that, I’ve added a byte[] staying alive for the whole program life. With size of such array=1G, average minor GC time has gone up to 0.05 sec. But the biggest surprise was when this buffer size was decreased to 512M. Instead of minor garbage collections I saw major garbage collections ~0.25 sec long (though they cleaned way more memory rather than minor GCs).

What we can do to solve this problem:

    • Copy required part of byte[] to temporary array before calling this constructor
      Or start using Java 7. It is about time to do it.
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    
    public class StringConstructorTest {
        private static final int BUF_SIZE = 200000;
        private static final Charset ASCII = Charset.forName( "US-ASCII" );
     
        public static void main(String[] args) {
            //init data with many repetitions of A-Z
            final byte[] temp = new byte[ 1024 * 1024 * 1024 ];
            temp[ 5 ] = 34;
            final byte[] data = getData();
     
            {
                final long start = System.currentTimeMillis();
                testFaulty(data);
                final long time = System.currentTimeMillis() - start;
                System.out.println( "Time for slow call: " + time / 1000.0 + " sec");
            }
            {
                final long start = System.currentTimeMillis();
                testFixed(data);
                final long time = System.currentTimeMillis() - start;
                System.out.println( "Time for fixed call: " + time / 1000.0 + " sec");
            }
            System.out.println( temp.length );
        }
     
        //test loop for slow constructor
        private static void testFaulty(byte[] data) {
            long totalLen = 0;
            //read each repetition as a separate String
            for ( int i = 0; i < 100; ++i )
            {
                totalLen += test(data);
                //System.out.println( i );
            }
     
            System.out.println( totalLen );
        }
     
        //test loop for fixed constructor call
        private static void testFixed(byte[] data) {
            long totalLen = 0;
            //read each repetition as a separate String
            for ( int i = 0; i < 100; ++i )
                totalLen += testOk(data);
     
            System.out.println( totalLen );
        }
     
        //single pass with slow constructor
        private static long test(byte[] data) {
            final int CNT = BUF_SIZE / 26;
            int p = 0;
            long len = 0;
            for ( int i = 0; i < CNT; ++i, p += 26 )
            {
                final String part = new String( data, p, 26, ASCII );
                len += part.length();
            }
            return len;
        }
     
        //single pass working ok in Java 6
        private static long testOk(byte[] data) {
            final int CNT = BUF_SIZE / 26;
            int p = 0;
            long len = 0;
            for ( int i = 0; i < CNT; ++i, p += 26 )
            {
                final byte[] tmp = Arrays.copyOfRange( data, p, p + 26 );
                final String part = new String( tmp, 0, tmp.length, ASCII );
                len += part.length();
            }
            return len;
        }
     
        private static byte[] getData() {
            final byte[] data = new byte[ BUF_SIZE ];
            byte c = 65;
            for ( int i = 0; i < BUF_SIZE; ++i )
            {
                data[ i ] = c;
                if ( c == 90 )
                    c = 65;
                else
                    ++c;
            }
            return data;
        }
    }
    public class StringConstructorTest {
        private static final int BUF_SIZE = 200000;
        private static final Charset ASCII = Charset.forName( "US-ASCII" );
    
        public static void main(String[] args) {
            //init data with many repetitions of A-Z
            final byte[] temp = new byte[ 1024 * 1024 * 1024 ];
            temp[ 5 ] = 34;
            final byte[] data = getData();
    
            {
                final long start = System.currentTimeMillis();
                testFaulty(data);
                final long time = System.currentTimeMillis() - start;
                System.out.println( "Time for slow call: " + time / 1000.0 + " sec");
            }
            {
                final long start = System.currentTimeMillis();
                testFixed(data);
                final long time = System.currentTimeMillis() - start;
                System.out.println( "Time for fixed call: " + time / 1000.0 + " sec");
            }
            System.out.println( temp.length );
        }
    
        //test loop for slow constructor
        private static void testFaulty(byte[] data) {
            long totalLen = 0;
            //read each repetition as a separate String
            for ( int i = 0; i < 100; ++i )
            {
                totalLen += test(data);
                //System.out.println( i );
            }
    
            System.out.println( totalLen );
        }
    
        //test loop for fixed constructor call
        private static void testFixed(byte[] data) {
            long totalLen = 0;
            //read each repetition as a separate String
            for ( int i = 0; i < 100; ++i )
                totalLen += testOk(data);
    
            System.out.println( totalLen );
        }
    
        //single pass with slow constructor
        private static long test(byte[] data) {
            final int CNT = BUF_SIZE / 26;
            int p = 0;
            long len = 0;
            for ( int i = 0; i < CNT; ++i, p += 26 )
            {
                final String part = new String( data, p, 26, ASCII );
                len += part.length();
            }
            return len;
        }
    
        //single pass working ok in Java 6
        private static long testOk(byte[] data) {
            final int CNT = BUF_SIZE / 26;
            int p = 0;
            long len = 0;
            for ( int i = 0; i < CNT; ++i, p += 26 )
            {
                final byte[] tmp = Arrays.copyOfRange( data, p, p + 26 );
                final String part = new String( tmp, 0, tmp.length, ASCII );
                len += part.length();
            }
            return len;
        }
    
        private static byte[] getData() {
            final byte[] data = new byte[ BUF_SIZE ];
            byte c = 65;
            for ( int i = 0; i < BUF_SIZE; ++i )
            {
                data[ i ] = c;
                if ( c == 90 )
                    c = 65;
                else
                    ++c;
            }
            return data;
        }
    }

    Summary

    Try to avoid unnecessary memory allocations in your program, because it may impact performance of your program in case if it is already using enough memory (1G+).

    Especially try to avoid discussed constructor calls with long buffers. For 100,000 byte buffer provided programs runs for 21.011 sec for original constructor call and 0.191 sec for fixed call. After extending source buffer to 200,000 bytes, runtime for original constructor call predictably increased 4-fold: 83.203 sec. Time for fixed call has increased less than 2-fold: 0.354 sec (this is right, because now we have 2 times more data to process).


    Leave a Reply

    Your email address will not be published. Required fields are marked *