1

I'm attempting to create an Android app which graphs simple mathematical functions that the user inputs (essentially a graphing calculator). Every onDraw call requires hundreds of arithmetic evaluations per second (which are plotted on screen to produce the graph). When my code evaluates the expression the program slows down considerably, when the inbuilt methods evaluate the expression, the app runs with no issue.

According to 'LogCat', garbage collection occurs about 12 times per second, each time pausing the app for roughly 15 milliseconds, resulting in a few hundred milliseconds worth of freezes every second. I think this is the problem.

Here is a distilled version of my evaluator function. The expression to be evaluated is named "postfixEquation", the String ArrayList "list" holds the final answer at the end of the process. There are also two String arrays titled "digits" and "operators" which store the numbers and signs which are able to be used:

String evaluate(String[] postfixEquation) {

    list.clear();

    for (int i = 0; i < postfixEquation.length; i++) {

        symbol = postfixEquation[i];

        // If the first character of our symbol is a digit, our symbol is a numeral
        if (Arrays.asList(digits).contains(Character.toString(symbol.charAt(0)))) {

            list.add(symbol);

            } else if (Arrays.asList(operators).contains(symbol))  {

                // There must be at least 2 numerals to operate on
                if (list.size() < 2) {
                    return "Error, Incorrect operator usage.";
                }

                // Operates on the top two numerals of the list, then removes them 
                // Adds the answer of the operation to the list
                firstItem = Double.parseDouble(list.get(list.size() - 1));
                secondItem = Double.parseDouble(list.get(list.size() - 2));
                list.remove(list.size() - 1);
                list.remove(list.size() - 1);

                if (symbol.equals(operators[0])){ 
                    list.add( Double.toString(secondItem - firstItem) );  
                } else if (symbol.equals(operators[1])) {
                    list.add( Double.toString(secondItem + firstItem) );
                } else if (symbol.equals(operators[2])) {
                    list.add( Double.toString(secondItem * firstItem) );
                } else if (symbol.equals(operators[3])) {
                    if (firstItem != 0) {
                        list.add( Double.toString(secondItem / firstItem) );
                    } else {
                        return "Error, Dividing by 0 is undefined.";
                    }
                } else {
                    return "Error, Unknown symbol '" + symbol + "'.";
                }
            }
        }

    // The list should contain a single item, the final answer 
    if (list.size() != 1) {
        return "Error, " + list has " + list.size() + " items left instead of 1.";
    }

    // All is fine, return the final answer
    return list.get(0);
}

The numerals used in the operations are all Strings, as I was unsure if it was possible to hold multiple types within one array (i.e. Strings and Doubles), hence the rampant "Double.parseDouble" and "Double.toString" calls.

How would I go about reducing the amount of garbage collection that occurs here?

If it's of any help, I have been using these steps to evaluate my postfix expression: http://scriptasylum.com/tutorials/infix_postfix/algorithms/postfix-evaluation/index.htm. I have been unable to get past this issue for weeks and weeks. Any help would be appreciated. Thanks.

Jaween
  • 374
  • 1
  • 3
  • 10

4 Answers4

3

The rule for tight loops in Java is don't allocate anything. The fact that you're seeing such frequent GC collections is proof of this.

You appear to be doing calculations with Double, then converting to a String. Don't do that, it's terrible for performance because you create tons and tons of strings then throw them out (plus you are converting back and forth between strings and doubles a lot). Just maintain an ArrayDeque<Double> and use it as a stack -- this also saves you from doing the array resizes that are probably also killing performance.

Precompile the input equations. Convert all the input operations to enum instances -- they are faster to compare (just takes a switch statement), and may even use less memory. If you need to handle doubles, either use a generic Object container and instanceof, or a container class that contains both an operation enum and a double. Precompiling saves you from having to do expensive tests in your tight loop.

If you do these things, your loop should positively fly.

