Convert int to String Java fastest method - feature image

How to convert int to String in Java – the fastest way

In this article, I show you how to convert an int into a String in Java the fastest way. The answer is probably surprising for some. I present four variants and measure and compare their speed using JMH microbenchmarks. I analyze the measurement results looking at the Java source code and also the generated bytecode. If you want to skip the details, you can use this link to scroll down directly to the result.

Java int-to-String conversion variants

The following four options are available (apart from intentionally more complicated variants):

  • Option 1: Integer.toString(i)
  • Option 2: String.valueOf(i)
  • Option 3: String.format("%d", i)
  • Option 4: "" + i

Quiz

What do you think? Which option is the fastest?

In the following sections, I first perform detailed benchmarks and then interpret the results based on the Java source code and the generated bytecode.

Performance measurements of the int-to-String conversion

To find out which of the options is the fastest, I ran several benchmarks with the Java Microbenchmark Harness – short: JMH.

JMH is a framework that facilitates benchmark tests for short code sections and provides meaningful measurements in milli-, micro- and nano-second ranges. Tests are repeated hundreds of thousands of times, and the actual measurement process is only started after a warm-up phase to give the just-in-time compiler sufficient lead time for code optimization.

A good tutorial for beginners can be found on tutorials.jenkov.com.

IntelliJ comes with a JMH plugin by default so that you can run the benchmark tests directly in your IDE.

Source code of the microbenchmarks

Below you find the complete source code of the int-to-String benchmark. You can copy the code directly into your IDE or clone it as a Maven project from my GitLab repository. When you create a project yourself, you need to add the following two dependencies:

package eu.happycoders.int2string;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.ThreadLocalRandom;

public class IntToStringBenchmark {

    @State(Scope.Thread)
    public static class MyState {
        public int i;

        @Setup(Level.Invocation)
        public void doSetup() {
            // always 7-digits, so that the String always has the same length
            i = 1_000_000 + ThreadLocalRandom.current().nextInt(9_000_000);
        }
    }

    @Benchmark
    public void option1(MyState state, Blackhole blackhole) {
        String s = Integer.toString(state.i);
        blackhole.consume(s);
    }

    @Benchmark
    public void option2(MyState state, Blackhole blackhole) {
        String s = String.valueOf(state.i);
        blackhole.consume(s);
    }

    @Benchmark
    public void option3(MyState state, Blackhole blackhole) {
        String s = String.format("%d", state.i);
        blackhole.consume(s);
    }

    @Benchmark
    public void option4(MyState state, Blackhole blackhole) {
        String s = "" + state.i;
        blackhole.consume(s);
    }

}

A few remarks about the source code:

  • We assign a random number to the int-variable i so that it is not replaced by a constant, which would result in the complete String conversion being optimized away.
  • The random number is generated in the setup() method of a so-called “state” so that the execution time for it is not measured.
  • The @Setup(Level.Invocation) annotation causes the setup() method to be executed before each invocation of the test method; thus, each invocation receives a new random number.
  • The conversion result is always passed to the Blackhole – again so that the compiler does not optimize away the conversion.

Microbenchmark results

In the following sections, you find the measurement results on my Dell XPS 15 9570 with an Intel Core i7-8750H. Detailed results (including all single tests, minimums, maximums, and standard deviations) can be found in the results/ directory of my GitLab repository.

Measurement results int-to-String on Java 7

MethodOperations per secondConfidence ​​interval (99.9%)
Integer.​​toString(i)20,365,94720,276,015 – 20,455,879
String.​​valueOf(i)20,318,31620,251,621 – 20,385,011
String.​​format("%d", i)2,107,3972,075,553 – 2,139,240
"" + i23,358,66823,178,506 – 23,538,831
Java 7 int-to-String performance
Java 7 int-to-String performance

The first two variants can be regarded as equally fast, which is to be expected since String.valueOf(i) simply calls Integer.toString(i), and this call is inlined by the HotSpot compiler.

As expected, the third variant is slower, since the format string must be parsed here.

The fourth variant ("" + i) is significantly faster under Java 7 (almost 15%) than the first two variants.

Measurement results int-to-string on Java 8

MethodOperations per secondConfidence ​interval (99.9%)
Integer.​toString(i)20,939,91020,699,671 – 21,180,149
String.​valueOf(i)20,920,35920,737,898 – 21,102,821
String.​format("%d", i)2,284,0272,218,004 – 2,350,050
"" + i23,777,73823,651,239 – 23,904,237
Java 8 int-to-String performance
Java 8 int-to-String performance

Java 8 shows a very similar result as Java 7, with all variants having increased in speed between 2% and 8%.

Measurement results int-to-string on Java 9

MethodOperations per secondConfidence ​​interval (99.9%)
Integer.​toString(i)28,025,70027,829,430 – 28,221,969
String.​​valueOf(i)27,732,47427,646,937 – 27,818,010
String.​​format("%d", i)2,718,3772,680,574 – 2,756,179
"" + i28,354,69028,151,883 – 28,557,497
Java 9 int-to-String performance
Java 9 int-to-String performance

