Friday, 16 January 2015

Optimising DotLiquid: Part 6

Exceptions Should Be Exceptional

A recent merge into the main DotLiquid repository added support for the keywords continue and break using exceptions and try/catch further up the render chain for flow control.

There are two significant reasons why exceptions should never be used for controlling the flow of a program:

  1. Exceptions have a major performance impact.
  2. It is not obvious without serious digging where those exceptions will be handled or if they will be handled at all.

Having this exception-based implementation is better than not having an implementation, of course, but in this series' war on performance it is an obvious target.

Optimisation

The concept for this optimisation is simple enough: to replace the flow controlling exceptions and try/catch handling with proper program flow.

You can see the full changeset on GitHub, but below is a brief summary of the changes.

The break tag class

// BEFORE
// ==================================
public class Break : Tag
{
    public override void Render(
                            Context context, 
                            TextWriter result)
    {
        throw new BreakInterrupt();
    }
}

// AFTER
// ==================================
public class Break : Tag
{
    public override ReturnCode Render(
                                Context context, 
                                TextWriter result)
    {
        return ReturnCode.Break;
    }
}

Shared - the RenderAll loop body

// BEFORE
// ==================================
if (token is IRenderable)
    ((IRenderable) token).Render(context, result);
else
    result.Write(token.ToString());

// AFTER
// ==================================
var renderable = token as IRenderable;
if (renderable != null)
{
    var retCode = renderable.Render(context, result);
    if (retCode != ReturnCode.Return)
        return retCode;
}
else
    result.Write(token.ToString());

The for tag class - break and continue handling

// BEFORE
// ==================================
try
{
    RenderAll(NodeList, context, result);
}
catch (BreakInterrupt)
{
    break;
}
catch (ContinueInterrupt)
{
}

// AFTER
// ==================================
if (RenderAll(NodeList, context, result) == ReturnCode.Break)
    break;

A quick re-run of all of the Unit Tests tells me that this far-reaching changeset has maintained all the original expected behaviours.

Initial 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)Part 5With New Flow Control
Minimum 6.631110 5.78710
Maximum 8.65750 7.61880
Range 2.02640 1.83170
Average 6.87194 5.99984
Std. Deviation 0.20780 0.18964

Summary

Simply avoiding the anti-pattern of using exceptions to control program flow has reduced render time by more than 10%.

A minor side-effect of updating the program flow in this way is that anyone who has written their own tags will need to make the following changes:

  • The return Type of the Render method is now ReturnCode.
  • Wherever return is used, return ReturnCode.Return instead.
  • Whenever the result of RenderAll is not ReturnCode.Return, return that result immediately. (Example in Block.RenderAll)

This side effect only affects developers who have created their own tags, anyone downloading and using the library as-is will enjoy increased performance without having to make any changes.

3 comments:

  1. Nice article serie, you have any idea if and when your pr will be added into the main dotliquid branch.

    ReplyDelete
  2. Hi Bryan, glad you like it! Due to the unavoidable change to the signature of the API in my PR, I've been told that it'll have to wait until DotLiquid v2.0, which doesn't currently have a concrete release date.

    I'll be making sure to keep my branch up to date with all the main branch changes, so feel free to grab it from github if you need the improved performance immediately.

    ReplyDelete
  3. Great work Steven. I am looking forward to see this being merged in main repository so many people can use it.

    I am going to use this with VirtoCommerce (https://github.com/VirtoCommerce/dotliquid).

    I am going request VirtoCommerce to see whether they can merge your changes with their fork.

    Thanks
    Anwar

    ReplyDelete