JIT - micro optimization - if statement elimination

Haasip Satang :

Let's assume we have the following code:

public static void check() {   
    if (Config.initialized) {
           ...
    }
}

Config.initialized is false at the beginning and only changes to true at some point after the method has been JIT compiled already. The value never goes back to false.

I "know" there are a lot of very sophisticated optimizations ongoing (loop-unrolling, branch prediction, inlining, escape analysis, etc) and although I'm far away from understanding all of them in detail I'm mainly interested in the following for now:

  1. Does the JIT compiler have a way to detect that the if will always be true after a certain point in time so that the check could be skipped completely? By completely I really mean no variable access, no condition check / jne, etc...

  2. In case there is no way for JIT to get rid of the (from a certain point onwards) unnecessary check from the sample (and I would not know how it could) is there anything I could do to support that? My only idea is to retransform the class and to remove the unnecessary code from the byte code after the initialization event took place.

I am aware that this is total micro optimization and probably hard to meassure even using tools like JMH, but I would still like to know and understand.

Last but not least:

  1. Is my understanding correct that if the above method got inlined somewhere that all those methods would be recompiled (assuming they are hot) in case something changes so that the check method needs to be recompiled?

If I understand the results of my JitWatch test correctly the answer to the above questions should be:

  1. No, no way. There will always be a condition check.
  2. Really only by transformation
  3. Yes
apangin :
  1. Does the JIT compiler have a way to detect that the if will always be true after a certain point

Yes, if the field is static final, and its holder class has been initialized by the time JIT compiler kicks in. Apparently this is not applicable in your case, since Config.initialized cannot be made static final.

  1. is there anything I could do to support that?

java.lang.invoke.MutableCallSite to the rescue.

This class is designed specifically for doing things you ask. Its setTarget method supports rebinding the call site in runtime. Under the hood it causes deoptimization of currently compiled method with the possibility to recompile it later with new target.

A MethodHandle for calling the MutableCallSite target can be obtained with dynamicInvoker method. Note that MethodHandle should be static final in order to allow inlining.

  1. if the above method got inlined somewhere that all those methods would be recompiled

Yes.

Here is a benchmark demonstrating that mutableCallSite method is as fast as alwaysFalse at the beginning, and also as fast as alwaysTrue after switching the toggle. I also included a static field toggle for comparison as @Holger suggested.

package bench;

import org.openjdk.jmh.annotations.*;
import java.lang.invoke.*;
import java.util.concurrent.*;

@State(Scope.Benchmark)
public class Toggle {
    static boolean toggleField = false;

    static final MutableCallSite toggleCallSite =
            new MutableCallSite(MethodHandles.constant(boolean.class, false));

    static final MethodHandle toggleMH = toggleCallSite.dynamicInvoker();

    public void switchToggle() {
        toggleField = true;
        toggleCallSite.setTarget(MethodHandles.constant(boolean.class, true));
        MutableCallSite.syncAll(new MutableCallSite[]{toggleCallSite});
        System.out.print("*** Toggle switched *** ");
    }

    @Setup
    public void init() {
        ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
        executor.schedule(this::switchToggle, 10100, TimeUnit.MILLISECONDS);
        executor.shutdown();
    }

    @Benchmark
    public int alwaysFalse() {
        return 0;
    }

    @Benchmark
    public int alwaysTrue() {
        return ThreadLocalRandom.current().nextInt();
    }

    @Benchmark
    public int field() {
        if (toggleField) {
            return ThreadLocalRandom.current().nextInt();
        } else {
            return 0;
        }
    }

    @Benchmark
    public int mutableCallSite() throws Throwable {
        if ((boolean) toggleMH.invokeExact()) {
            return ThreadLocalRandom.current().nextInt();
        } else {
            return 0;
        }
    }
}

Running the benchmark with 5 warmup iterations and 10 measurement iterations I get the following results:

# JMH version: 1.20
# VM version: JDK 1.8.0_192, VM 25.192-b12

# Benchmark: bench.Toggle.alwaysFalse

# Run progress: 0,00% complete, ETA 00:01:00
# Fork: 1 of 1
# Warmup Iteration   1: 3,875 ns/op
# Warmup Iteration   2: 3,369 ns/op
# Warmup Iteration   3: 2,699 ns/op
# Warmup Iteration   4: 2,696 ns/op
# Warmup Iteration   5: 2,703 ns/op
Iteration   1: 2,697 ns/op
Iteration   2: 2,696 ns/op
Iteration   3: 2,696 ns/op
Iteration   4: 2,706 ns/op
Iteration   5: *** Toggle switched *** 2,698 ns/op
Iteration   6: 2,698 ns/op
Iteration   7: 2,692 ns/op
Iteration   8: 2,707 ns/op
Iteration   9: 2,712 ns/op
Iteration  10: 2,702 ns/op


# Benchmark: bench.Toggle.alwaysTrue

# Run progress: 25,00% complete, ETA 00:00:48
# Fork: 1 of 1
# Warmup Iteration   1: 5,159 ns/op
# Warmup Iteration   2: 5,198 ns/op
# Warmup Iteration   3: 4,314 ns/op
# Warmup Iteration   4: 4,321 ns/op
# Warmup Iteration   5: 4,306 ns/op
Iteration   1: 4,306 ns/op
Iteration   2: 4,310 ns/op
Iteration   3: 4,297 ns/op
Iteration   4: 4,324 ns/op
Iteration   5: *** Toggle switched *** 4,356 ns/op
Iteration   6: 4,300 ns/op
Iteration   7: 4,310 ns/op
Iteration   8: 4,290 ns/op
Iteration   9: 4,297 ns/op
Iteration  10: 4,294 ns/op


# Benchmark: bench.Toggle.field

# Run progress: 50,00% complete, ETA 00:00:32
# Fork: 1 of 1
# Warmup Iteration   1: 3,596 ns/op
# Warmup Iteration   2: 3,429 ns/op
# Warmup Iteration   3: 2,973 ns/op
# Warmup Iteration   4: 2,937 ns/op
# Warmup Iteration   5: 2,934 ns/op
Iteration   1: 2,927 ns/op
Iteration   2: 2,928 ns/op
Iteration   3: 2,932 ns/op
Iteration   4: 2,929 ns/op
Iteration   5: *** Toggle switched *** 3,002 ns/op
Iteration   6: 4,887 ns/op
Iteration   7: 4,866 ns/op
Iteration   8: 4,877 ns/op
Iteration   9: 4,867 ns/op
Iteration  10: 4,877 ns/op


# Benchmark: bench.Toggle.mutableCallSite

# Run progress: 75,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration   1: 3,474 ns/op
# Warmup Iteration   2: 3,332 ns/op
# Warmup Iteration   3: 2,750 ns/op
# Warmup Iteration   4: 2,701 ns/op
# Warmup Iteration   5: 2,701 ns/op
Iteration   1: 2,697 ns/op
Iteration   2: 2,696 ns/op
Iteration   3: 2,699 ns/op
Iteration   4: 2,706 ns/op
Iteration   5: *** Toggle switched *** 2,771 ns/op
Iteration   6: 4,310 ns/op
Iteration   7: 4,306 ns/op
Iteration   8: 4,312 ns/op
Iteration   9: 4,317 ns/op
Iteration  10: 4,301 ns/op

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=89295&siteId=1