Java 9 makes it interesting: All variants have significantly increased throughput (20 to 30%). However, the margin of variant four ("" + i) has dropped back to about 2%. To exclude measurement inaccuracies, I repeated the test several times.

Measurement results int-to-string on Java 11

I skip Java 10. Java 11 is the current LTS (Long Term Support) release. Java 9 was the first release after the new release cycle and came three and a half years after Java 8, so I included it.

MethodOperations per secondConfidence ​interval (99.9%)
Integer.​toString(i)27,755,91427,537,830 – 27,973,998
String.​valueOf(i)27,836,73527,676,576 – 27,996,894
String.​format("%d", i)2,717,5512,602,165 – 2,832,937
"" + i28,237,06627,965,904 – 28,508,227
Java 11 int-to-String performance
Java 11 int-to-String performance

There were no significant changes from Java 9 to Java 11. I attribute the minimal fluctuations to measurement inaccuracies.

Measurement results int-to-string on Java 13

I also skip Java 12 and come directly to the current release, Java 13.

MethodOperations per secondConfidence ​interval (99.9%)
Integer.​toString(i)27,664,97627,591,996 – 27,737,955
String.​valueOf(i)27,718,09627,646,080 – 27,790,112
String.​format("%d", i)1,800,3451,763,017 – 1,837,672
"" + i28,293,22828,156,241 – 28,430,215
Java 13 int-to-String performance
Java 13 int-to-String performance

The variants one, two, and four are virtually unchanged; variant four is still the front runner. Something interesting happened to variant three (String.format("%d", i)): compared to Java 11 it is 34% slower.

Measurement results int-to-string on Java 14

For the sake of completeness, I also tested the latest early access build of Java 14 (ea+19).

MethodOperations per secondConfidence ​interval (99.9%)
Integer.​toString(i)27,642,63027,484,253 – 27,801,007
String.​valueOf(i)27,571,93827,456,427 – 27,687,448
String.​format("%d", i)1,828,3821,780,958 – 1,875,807
"" + i28,226,17528,030,308 – 28,422,042
Java 14 int-to-String performance
Java 14 int-to-String performance

Here we see almost the same result as with Java 13. "" + i is still the fastest way. The 2% lead over the first two variants has proven to be stable in the last four Java versions measured so that we can rule out measurement uncertainty.

Measurement result overview of all Java versions

Here you can see all measurement results summarized in one diagram:

Java int-to-String performance
Java int-to-String performance

We can summarize – regardless of the Java version:


… whereby the margin up to Java 8 with almost 15% was clearly more significant than since Java 9 with about 2%.

Analysis of performance differences

The measurements have raised the following questions, which I would like to clarify in this section:

  • Why have all variants significantly increased in speed in Java 9?
  • Why does "" + i perform best throughout all Java versions?
  • Why has String.format(i) become so slow in Java 13?

I will, therefore, analyze the JDK source code and the generated byte code.

Why have all variants significantly increased in speed in Java 9?

My first guess was that “Compact Strings“, which are enabled by default in Java 9, are responsible for the increased performance. However, disabling them (VM option “-XX:-CompactStrings”) did not result in any relevant speed change.

My second approach was to compare the Java source code of the Integer.toString(i) methods of Java 8 and Java 9. While the Java 9 code is easy to understand, the Java 8 code looks pretty cryptic and optimized. To see if it’s this code change (and not JVM optimizations), I extracted the Java 8 source code from Integer, copied it into a class Integer8 under Java 9, and repeated the benchmark test with it. And indeed: The Integer8.toString(i)-Method was also much slower under Java 9, but a bit faster than under Java 8. So the main reason for the performance gain lies in code improvements, and besides, there are some JVM optimizations.

MethodOperations per secondConfidence ​interval (99.9%)
Java 8 code on Java 820,939,91020,699,671 – 21,180,149
Java 8 code on Java 921,737,98121,517,415 – 21,958,547
Java 9 code on Java 928,025,70027,829,430 – 28,221,969
Performance of the Integer.toString() method on Java 8 and Java 9
Performance of the Integer.toString() method on Java 8 and Java 9

I didn’t put the corresponding source code in my GitLab repository, because I’m not sure to what extent I can publish code from the JDK or even parts of it.

I have not further investigated variants three and four at this point. I assume that also, their algorithms have been massively improved.

Why does "" + i perform best throughout all Java versions?

To find out why "" + i is the fastest, let’s first look at the generated byte code. We do this as follows:

We create a file IntToStringFast.java with the following content.

package eu.happycoders.int2string;

import java.util.Random;

public class IntToStringFast {
    public static void main(String[] args) {
        int i = new Random().nextInt();
        System.out.println("" + i);
    }
}

We compile the file as follows:

javac IntToStringFast.java

And look at the byte code with the following command:

javap -c IntToStringFast.class

Variant "" + i on Java 7 and Java 8

