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
CompilationModemakes 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
Evaluatereaches 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.