String Performance: Getting Good Performance from Internet Explorer

By on June 9, 2008 12:01 am

In the last post on string performance, we did an analysis of string performance that spanned all of the major browsers, with the goal of optimizing the performance of the dojox.string.Builder. While we were able to create significant improvements in performance—particularly with Firefox—the performance under Internet Explorer was still pretty poor compared to native methods.

The goal for this article was to bring Builder’s performance down to comparable native operations—and we were able to do with through a combination of a slight change in code with using different ways of calling the append method.

Dispelling the myth of Array.join

Past wisdom has said that for performance reasons it is better to push string fragments into an array, and then join them together at the last minute. This idea was based on the poor string performance present since at least IE3 and in part on the memory consumption large object operations tends to have, due to the simplistic mark-and-sweep generational garbage collector used for the JScript engine—admirably demonstrated by this article on JScript garbage collector written by Dan Pupius.

However, the previous string performance analysis revealed an interesting tidbit: when appending string fragments via iteration, the performance of the “+=” operator was actually better in Internet Explorer 7 than pushing fragments into an array and joining them. With this in mind, the first thing we did was to remove the IE-only branch from the source code of the Builder, so that instead of using an array as an internal buffer, it used the same string that all of the other browsers do. From here, we tested performance with both Internet Explorer 6 and Internet Explorer 7:

Internet Explorer 6 Builder branch

Internet Explorer 7 Builder branch

The first thing we notice is that with IE6, using an array as a buffer is slightly faster—but we can also notice that the difference is negligible. We also notice here that with IE7, the performance of using a string as an internal buffer is better than using an array. But these tests show another interesting tidbit: the performance in both browsers is better when passing more than one argument to the Builder.append method.

(For a quick refresher: the dojox.string.Builder object’s primary method is append, which was written to take N arguments; the three tests here are when a builder is reused and one argument is appended at a time, a new builder is used and 2 arguments are passed, and a new builder is used on one argument is passed.)

Based on this data, we removed the IE-specific branch from Builder, so that there is only the internal string as a buffer.

To append or not to append, that is the question.

Following the last observation, a quick battery of tests were undertaken to see exactly what kind of performance improvement there would be when passing more than one argument. In particular, we wanted to see what the effects of passing anywhere from 1 to 9 arguments (9 being the arbitrary optimization limit we wrote during the course of the last analysis).

As a reminder, this is the optimization we wrote for Firefox during the last analysis:

append: function(s){ 
	if(arguments.length>1){
		var tmp="", l=arguments.length;
		switch(l){
			case 9: tmp=arguments[8]+tmp;
			case 8: tmp=arguments[7]+tmp;
			case 7: tmp=arguments[6]+tmp;
			case 6: tmp=arguments[5]+tmp;
			case 5: tmp=arguments[4]+tmp;
			case 4: tmp=arguments[3]+tmp;
			case 3: tmp=arguments[2]+tmp;
			case 2: {
				this.b+=arguments[0]+arguments[1]+tmp;
				break;
			}
			default: {
				var i=0, tmp="";
				while(i < arguments.length){
					tmp += arguments[i++];
				}
				this.b += tmp;
			}
		}
	} else {
		this.b += s;
	}
	return this;
}

(Note: we added another slight optimization for the case of more than 9 arguments by creating a temporary string buffer and then adding the results to the real buffer.)

To test, we created alternate versions of the builderForMulti test that passed N arguments at a time. To ensure the results of the tests were accurate, we had to follow each loop with a remainder loop (which introduced a slight noise factor). Here are the results, for both IE6 and IE7:

IE6, multiple arguments to Builder.append

IE7, passing multiple arguments to Builder.append

As you can see, the results in performance are dramatic; passing only 3 arguments at a time cut the performance almost in half, and passing 9 cut the performance time in both versions of Internet Explorer by close to a factor of 4.

So what does this mean?

We think the key to this performance improvement has less to do the number of arguments and more to do with the way the previous optimization works; because we used a temporary string buffer to put all of the passed arguments together before appending it to the main string buffer, we minimize the effect of large string copying operations—which has been one of the major issues with performance with Internet Explorer.

In addition, being able to cut the number of loops made in each test makes a significant difference; we know from previous tests that loop overhead in IE tends to be a little higher than other browsers.

Comparing the results with Array.join.

Finally, we compare the performance numbers of passing 5 or more arguments to Builder.append against the joinFor numbers from the previous analysis:

Array.join() vs. Builder with 5 or more args

As you can see here, passing 7 to 9 arguments has the effect of being faster performance-wise than pushing fragments one at a time into an array and joining them. Passing 5 or 6 arguments is slightly slower but comparable enough (given the Monte Carlo nature of the tests) to warrant consideration for usage.

Results, conclusions and other considerations.

First things first—with the performance improvements with IE7, we no longer need to consider using an alternate path when doing large scale string operations; using Array.join in an iterative situation gives you no major advantages than using += in the same situation. In addition, the differences with IE6 were slight enough to allow you to not bother forking for that specific version.

