0

I was just wondering about the efficency of the following two codes:

public void appendInput(String s, boolean isCommand) {

    s = "\n" + (isCommand == true ? ">" : "") + s + "\n";
    getConsoleInput().append(s);

    //vs

    getConsoleInput().append("\n" + (isCommand == true ? ">" : "") + s + "\n");

}

By my basic grasp of this language, would the second one be more efficent than the second variation because it does not create a new object (String)? Or does it create a new String when you contract two Strings? I think so. I just need someone who has a better grasp of Java than me.

So I decided to run some tests. This is what I came up with:

public class Main {

public static void main(String[] args) {
    String s;
    Boolean b;
    long startTime;
    long endTime;
    long[] results = new long[4];

    System.out.println("*ALL TESTS WILL BE LOOPED *1000");

    sep();

    System.out.println("METHOD 1;-- TEST 1 - STRING = \"TESTING \" && BOOLEAN = FALSE:");

    s = "TESTING";
    b = false;  // change variables

    startTime = System.currentTimeMillis(); //commence testing from here

    for(int i = 0; i < 1000; i ++) {
        s = "" + (b ? ">" : "") + s + "";
        System.out.print(s);
    }

    endTime = System.currentTimeMillis();
    results[0] = endTime - startTime;
    System.out.println("Total execution time: " + results[0] + "ms");

    sep();

    //Next test

    System.out.println("METHOD 1;-- TEST 2 - STRING = \"TESTING \" && BOOLEAN = TRUE:");

    s = "TESTING";
    b = false;

    startTime = System.currentTimeMillis();

    for(int i = 0; i < 1000; i ++) {
        s = "" + (b ? ">" : "") + s + "";
        System.out.print(s);
    }

    endTime = System.currentTimeMillis();
    results[1] = endTime - startTime;
    System.out.println("Total execution time: " + results[1] + "ms");
    sep();

    //Next test

    System.out.println("METHOD 2;-- TEST 1 - STRING = \"TESTING \" && BOOLEAN = FALSE:");

    s = "TESTING";
    b = false;  // change variables

    startTime = System.currentTimeMillis(); //commence testing from here

    for(int i = 0; i < 1000; i ++) {
        System.out.print("" + (b == true ? ">" : "") + s + "");
    }

    endTime = System.currentTimeMillis();
    results[2] = endTime - startTime;
    System.out.println("Total execution time: " + results[2] + "ms");
    sep();

    //Next test

    System.out.println("METHOD 2;-- TEST 2 - STRING = \"TESTING \" && BOOLEAN = TRUE:");

    s = "TESTING";
    b = false;

    startTime = System.currentTimeMillis();

    for(int i = 0; i < 1000; i ++) {
        System.out.print("" + (b == true ? ">" : "") + s + "");
    }

    endTime = System.currentTimeMillis();
    results[3] = endTime - startTime;
    System.out.println("Total execution time: " + results[3] + "ms");
    sep();

    System.out.println("RESULTS:");
    System.out.println("-----------------------");

    String[] typesOfTests = {"METHOD 1 BOOLEAN = FALSE", "METHOD 1 BOOLEAN = TRUE  ",
            "METHOD 2 BOOLEAN = FALSE", "METHOD 2 BOOLEAN = TRUE  "};

    for(int i = 0; i < typesOfTests.length && i < results.length; i++) {
        System.out.print(typesOfTests[i]);
        System.out.print("\t");
        System.out.println(results[i]);
    }
}

private static void sep() {
    System.out.println("====================================");
}
private void optOne(String s, boolean b) {
    s = "\n" + (b == true ? ">" : "") + s + "\n";
    System.out.println(s);
}

private void optTwo(String s, boolean b) {
    System.out.println("\n" + (b == true ? ">" : "") + s + "\n");

}

}

And here are some results...

ALL TESTS WILL BE LOOPED *1000
====================================
METHOD 1;-- TEST 1 - STRING = "TESTING " && BOOLEAN = FALSE:
TESTING *1000 ...
Total execution time: 25ms
====================================
METHOD 1;-- TEST 2 - STRING = "TESTING " && BOOLEAN = TRUE:
TESTING *1000 ...
Total execution time: 17ms
====================================
METHOD 2;-- TEST 1 - STRING = "TESTING " && BOOLEAN = FALSE:
TESTING *1000 ...
Total execution time: 4ms
====================================
METHOD 2;-- TEST 2 - STRING = "TESTING " && BOOLEAN = TRUE:
TESTING *1000 ...
Total execution time: 8ms
====================================
RESULTS:
-----------------------
METHOD 1 | BOOLEAN = FALSE  | 25
METHOD 1 | BOOLEAN = TRUE   | 17
METHOD 2 | BOOLEAN = FALSE  | 4
METHOD 2 | BOOLEAN = TRUE   | 8

and some others (OK, OK, table only!)

METHOD 1 | BOOLEAN = FALSE  | 19
METHOD 1 | BOOLEAN = TRUE   | 10
METHOD 2 | BOOLEAN = FALSE  | 5
METHOD 2 | BOOLEAN = TRUE   | 5

METHOD 1 | BOOLEAN = FALSE  | 18
METHOD 1 | BOOLEAN = TRUE   | 11
METHOD 2 | BOOLEAN = FALSE  | 5
METHOD 2 | BOOLEAN = TRUE   | 4

METHOD 1 | BOOLEAN = FALSE  | 20
METHOD 1 | BOOLEAN = TRUE   | 16
METHOD 2 | BOOLEAN = FALSE  | 6
METHOD 2 | BOOLEAN = TRUE   | 4

Conclusion:

