Wednesday, 14 January 2015

Optimising DotLiquid: Part 4

Low Hanging Fruit

I've already snagged some low hanging fruit in the Optimising DotLiquid series by avoiding rework and respecting regex. DotLiquid's rendering is now more than twice as fast.

There's still more gains for the taking, though, and now that I'm familiar with the codebase I've turned my sights squarely on the Hash class.

The Hash class is used everywhere in DotLiquid for storing and retrieving the current scope's data, so even a small performance gain should have a large impact.

Optimisations

I replaced occasions of checking a Dictionary for a key before getting the value with a single call to TryGetValue.

// BEFORE
// ==================================
if (_nestedDictionary.ContainsKey(key))
    return _nestedDictionary[key];

// AFTER
// ==================================
object result;
if (_nestedDictionary.TryGetValue(key, out result))
    return result;

The class For, which renders loop blocks, used the reflection-based method Hash.FromAnonymousObject in every iteration. I avoided the overhead of reflection by setting the values directly instead.

// BEFORE
// ==================================
context["forloop"] = Hash.FromAnonymousObject(new
{
    name = _name,
    length = length,
    index = index + 1,
    index0 = index,
    rindex = length - index,
    rindex0 = length - index - 1,
    first = (index == 0),
    last = (index == length - 1)
});

// AFTER
// ==================================
var forHash = new Hash();

forHash["name"] = _name;
forHash["length"] = length;
forHash["index"] = index + 1;
forHash["index0"] = index;
forHash["rindex"] = length - index;
forHash["rindex0"] = length - index - 1;
forHash["first"] = (index == 0);
forHash["last"] = (index == length - 1);

context["forloop"] = forHash;

A few performance critical paths cast the same object to the same Type more than once. I replaced double casts with the as operator and a null check.

// BEFORE
// ==================================
if ((obj is IIndexable) 
    && ((IIndexable) obj)
            .ContainsKey((string) part))
    return true;

// AFTER
// ==================================
var indexable = obj as IIndexable;
if (indexable != null 
    && indexable.ContainsKey((string) part))
    return true;

The frequently visited IDictionary.this[object key] implementation in Hash checks the object's Type is string and throws an exception if not. Since the subsequent cast to string will throw an InvalidCastException under that circumstance anyway, I removed the check to improve performance.

// BEFORE
// ==================================
if (!(key is string))
    throw new NotSupportedException();
return GetValue((string) key);

// AFTER
// ==================================
return GetValue((string) key);

I removed an unnecessary null-check in the performance critical method Hash.GetValue.

// BEFORE
// ==================================
if (_defaultValue != null)
    return _defaultValue;

return null;

// AFTER
// ==================================
return _defaultValue;

I avoided assigning values retrieved from the Hash back into the Hash.

// BEFORE
// ==================================
context.Registers["for"] = context.Registers["for"] 
                         ?? new Hash(0);

// AFTER
// ==================================
object forRegister = context.Registers["for"];
if (forRegister == null)
{
    forRegister = new Hash(0);
    context.Registers["for"] = forRegister;
}

Timings

The below timings were all taken during the same time period on the same machine. They are based on 10,000 iterations per test.

Render Time (ms)Original CodePart 3 CodeHash Improvements
Minimum 3.42810 1.50950 1.33320
Maximum 5.76840 2.99220 2.60190
Range 2.34030 1.48270 1.26870
Average 3.61269 1.56977 1.38641
Std. Deviation 0.17960 0.07936 0.06621

Summary

DotLiquid's rendering is now two and a half times faster. It's worth noting too that due the nested nature of rendering, as a template grows in size and complexity the performance savings will grow exponentially.

The next step now is to put together a mega-template that uses every single DotLiquid feature, then improve performance even further!

No comments:

Post a Comment