The only time considering using an array as opposed to a string for these kind of operations is when you are aware that the fragments you are appending are very large (on the order of > 65536 bytes); doing this will cause the GC issues Dan talks about in his analysis of object allocation and the JScript garbage collector.

From there, we can progress to programming techniques—with Internet Explorer, it is much better to call Builder.append with as many arguments as possible than to simply iterate and push things in one at a time.

It is also better to start small; try to structure your string operations so that very large string operations are minimized. In this case, using a temporary buffer to assemble a set of strings together and then adding them to a much larger string is better than constantly adding small fragments to a larger string.

And as always, minimizing the size of an iteration will help get extra performance out of JScript.

Other browsers.

While we did not record any numbers with other browsers, we did test the multiple argument approach with Firefox, Safari and Opera on both OS X and Windows 2003 Server. Passing as many arguments as possible did increase performance results with both Safari and Opera—though because both browsers have such good performance, the differences were much smaller than with Internet Explorer.

An interesting thing though—passing multiple arguments with Firefox did not increase performance at all. In fact, the numbers remained flat, no matter how many arguments were passed.

As with the last analysis, here are the raw numbers.

Comments

  • Michael Toast

    Great work.

    Also, what do you use to build your charts? They are gorgeous.

  • I’m sorry to admit it, but I like your charts, too! Where do they come from?

  • ttrenka

    These charts (like in the first post) were done with Numbers (Apple iWork ’08).

  • Hey Tom,

    Just wondering, but how does the new iterative builder compare to a one-time join of the constituent parts? That is, how does
    var myString = sourceArray.join(“”);
    compare to
    var builder = new dojox.string.Builder();
    builder.append(sourceArray);
    myString = builder.toString();

  • I ask because we (Bloglines.com) do a lot stuff somewhat like this:

    return buildString([
    firstPart(),
    secondPart(),
    thirdPart()
    ]);

    where buildString is:
    var buildString = dojo.isMozilla ?
    function(arr){
    return String.prototype.concat.apply(“”,arr);
    } :
    function(arr){
    return arr.join(“”);
    };

    Because (IIRC) we found that one Firefox 2, calling concat was a fair bit faster than using array.join, and that newing up a string.Builder was expensive enough across browsers to dominate string building costs.

    So now we just build up arrays and use buildString instead of using a buffer and calling append on it. Looks like we might want to revisit the issue.

  • ttrenka

    Hey Ben,

    One time ops always kick the butt out of iterative ones, and your observations WRT String.prototype.concat vs. Array.join are still accurate. But I did these studies because the majority of the time there’s *some* form of iteration going on–even though it’s usually not part of a loop (unless you’re building something repeatable, like a table).

    I did start playing with trying to set up internal buffers in case a string’s length is long enough to force the IE GC issue, and immediately ran into walls in terms of regular performance; I might write a third follow up to this series detailing what I was trying to do and why it was either successful or unsuccessful.

  • ttrenka

    I should add that the additional loop-unrolling, in conjunction with the loop unroll in .append, seems to be the biggest factor with the performance in IE. Mostly what I was looking to do here with the study was to figure out what current thinking factors (i.e. array.join) were or were not still a major factor with performance enhancements.

  • tahorg

    What myth? What do you talking about? Simle test :

    ————————————————————-

    var count = 30000;	
    var s = "I'm Going Slightly Mad";
    	
    function test1()
    {	
    	var before = new Date().getTime();
    
    	x = "";
    
    	for (var i = 0; i < count; i++) {
    	     x += s;
    	}
    	var after = new Date().getTime();
    	var time = after - before;
    	
    	alert(time);
    }
    
    function test2()
    {	
    	var before = new Date().getTime();
    
    	x = [];
    
    	for (var i = 0; i < count; i++) {
    	     x.push(s);
    	}
    	
    	x = x.join();
    	
    	var after = new Date().getTime();
    	var time = after - before;
    	
    	alert(time);
    }
    
    test1();
    test2();
    
    

    ——————————————————

    test1 : 16700ms
    test2 : 78ms

    Forget about concatenation in IE7.

  • @tahorg:

    Your test crosses the GC limit as shown by Dan Pupius; this is the reason for the disparate test figures. More importantly–do you consider that to be a real-world test situation? 30,000 iterations on a phrase?

    The goal here is to work in both theoretical *and* practical terms. Try this test in other browsers and see what happens, and then (if you don’t mind) post the results. The difference here (with your test) is that with the single join, you only cross that 64K GC threshold once, whereas with the concatention iteration, you cross it very quickly.

    The key to good string performance is having an idea (or at least a general idea) of the practical usage of your code. This example is (IMHO) a bit more contrived than most of the tests I’ve seen and/or worked with.

    (For the record, I did do a battery of tests similar to this, designed to push the limits of the IE GC and got similar results; Dan’s article on the IE GC does a lot to explain why this is.)

  • Pingback: Performance web » Concat√©nation javascript()

  • Pingback: Garbage Collection in IE7 heavily impacted by number of JavaScript objects and string sizes Performance, Scalability and Architecture – Java and .NET Application Performance Management (dynaTrace Blog)()