Method 1 (s = "" + (b ? ">" : "") + s + ""; System.out.print(s);) is much faster than method 2 (System.out.print("" + (b == true ? ">" : "") + s + "");) as there is no need to create a new String object.

Another trend I have noticed is that the time increases by more than a third when the boolean is false, in both of the methods even though in method 1 it is more noticeable than in method 2. I have no idea why this is... Could anyone explain this to me?


TL;DR method 1 is slower as a new String object needs to be created. Furthermore, when the boolean is equal to false, time taken to execute methods increases in both cases. Could you explain this to me?

gvlasov
  • 18,638
  • 21
  • 74
  • 110
nyxaria
  • 479
  • 1
  • 3
  • 13
  • 2
    You're creating the same number of objects each time; the fact that you don't assign that object's reference to a variable (but instead use it directly) doesn't matter. Your test results are probably due to the nature of the test itself -- specifically, that Java optimizes as it runs (that's what the JIT does). Micro-benchmarks like this are notoriously difficult in Java. – yshavit Jan 23 '15 at 22:45
  • @yshavit oh. Thanks for the explanation, this truly is one wierd language...but beautiful at the same time! – nyxaria Jan 23 '15 at 22:47
  • Both versions create StringBuilder and String objects. There is no significant difference between them. In any case, start by making it easy to understand. – Alan Stokes Jan 23 '15 at 23:19
  • @AlanStokes sorry, I tried to make it as detailed as I could... I'll try make it more concise next time. – nyxaria Jan 23 '15 at 23:21
  • You can try playing around with [`javap`](http://docs.oracle.com/javase/7/docs/technotes/tools/windows/javap.html) to see what each bit of code is compiled into. That's always a good starting point. – biziclop Jan 23 '15 at 23:27
  • yshavit is correct in saying that it shouldn't matter if you assign the new object to a new variable. Similarly, the compiler should be clever enough to work out that `b == true` is the same as `b`. The only difference I can see between methods 1 and 2 that might be significant is that in method 1, `s` is the output as well as the input of the concatenation. Who knows? Maybe it runs method 2 as `if(b) System.out.print(">"); System.out.print(s);`, which isn't possible in method 1 if you consider that the new value of `s` will be needed in the next iteration. – David Knipe Jan 23 '15 at 23:51
  • @DavidKnipe Yes, that looks like a logical assumption. It might also do something with [this](http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array) or something related? Maybe the value `true` is the _default_, or the system has some sort of bias towards it. Probably not. This is for the small difference between the booleans I'm talking about. For the String it might be what you suggested, but I'm not a [_rapper_] computer scientist. – nyxaria Jan 23 '15 at 23:58
  • Probably more efficient: `getConsoleInput().append("\n").append(isCommand ? ">" : "").append(s).append("\n");` – xehpuk Jan 24 '15 at 00:03
  • `this` isn't mentioned at all in the code, and anyway you only run static methods, so I doubt it's significant. I'd be surprised if `true` was a default, and in fact you've never actually run it with `true` :-) Maybe it's slower the first time it runs because of JIT compilation or something similar, like it need to build up statistics for branch prediction. For this to happen, the compiler would have to notice the similarity between the two method 1 loops, and the similarity between the method 2 loops. Which it might plausibly notice. – David Knipe Jan 24 '15 at 00:14
  • @DavidKnipe ... I feel like such a pleb. That sounds plausible. Thanks for the explanation :D. – nyxaria Jan 24 '15 at 00:21

1 Answers1

1

Writing a benchmark "with your bare hands" is mosty useless because of how JVM works. You can get greatly varying results. The way to go is to run your code a lot of times when your bytecode is properly optimized by JVM, which happens after running that code a while. Which is what JMH framework is about. Always use it if you need to properly benchmark code, especially for microbenchmarks such as yours.

So I put up some tests myself (just look at how much simpler the code becomes with JMH):

package org.sample;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.infra.Blackhole;

public class StringsAppendBenchmark {
    private static String TESTING = "TESTING";

    @Benchmark
    @Fork(3)
    public void withVariable(Blackhole bh) {
        String s = "\n" + ">" + TESTING + "\n";
        bh.consume(s);
    }

    @Benchmark
    @Fork(3)
    public void withoutVariable(Blackhole bh) {
        bh.consume( "\n" + ">" + TESTING + "\n" );
    }
}

Now, using System.out.println() for benchmarking purposes is not quite useful, because the resulting performance heavily depends on what is reading your application's stdout. Writing to /dev/null can be negligible time-wise, while writing to IDE console may quickly use up all your memory and leave you waiting for the system to swap out. That's why Blackhole is used, which just accepts an argument an does nothing with it, for that argument not to be optimized into nonexistence by runtime bytecode optimizations in JVM.

Running the benchmark outputs:

Benchmark                                      Mode  Samples         Score         Error  Units
o.s.StringsAppendBenchmark.withVariable       thrpt       60  37105629.065 ± 1124455.355  ops/s
o.s.StringsAppendBenchmark.withoutVariable    thrpt       60  38059994.264 ± 1021414.039  ops/s

The results and the margin of error show us that it doesn't matter whether you store it in a variable beforehand or not. Just as expected, to be honest.

gvlasov
  • 18,638
  • 21
  • 74
  • 110
  • Thanks! Nice API, will use in the future~~~ was wondering why this was- and it turns out there was no difference :-P. – nyxaria Feb 01 '15 at 01:28
  • 1
    It is worth mentioning, you don't need an explicit `Blackhole` here, you may as well return the result from `@Benchmark`. – Aleksey Shipilev Feb 02 '15 at 06:56