nneonneo
  • 171,345
  • 36
  • 312
  • 383
  • Thanks for your suggestions! I followed your instructions and it has reduced the number of garbage collections from 12 per second to about 2! Thank you so much! I used an `ArrayDeque`, an `Object` array to hold my numbers and operators, and used `enum`s to represent the operators. I was already precompiling the equations. Thank you very much nneonneo! – Jaween Sep 28 '12 at 11:36
0

Probably your list manipulation is the source of this problem. Lists internally have arrays, which are expanded/shrunk depending on how much data is on the list. So doing lots of add and removes randomly will heavily require garbage collection.

A solution to avoid this is using the right List implementation for your problem, allocate enough space to the list at the beginning to avoid resizing the internal array and to mark unused elements instead of removing them

The freezing symptoms are because you're doing your calculations in your UIThread. If you don't want your app to freeze, you might want to check AsyncTask to do calculations on a separate thread.

PS: also looks like you're doing some useless operations in there... why parseDouble() secondItem?

Community
  • 1
  • 1
m0skit0
  • 25,268
  • 11
  • 79
  • 127
  • Yes, I noticed that the resizing of Lists was causing an issue and I had actually tried something similar in which I replaced my List with an `Object[]` filled with null values, and an `int` index indicator. It didn't seem to have much effect though and so I gave up. I will look into `AsyncTask`, but at the moment, nneonneo's answer fixed most of my issues. Thank you very much m0skit0! – Jaween Sep 28 '12 at 11:50
  • I have a followup question. During each loop my program does calculations and subsequently draws/plots them, all within my UIThread, however if I were to use `AsyncTask` and do the calculations on a separate thread as you suggested, wouldn't I need to create a new object every time it looped to run the AsyncTask, defeating the purpose? According to the documentation "The task can be executed only once". I was able to see a single frame drawn but then an exception was thrown "Cannot execute the task: the task has already been executed (a task can be executed only once)". – Jaween Oct 01 '12 at 03:54
  • Really this is a completely different question. I suggest you creating a new one for this. – m0skit0 Oct 01 '12 at 09:41
0

The 15ms pauses are not occurring in your UI thread, so they should not be affecting performance to much. If your UI is pausing while your method is executing, consider running it on another thread (with AsyncTask)

To reduce your garbage collection you need to reduce the amount of memory allocated within the loop.

I would suggest looking at:

  1. Performing the Arrays.asList functions outside the loop (ideally somewhere that is only executed once such as your constructor or a static constructor)
  2. If your list is a LinkedList, consider changing it to an ArrayList
  3. If your List is an ArrayList, make sure you initialise it with enough capacity so it won't need to be resized
  4. Consider making your List store Objects rather than Strings, then you can store both your symbols and Doubles in it, and don't need to convert back and forward from Double to String as much
  5. Consider writing a proper parser (but this is a'lot more work)
Rob
  • 4,733
  • 3
  • 32
  • 29
  • It appears that number 4 was the largest cause for the garbage collection issues. Continuously converting between `String`s and `Double`s and the resizing of my Lists was causing trouble. However, could you elaborate on what you mean by "writing a proper parser"? Thank you! – Jaween Sep 28 '12 at 11:56
0

However, you are using a lot of strings. While this may not be the case, it's always one of those things you can check out because Java does funky stuff with String. If you are having to convert the string to double as you are outputting, then there's quite a bit of overhead going on.

Do you need to store the data as String? (Note that the answer may actually be yes) Heavy use of temporary strings can actually cause the garbage collector to get fired off often.

Be careful about premature optimization. Profilers and running through the function line-by-line can help

Joe Plante
  • 6,308
  • 2
  • 30
  • 23
  • I think you're right about the overhead associated with converting between Strings and Doubles. The reason I had to store the numbers as Strings was because I was unaware of how to use the type `Object` and how to have my program check the type of data it is handling, nneonneo suggested `instanceof`, which appears to have worked. What do you mean by "premature optimization"? Have I been doing that? Thank you for your help! – Jaween Sep 28 '12 at 12:03