Why is the “new” keyword so much more efficient than assignment?

后端 未结 2 721
感情败类
感情败类 2020-12-14 21:17

I\'ve got two methods to read in a string, and create Character objects:

static void newChar(String string) {
    int len = string.length();
    System.out.p         


        
2条回答
  •  借酒劲吻你
    2020-12-14 21:36

    TL;DR section

    Good news

    Your measurement does expose a real effect.

    Bad news

    It does so mostly by chance because your benchmark has many technical flaws, and the effect it exposes is probably not the one you have in mind.

    The new Character() approach is faster if and only if HotSpot's Escape Analysis succeeds in proving that the resulting instance can be safely allocated on the stack instead of heap. Therefore the effect is not nearly as general as implied in your question.

    Explanation of effect

    The reason why new Character() is faster is locality of reference: your instance is on the stack and all access to it is via CPU cache hits. When you reuse a cached instance, you must

    1. access a remote static field;
    2. dereference it into a remote array;
    3. dereference an array entry into a remote Character instance;
    4. acces the char contained in that instance.

    Each dereference is a potential CPU cache miss. Furthermore, it forces a part of the cache to be redirected towards those remote locations, causing more cache misses on the input string and/or the stack locations.

    DETAILS

    I have run this code with jmh:

    @OutputTimeUnit(TimeUnit.MICROSECONDS)
    @BenchmarkMode(Mode.AverageTime)
    public class Chars {
      static String string = "12345678901234567890"; static {
        for (int i = 0; i < 10; i++) string += string;
      }
    
      @GenerateMicroBenchmark
      public void newChar() {
        int len = string.length();
        for (int i = 0; i < len; i++) new Character(string.charAt(i));
      }
    
      @GenerateMicroBenchmark
      public void justChar() {
        int len = string.length();
        for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
      }
    }
    

    This keeps the essence of your code, but eliminates some systematic errors like warmup and compilation times. These are the results:

    Benchmark              Mode Thr    Cnt  Sec         Mean   Mean error    Units
    o.s.Chars.justChar     avgt   1      3    5       39.062        6.587  usec/op
    o.s.Chars.newChar      avgt   1      3    5       19.114        0.653  usec/op
    

    And this would be my best guess at what's going on:

    • in newChar you are creating a fresh instance of Character. HotSpot's Escape Analysis can prove the instance never escapes, therefore it allows stack allocation, or, in the special case of Character, could eliminate the allocation altogether because the data from it is provably never used;

    • in justChar you involve lookup into the Character cache array, which has some cost.

    UPDATE

    In response to Aleks's criticism, I added some more methods to the benchmark. The main effect remains stable, but we get even more fine-grained details about the lesser optimization effects.

      @GenerateMicroBenchmark
      public int newCharUsed() {
        int len = string.length(), sum = 0;
        for (int i = 0; i < len; i++) sum += new Character(string.charAt(i));
        return sum;
      }
    
      @GenerateMicroBenchmark
      public int justCharUsed() {
        int len = string.length(), sum = 0;
        for (int i = 0; i < len; i++) sum += Character.valueOf(string.charAt(i));
        return sum;
      }
    
      @GenerateMicroBenchmark
      public void newChar() {
        int len = string.length();
        for (int i = 0; i < len; i++) new Character(string.charAt(i));
      }
    
      @GenerateMicroBenchmark
      public void justChar() {
        int len = string.length();
        for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
      }
    
      @GenerateMicroBenchmark
      public void newCharValue() {
        int len = string.length();
        for (int i = 0; i < len; i++) new Character(string.charAt(i)).charValue();
      }
    
      @GenerateMicroBenchmark
      public void justCharValue() {
        int len = string.length();
        for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i)).charValue();
      }
    

    DESCRIPTION:

    • the base versions are justChar and newChar;
    • ...Value methods add the charValue call to the base version;
    • ...Used methods add both the charValue call (implicitly) and use the value to preclude any Dead Code Elimination.

    RESULTS:

    Benchmark                   Mode Thr    Cnt  Sec         Mean   Mean error    Units
    o.s.Chars.justChar          avgt   1      3    1      246.847        5.969  usec/op
    o.s.Chars.justCharUsed      avgt   1      3    1      370.031       26.057  usec/op
    o.s.Chars.justCharValue     avgt   1      3    1      296.342       60.705  usec/op
    o.s.Chars.newChar           avgt   1      3    1      123.302       10.596  usec/op
    o.s.Chars.newCharUsed       avgt   1      3    1      172.721        9.055  usec/op
    o.s.Chars.newCharValue      avgt   1      3    1      123.040        5.095  usec/op
    
    • there is evidence of some Dead Code Elimination (DCE) both in justChar and newChar variants, but it is only partial;
    • with newChar variant, adding charValue has no effect so apparently it was DCE'd;
    • with justChar, charValue does have an effect, so seems not to have been eliminated;
    • DCE has a minor overall effect, as witnessed by the stable difference between newCharUsed and justCharUsed.

提交回复
热议问题