Table of Contents

Performance, caching and threads

RuleWright spends its time up front so evaluation is cheap: a warm, compiled evaluation costs about 100 nanoseconds a rule. Knowing where the one-time costs fall is most of what it takes to keep it that way.

Measured costs

From the benchmark suite in the RuleWright repository (BenchmarkDotNet, .NET 8, an Intel Core i9-13980HX). Absolute times depend on hardware; the ratios carry over.

Operation 1 rule 100 rules 10,000 rules
Evaluate, compiled (typed fact) 123 ns 9.8 µs 813 µs
Evaluate, compiled, traced 376 ns 34.0 µs 4.77 ms
Evaluate, interpreted (dictionary fact) 238 ns 18.0 µs 1.50 ms
LoadRuleSet (parse, validate, hash) 7.1 µs 637 µs
Load, compile and first evaluation 956 µs 91 ms

What that means in practice:

  • Evaluation scales linearly, at about 100 ns a rule compiled.
  • Dictionary facts cost about 1.8 times as much. That's the price of a fact with no compile-time shape, and CompilationMode makes it visible.
  • Tracing costs about 3 times as much, and only when you ask for it.
  • Compilation dominates the first call, around a millisecond a rule. It happens once per rule and fact type. Load and warm up at startup, and no request pays it.

Against other .NET engines, for this stateless evaluate-one-fact pattern, the same suite measured RuleWright 1.6 to 2.2 times faster than Microsoft RulesEngine and 7 to 8 times faster than NRules, allocating 3.6 to 4.5 times less than the first and 16 to 20 times less than the second. NRules is built for a different problem, long-lived inference over changing facts, and the repository's docs/benchmarks.md explains the comparison and how to check that all three engines made the same decisions.

The cache

  • Compiled delegates live on the engine, keyed by fact type and rule content hash.
  • A rule compiles the first time Evaluate reaches it for a fact type. Disabled rules, and rules after a stop, aren't compiled until an evaluation reaches them.
  • Reloading a document recompiles only rules whose logic changed.
  • The cache is never evicted. It grows with the number of distinct rules an engine has ever compiled. For hot reload at scale, see Host and reload rules.

Threads

Everything you share is immutable:

Object Shareable
RuleWrightEngine Yes. Built once, immutable, with a lock-free concurrent cache.
LoadedRuleSet, RuleSet, Rule Yes.
EvaluationOptions Yes.
RuleEvaluationResult, traces Yes.
Your IRuleFunction implementations Must be: one instance serves every concurrent evaluation.

Every evaluation's working state (the outputs being built, the trace slots) is local to the call. So one engine and one loaded rule set can serve any number of threads:

using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;
using RuleWright.Serialization;

// One engine and one loaded rule set, shared by every thread.
RuleWrightEngine engine = new RuleWrightBuilder()
    .UseJsonReader(new SystemTextJsonReader())
    .Build();
LoadedRuleSet rules = engine.LoadRuleSet(File.ReadAllText("actions.json"));

Checkout[] facts = Enumerable.Range(0, 10_000)
    .Select(i => i % 2 == 0 ? Facts.Vip() : Facts.Newcomer())
    .ToArray();

var scores = new decimal[facts.Length];
Parallel.For(0, facts.Length, i =>
{
    RuleEvaluationResult result = engine.Evaluate(rules, facts[i]);
    scores[i] = result.Outputs.TryGetValue("Score", out object? score) ? (decimal)score! : 0m;
});

Console.WriteLine($"Evaluated {facts.Length:N0} facts in parallel; total score {scores.Sum():N0}");

Output

Evaluated 10,000 facts in parallel; total score 375,000

Tips

  • One engine per process. An engine per request recompiles everything on every request.
  • Type the variable. Evaluate(rules, (object)fact) takes the interpreter. See Typed and dictionary facts.
  • Constant outputs are free. A rule whose actions are all constant setOutputs has its outputs built at load time and shared, so firing it allocates nothing for them.
  • Trace a sample, not everything, if you trace in production.
  • Native AOT can't compile expression trees. Use dictionary facts there.

Next

Reference.