With Java 7 and Java 8 the following byte code is generated (here only the relevant excerpt):

14: new           #6                  // class java/lang/StringBuilder
17: dup
18: invokespecial #7                  // Method java/lang/StringBuilder."":()V
21: ldc           #8                  // String
23: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: iload_1
27: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
30: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

The bytecode corresponds to the following Java code (interestingly append("") was not eliminated – this task is apparently left to the HotSpot compiler):

new StringBuilder().append("").append(i).toString();

To confirm this assumption, I repeat the benchmark with the StringBuilder.append(i) code and come to the same result as for "" + i.

Looking at the AbstractStringBuilder.append(int i) method, we realize that in both Java 7 and Java 8, it contains virtually the same code as Integer.toString(int i). The only difference I could see is that StringBuilder internally first creates a char array of length 16, which is then copied in the toString() method via System.arraycopy() into an array of the required length; while Integer.toString() creates a char array of the final required length from the start. To check if this makes a difference, I create another test in which I pass 7 as capacity when creating the StringBuilder (only 7-digit random numbers are generated in the test). However, this also leads to the same result.

This is a short interim result of my current test:

Benchmark                                                        Mode  Cnt         Score   Error  Units
IntToStringBenchmarkStringBuilder.integerToString               thrpt    2  21168090.044          ops/s
IntToStringBenchmarkStringBuilder.stringBuilderCapacity7        thrpt    2  23968649.108          ops/s
IntToStringBenchmarkStringBuilder.stringBuilderCapacityDefault  thrpt    2  23769306.792          ops/s
IntToStringBenchmarkStringBuilder.stringPlus                    thrpt    2  23989334.180          ops/s

All StringBuilder variations are almost equally fast and, still, significantly faster than Integer.toString(). To get to the bottom of this, I copy the source code from both Integer and StringBuilder and run the tests again. I get the following result (the benchmarks with the “8” are the ones with the copied source code):

Benchmark                                                  Mode  Cnt         Score   Error  Units
IntToStringBenchmarkStringBuilderInline.integerToString   thrpt    2  20518228.534          ops/s
IntToStringBenchmarkStringBuilderInline.integer8ToString  thrpt    2  19681140.450          ops/s
IntToStringBenchmarkStringBuilderInline.stringBuilder     thrpt    2  23873235.183          ops/s
IntToStringBenchmarkStringBuilderInline.stringBuilder8    thrpt    2  19990576.858          ops/s 

Interesting: In the copied source code, Integer.toString(i) and "" + i are almost equally fast – as I would have expected after viewing the source code. However, both copied classes are slower than the classes from the JDK. So what does this mean?

Do the compiled classes in the JDK not match the source code provided? To check this, I remove the file src.zip from the JDK directory so that IntelliJ does not display the source code when clicking on a class, but decompiles the class. The decompiled class looks exactly like the source code (except that local variable names are generic).

At this point, I stop the analysis for Java 7 and Java 8 for the time being. You could now use the VM options -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining to see how HotSpot optimizes the code in detail, but I don’t have enough time to do that.

Variante "" + i since Java 9

Since Java 9, different byte code is generated from "" + i – only a single line:

15: invokedynamic #6,  0              // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;

The makeConcatWithConstants() method “facilitate the creation of String concatenation methods, that can be used to efficiently concatenate a known number of arguments of known types” (see StringConcatFactory’s JavaDoc). I looked at the source code of StringConcatFactory.makeConcatWithConstants(). Eventually, it also invokes StringBuilder.append(int) using a MethodHandle. Therefore also here I cannot see why this variant is faster than Integer.toString(). Again, sophisticated HotSpot optimizations seem to be at work.

Why has String.format(i) become so slow in Java 13?

The method String.format() invokes – in both Java 11 and Java 13 -Formatter().format(format, args).toString(). To check if it’s the int-to-String conversion or the formatter in general, I do a test with the format string “%s”. As parameter, I pass a random number again, which I already convert into a String in the “state” and pass it as such to the test method (you can also find this test in the GitLab repository).

Java versionOperations per secondConfidence ​interval (99.9%)
Java 112,978,2412,377,067 – 3,579,416
Java 131,924,1831,624,398 – 2,223,968
Performance der String.format()-Method unter Java 11 und Java 13
Performance of the String.format() Method on Java 11 and Java 13

So the formatter has generally become much slower, not only when converting integers to Strings. Unfortunately, I don’t have the time to analyze further details here. This article has already become much longer than originally planned.

Summary

Extensive benchmark tests have shown that across all Java versions, "" + i is the fastest way to convert an integer to a String. While the margin for Java 7 and Java 8 was still an impressive 15%, it has dropped to about 2% since Java 9, so today it’s basically a matter of taste which variant you use.

Unfortunately, I did not succeed in finding out the underlying reason. Do you know the cause? Or do you know other performant ways to convert ints into Strings? Then I look forward to your comment!

In the next article, I’ll show you what to consider in the opposite direction, i.e. when converting Strings into ints.

Leave a Comment

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