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:
- Exceptions have a major performance impact.
- 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 5 | With 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.
Nice article serie, you have any idea if and when your pr will be added into the main dotliquid branch.
ReplyDeleteHi 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.
ReplyDeleteI'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.
Great work Steven. I am looking forward to see this being merged in main repository so many people can use it.
ReplyDeleteI 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