Friday 9 May 2014

Reflection

G'day:
No I'm not being philosophically introspective; I mean the Java kind of reflection. Recently there's been some yipyap about the place regarding both Railo (RAILO-3001) and ColdFusion (3753710) steamrolling over perfectly good Java object methods with member functions of their own which work differently (and less well than their Java counterparts). Both these happenstances have broken FW/1, and have made Sean grumpy. Yikes.

But I don't care about that for the purposes of this article (no diss', Sean: I think you've got it covered anyhow ;-).


One thing that was trotted out amongst the rest of the back and forth was that when calling Java methods on ColdFusion objects, out method is not called directly, it's called via reflection, and reflection is bad. It's bad because it's non-performant. This comes up on the Railo Google Group too, occasionally... someone's using a Java method call in their CFML code, and someone comes in with "Reflection! Bad! Slow!"

Recently I had a need to use reflection in some actually Java code, and needed to RTFM to find out how to (I suck at Java, which we'll see in a minute), and a lot of the commentary on StackOverflow was "Reflection! Bad! Slow!"

So given I've now heard this from two quarters in the space of one week, I've decided to have a look.

Here's some test code.

include "/scribble/shared/util/stopwatch.cfm";

param name="URL.haystack"    default="";
param name="URL.needle"        default="";
param name="URL.iterations"    default=0;

writeOutput("Search for '#URL.needle#' in '#URL.haystack#' #URL.iterations# times<br><br>");

// CFML tests

runTest(function(){
        find(URL.needle, URL.haystack);
    },
    "Using find() CFML function"
);


runTest(function(){
        URL.haystack.find(URL.needle);
    },
    "Using .find() CFML member function"
);

runTest(function(){
        URL.haystack.indexOf(URL.needle);
    },
    "Using indexOf() Java method via CFML"
);
writeOutput("<hr>");

// Java tests

o = createObject("java", "TestReflectionPerformance");
runTest(function(){
        o.directCall(URL.needle, URL.haystack);
    },
    "Using indexOf() Java method natively"
);

runTest(function(){
        o.viaReflection(URL.needle, URL.haystack);
    },
    "Using indexOf() Java method via reflection"
);


function runTest(required function test, string message=""){
    var sw = makeStopWatch();
    sw.start();
    for (var i=1; i <= URL.iterations; i++){
        test();
    }
    sw.stop();
    writeOutput("#message#: #sw.getTimeline()[2].totalDuration#ms<br>");
}

Here I've got a helper function which runs a test a certain number of times, and timing it. Then I have three CFML-centric tests:
  1. calling the inbuilt function find() to find a substring in a string
  2. same, but using the .find() member function
  3. same but using Java's equivalent .indexOf() method
And I then have two Java-centric methods. I'm still obviously calling these via CFML, but I'm explicitly using first a direct call to .indexOf(), and in the second using reflection to call it:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TestReflectionPerformance {

    public int directCall(String needle, String haystack){
        return haystack.indexOf(needle);
    }

    public int viaReflection(String needle, String haystack)
        throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException
    {
        Method  method = haystack.getClass().getMethod("indexOf", String.class);
        return (int) method.invoke(haystack, needle);
    }

}

Running this with a single iteration yields nominal results:

Search for 'clue' in 'this is a sentence with a clue in it' 1 times

Using find() CFML function: 0ms
Using .find() CFML member function: 0ms
Using indexOf() Java method via CFML: 0ms


Using indexOf() Java method natively: 0ms
Using indexOf() Java method via reflection: 0ms


(this is on Railo 4.2.0.008).

No surprises there. And ramping it up through ten then 100 iterations shows little difference... instead of being zero all the time, somtimes they're 1ms (but each is either 0ms or 1ms without pattern or any way of sensibly drawing a conclusion).

Up at 1000 iterations we start getting meaningful information:

Search for 'clue' in 'this is a sentence with a clue in it' 1000 times

Using find() CFML function: 1ms
Using .find() CFML member function: 2ms
Using indexOf() Java method via CFML: 1ms


Using indexOf() Java method natively: 2ms
Using indexOf() Java method via reflection: 3ms


This is typical of about 20-odd test runs. Still nothing to really get concerned about. Screw it: let's try 100000 iterations:

Search for 'clue' in 'this is a sentence with a clue in it' 100000 times

Using find() CFML function: 63ms
Using .find() CFML member function: 116ms
Using indexOf() Java method via CFML: 127ms


Using indexOf() Java method natively: 155ms
Using indexOf() Java method via reflection: 248ms


So finally we see that there's a "significant" difference between calling the CFML method and using reflection (I dunno what's going on with the member function?). I ran this test round about 20 times, and this is indicative of all of them.

This is definitely in the realms of "you know what? Who the hell cares?". 60ms difference across 100000 iterations is just too inconsequential to worry about.

Let's run this on ColdFusion 11 now:

Search for 'clue' in 'this is a sentence with a clue in it' 100000 times

Using find() CFML function: 287ms
Using .find() CFML member function: 713ms
Using indexOf() Java method via CFML: 3560ms


Using indexOf() Java method natively: 341ms
Using indexOf() Java method via reflection: 536ms


This is on a similarly-provisioned JVM as Railo, and the difference is quite considerable (ie: an order of magnitude). But even then... it's 3sec difference over 100000 iterations. 0.03ms (0.03 milliseconds) per iteration. Don't care.

The bottom line here is that I think I have used more time investigating this and writing this article than all the time I could ever save by not using reflection when it seems to be a better approach to my source code, and code maintenance in general. The performance differences simply don't matter.

So that was a good use of my time.

--
Adam