Showing posts with label instanceof java. Show all posts
Showing posts with label instanceof java. Show all posts

Tuesday, May 20, 2014

Efficiency of instanceof operator (revisited)

For the previous test, all conclusions had to be prefaced with a disclaimer because the test suite's configuration  made it difficult to accurately determine instanceof's efficiency relative to the alternatives (i.e. .getClass().equals()). The test suite's inability to generate average values for each test turned out to be the biggest stumbling block. This flaw in the test's design meant that the values gotten from each test varied, sometimes drastically, making it difficult to pin down an accurate relationship. With the new test suite it is possible to specify how many times each test should run. For example, to test every method fifty times at a hundred iterations, the following command can be used:

java PerformanceTest 50 100

Hence, the general command is:

java PerformanceTest [# of repetitions] [# of iterations]

Once the repetition value is supplied, the test suite repeats each test for the specified amount of time and then automatically generates the average values. Unsurprisingly, the average times for each test are more consistent than the individual values (which could have a 10-20 ms margin of error).

On my aging core i3 machine, testing each operation fifty times for one million iterations yielded the following times:

.getClass.getName().equals("A"): 48ms
.getClass().getName() == "A": 65ms
a.equals(Type.A): 48ms
a == Type.A: 58ms
a instanceof A: 58ms

Testing each operation 50 times for ten million iterations yielded the following times:

.getClass.getName().equals("A"): 441ms
.getClass().getName() == "A": 624ms
a.equals(Type.A): 451ms
a == Type.A: 536ms
a instanceof A: 539ms

(Note how increasing the # of iterations by ten increased the running times by ten as well)

With reliable data in hand, it's now reasonable to conclude that:
1. Trying to elucidate a class' identity using .getClass().getName() == will far and away take the longest amount of time.
2. Conversely, using .getClass.getName().equals("A") for the same purpose will take the least amount of time.
3. Using the equals method associated with the enum (i.e. a.equals(Type.A)), will result in a relatively short running time.
4. instanceof and a == Type.A are neither the slowest nor the fastest ways to determine class identity.

Representing the relative efficiency of all five options visually you get (from least efficient to most efficient):
.getClass().getName() == "A" > a instanceof A > a == Type.A > a.equals(Type.A) > .getClass.getName().equals("A")

Thus, the data favours a class hierarchy design in which every entity (i.e. operation, integer, function) is represented by its own class (Category A in the previous entry). From an efficiency standpoint, the current system is not the worst. However, letting a full fledged math library handle all of the ASTNode operations will result in a marginal increase in efficiency, which is the end-goal of this project.

Thursday, May 15, 2014

ASTNode Class Hierarchy Phase II and Efficiency Of instanceof Operator In Java


The charts for Phase II are roughly split between two categories. Category A consists of charts in which all distinct entities have been assigned to their own specific classes or interfaces. Charts in category B have classes which subsume other potential classes (similar to how ASTNode is currently structured). All the charts, including their descriptions, can be found below:

Category A
1A: (Fully Extended) All entities which could possibly have their own class or interface have them. No concessions have been made.
2A: (Semi-Fully Extended) All trigonometric functions extend ASTTrigonometricNode. All hyperbolic functions extend ASTHyperbolicNode.
3A: Similar to 2A, but all logical and relational operators (i.e. XOR, OR, ==, >=) extend the same class.

Category B
1B: (Conservative) None of the relational / logical operators have their own classes.
2B: (Ultra Conservative) None of the trigonometric functions have their own class. None of the operators relational / logical operators have their own class.

Generally speaking, the charts in category A tend to be larger than the charts in category B. However, whether or not the charts in category A are cleaner or easier to understand is a matter of personal opinion. If design aesthetics were the only factor to consider then the final decision would ultimately be an arbitrary one. Unfortunately, the consequences of choosing one option over another are not just restricted to offended sensibilities and smarting eyes. Tied to each design is a (suspected) impact on performance. Extending the class hierarchy to its utmost limits, as is done in most charts belonging to category A, results in diagrams with numerous classes wherein relationships are very clearly defined. For these charts it is expected that the instanceof operator will be excessively used (to determine a particular object's class).

The efficiency of the instanceof operator, especially as it relates to the library's use has not been confirmed. Googling the issue yields this excellent stack overflow thread which while enlightening yields many contradictory answers. A clearly defined answer to the question is therefore quite difficult to pin down. In order to have something to work with in the interim, I constructed a test that would enable me to compare the performance of the different methods:

import java.util.concurrent.TimeUnit;

public class PerformanceTest {
  
  private static enum Type {
    A,
    B,
    C,
  }
  
  public static void main(String[] args) {
    long x, y, equals_time = 0, instanceof_time = 0, relative_eq_time = 0, 
         getclass_time = 0, getclass_eq_time = 0, MAX = Long.parseLong(args[0]);
    A a = new A();
    int i, j, k, l, m;
    boolean bool;
    Type type = Type.C;

    // Time comparison using instanceof operator
    for (l = 0; l < MAX; l++) {
      x = System.nanoTime();
      bool = a instanceof A;
      y = System.nanoTime();
      instanceof_time += y - x;
    }

    // Time comparison using relative eq operator
    for (m = 0; m < MAX; m++) {
      x = System.nanoTime();
      bool = type == Type.C;
      y = System.nanoTime();
      relative_eq_time += y - x;
    }

    // Time comparison using .equals() (between the different enums)
    for (k = 0; k < MAX; k++) {
      x = System.nanoTime();
      bool = type.equals(Type.C);
      y = System.nanoTime();
      equals_time += y - x;
    }

    // Time comparison using .getClass().getName() ==
    for (i = 0; i < MAX; i++) {
      x = System.nanoTime();
      bool = a.getClass().getName() == "A";
      y = System.nanoTime();
      getclass_eq_time += y - x;
    }

    // Time comparison using .getClass().getName().equals()
    for (j = 0; j < MAX; j++) {
      x = System.nanoTime();
      bool = a.getClass().getName().equals("A");
      y = System.nanoTime();
      getclass_time += y - x;
    }
    
    String results = String.format(".getClass.getName().equals(\"A\"): %dms\n.getClass().getName() == \"A\": %dms\na.equals(Type.A): %dms\na == Type.A: %dms\na instanceof A: %dms",                               TimeUnit.NANOSECONDS.toMillis(getclass_time), 
                                   TimeUnit.NANOSECONDS.toMillis(getclass_eq_time), 
                                   TimeUnit.NANOSECONDS.toMillis(equals_time),
                                   TimeUnit.NANOSECONDS.toMillis(relative_eq_time),
                                   TimeUnit.NANOSECONDS.toMillis(instanceof_time));
    
    System.out.println(results);
  }
  
}

This is how the times break down grosso modo (they are not always consistent):

.getClass.getName().equals("A"): 87ms
.getClass().getName() == "A": 38ms
a.equals(Type.A): 46ms
a == Type.A: 55ms
a instanceof A: 56ms

Through the above test it is evident that identifying a class' identity with .getClass.getName().equals() exacts the greatest cost to the CPU. Conversely, the least expensive approach is to use the equals method associated with the object's type enum. The performance of the instanceof and == operator sits somewhere in between the two extremes. It should be noted that switching the relative positioning of the different subroutines causes a change in results. For example, reversing the positioning of the functions results in an almost complete reversal of the ranking. This code for instance ...

import java.util.concurrent.TimeUnit;

public class PerformanceTest2 {
  
  private static enum Type {
    A,
    B,
    C,
  }

  private static Type type = Type.C;

  public static void main(String[] args) {
    long x, y, equals_time = 0, instanceof_time = 0, relative_eq_time = 0, 
         getclass_time = 0, getclass_eq_time = 0, MAX = Long.parseLong(args[0]);
    A a = new A();
    int i, j, k, l, m;
    boolean bool;

    // Time comparison using .getClass().getName().equals()
    for (j = 0; j < MAX; j++) {
      x = System.nanoTime();
      bool = a.getClass().getName().equals("A");
      y = System.nanoTime();
      getclass_time += y - x;
    }

    // Time comparison using .getClass().getName() ==
    for (i = 0; i < MAX; i++) {
      x = System.nanoTime();
      bool = a.getClass().getName() == "A";
      y = System.nanoTime();
      getclass_eq_time += y - x;
    }

    // Time comparison using .equals() (between the different enums)
    for (k = 0; k < MAX; k++) {
      x = System.nanoTime();
      bool = type.equals(Type.C);
      y = System.nanoTime();
      equals_time += y - x;
    }

    // Time comparison using .equals() (between the different enums)
    for (k = 0; k < MAX; k++) {
      x = System.nanoTime();
      bool = type.equals(Type.C);
      y = System.nanoTime();
      equals_time += y - x;
    }

    // Time comparison using relative eq operator
    for (m = 0; m < MAX; m++) {
      x = System.nanoTime();
      bool = type == Type.C;
      y = System.nanoTime();
      relative_eq_time += y - x;
    }

    // Time comparison using instanceof operator
    for (l = 0; l < MAX; l++) {
      x = System.nanoTime();
      bool = a instanceof A;
      y = System.nanoTime();
      instanceof_time += y - x;
    }

    String results = String.format(".getClass.getName().equals(\"A\"): %dms\n.getClass().getName() == \"A\": %dms\na.equals(Type.A): %dms\na == Type.A: %dms\na instanceof A: %dms",                               TimeUnit.NANOSECONDS.toMillis(getclass_time), 
                                   TimeUnit.NANOSECONDS.toMillis(getclass_eq_time), 
                                   TimeUnit.NANOSECONDS.toMillis(equals_time),
                                   TimeUnit.NANOSECONDS.toMillis(relative_eq_time),
                                   TimeUnit.NANOSECONDS.toMillis(instanceof_time));
    
    System.out.println(results);
  }
  
}

... yields the following times (on average of course):

.getClass.getName().equals("A"): 47ms
.getClass().getName() == "A": 81ms
a.equals(Type.A): 52ms
a == Type.A: 56ms
a instanceof A: 72ms

Does anybody know what could be causing this?

Overall, these results suggest a variety of things. It is possible that the above approach to assessing efficiency is inherently flawed and that it is unlikely to yield reliable or consistent results. If the test itself is not flawed, then it is likely that performance is being governed by factors that are not under our immediate control. Finally, another explanation for the results could be that at the JVM level the operators take the same amount of time to evaluate and that the variation seen is just what's expected and normal.

While the above tests are inconsistent and not particularly rigorous they at least demonstrate that instanceof's performance is roughly comparable to that of the other operators. Using one operation over another does not result in a significant difference in performance time(on the order of seconds). Thus, we should not allow optimitization to influence our design decisions (in this case Knuth's overused 'premature optimization is the root of all evil' aphorism actually applies). Given that the class hierarchy diagrams in category A respect OOP principles more and have more well defined relationships it is recommended that they be used as a starting point.