diff --git a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs index 22662fb9..9db57165 100644 --- a/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs +++ b/source/Handlebars/Compiler/Translation/Expression/PathBinder.cs @@ -9,6 +9,10 @@ namespace HandlebarsDotNet.Compiler { internal class PathBinder : HandlebarsExpressionVisitor { + private static readonly System.Reflection.MethodInfo WriteObjectToMethod = + typeof(EncodedTextWriter).GetMethod("WriteObjectTo", + System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!; + private CompilationContext CompilationContext { get; } public PathBinder(CompilationContext compilationContext) @@ -68,12 +72,20 @@ protected override Expression VisitStatementExpression(StatementExpression sex) var contextValue = New(() => new Context(bindingContext)); var args = New(() => new Arguments(0)); var textWriter = CompilationContext.Args.EncodedWriter; + // Kept inline (unlike the plain-path statement route below): this is the + // hottest render-time shape, and routing it through a NoInlining entry point + // costs ~2ns per value per render. Compile cost of this shape is moderate + // because the interface dispatch below cannot be inline-expanded by the JIT. return Call(() => helper.Value.Invoke(textWriter, options, contextValue, args)); } var writer = CompilationContext.Args.EncodedWriter; var value = Arg(Visit(sex.Body)); - return writer.Call(o => o.Write(value)); + // Emit as a static call with the writer in (in-)argument position: LambdaCompiler + // compiles an instance call on the struct parameter whose argument is a call + // result dramatically slower (~0.3ms+ per statement) than the equivalent + // static-call shape used by the helper invocation route. + return Expression.Call(WriteObjectToMethod, writer.Expression, value.Expression); } protected override Expression VisitPathExpression(PathExpression pex) @@ -119,10 +131,9 @@ protected override Expression VisitPathExpression(PathExpression pex) } } - var options = New(() => new HelperOptions(pathInfo, bindingContext)); - var context = New(() => new Context(bindingContext)); - var argumentsArg = New(() => new Arguments(0)); - return Call(() => helper.Value.Invoke(options, context, argumentsArg)); + // Single NoInlining entry point instead of inline options/context/arguments + // construction + dispatch — see CompiledHelperInvokers for rationale. + return Call(() => CompiledHelperInvokers.Invoke(helper, pathInfo, bindingContext)); } } } diff --git a/source/Handlebars/Helpers/CompiledHelperInvokers.cs b/source/Handlebars/Helpers/CompiledHelperInvokers.cs new file mode 100644 index 00000000..56c7cb3f --- /dev/null +++ b/source/Handlebars/Helpers/CompiledHelperInvokers.cs @@ -0,0 +1,25 @@ +using System.Runtime.CompilerServices; +using HandlebarsDotNet.PathStructure; +using HandlebarsDotNet.Runtime; + +namespace HandlebarsDotNet.Helpers +{ + /// + /// Static entry points emitted by the compiler for helper-literal statements + /// ({{name}} with no arguments). NoInlining keeps template JIT fast: dynamic methods + /// are compiled at CreateDelegate, and expanding the options/context/arguments + /// construction plus dispatch into every call site multiplied template JIT cost. + /// These methods are JIT-compiled once per process; templates emit one thin call. + /// + internal static class CompiledHelperInvokers + { + [MethodImpl(MethodImplOptions.NoInlining)] + internal static object? Invoke( + Ref> helper, + PathInfo pathInfo, + BindingContext bindingContext) + { + return helper.Value.Invoke(new HelperOptions(pathInfo, bindingContext), new Context(bindingContext), new Arguments(0)); + } + } +} diff --git a/source/Handlebars/IO/EncodedTextWriter.cs b/source/Handlebars/IO/EncodedTextWriter.cs index e74f3a74..278732ec 100644 --- a/source/Handlebars/IO/EncodedTextWriter.cs +++ b/source/Handlebars/IO/EncodedTextWriter.cs @@ -114,6 +114,14 @@ public void Write(T value, bool encode) where T: IEnumerator [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(object? value) => Write(value); + // Static entry point used by compiled templates ({{expr}} statements). NoInlining is + // what makes template compilation fast: dynamic methods are JIT-compiled at + // CreateDelegate, and inlining the aggressive-inline Write type-switch and + // encoder machinery into every mustache call site multiplied template JIT cost ~6x. + // This wrapper is JIT-compiled once per process; templates emit one thin call. + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void WriteObjectTo(in EncodedTextWriter writer, object? value) => writer.Write(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(T? value) {