Skip to content

Commit 6bdfacb

Browse files
authored
Merge pull request #386 from nuskey8/codex/pr-benchmark-comparison
bench: compare Lua-CSharp, MoonSharp and NuLua on .NET 10
2 parents ad530ca + f54e526 commit 6bdfacb

12 files changed

Lines changed: 649 additions & 5 deletions

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ jobs:
3636
run: dotnet restore Lua.slnx
3737
- name: Test
3838
run: dotnet test tests/Lua.Tests/Lua.Tests.csproj -c Release --filter "TestCategory!=ExpectedFailure"
39+
- name: Verify interpreter benchmark workloads
40+
working-directory: sandbox/Benchmark
41+
run: dotnet run --project Benchmark.csproj -c Release --no-restore -- --verify

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ English | [日本語](README_JA.md)
1414

1515
Lua-CSharp is a library that provides a Lua interpreter implemented in C#. By integrating Lua-CSharp, you can easily embed Lua scripts into your .NET applications.
1616

17-
Lua-CSharp leverages the latest C# features, designed with low allocation and high performance in mind. It is optimized to deliver maximum performance when used for interoperation between C# and Lua in C# applications. Below is a benchmark comparison with [MoonSharp](https://github.com/moonsharp-devs/moonsharp/) and [NLua](https://github.com/NLua/NLua):
17+
Lua-CSharp uses modern C# features to reduce interpreter and embedding overhead. The [.NET 10 benchmark guide](sandbox/Benchmark/README.md) separates execution, compilation and startup costs, with reproducible MoonSharp/NuLua Lua 5.2 comparisons. The earlier MoonSharp/NLua chart below uses a different benchmark setup:
1818

1919
![img](docs/Benchmark.png)
2020

21-
MoonSharp generally provides good speed but incurs significant allocations due to its design. NLua, being a C-binding implementation, is fast, but introduces substantial overhead when interacting with the C# layer. Lua-CSharp, fully implemented in C#, allows for seamless interaction with C# code without additional overhead. Moreover, it operates reliably in AOT environments since it does not rely on IL generation.
21+
Performance and allocation costs depend on the workload and embedding API. The interpreter runs in managed C# and supports AOT environments without runtime IL generation.
2222

2323
## Features
2424

‎sandbox/Benchmark/Benchmark.csproj‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<OutputType>Exe</OutputType>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
10+
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
1111
<PackageReference Include="MoonSharp" Version="2.0.0" />
12+
<PackageReference Include="NuLua.Lua52" Version="0.1.0" />
1213
<PackageReference Include="NLua" Version="1.7.3" />
1314
</ItemGroup>
1415

1516
<ItemGroup>
1617
<ProjectReference Include="..\..\src\Lua\Lua.csproj" />
1718
</ItemGroup>
19+
20+
<ItemGroup>
21+
<EmbeddedResource Include="n-body.lua" LogicalName="Comparison.n-body.lua" />
22+
</ItemGroup>
1823
</Project>

‎sandbox/Benchmark/BenchmarkCore.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public void Setup(string fileName)
3333

3434
public void Dispose()
3535
{
36+
luaCSharpState.Dispose();
3637
nLuaState.Dispose();
3738
}
3839
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using MoonSharp.Interpreter;
2+
using NuLua;
3+
using NuLua.Lua52;
4+
5+
public sealed class MoonSharpComparison
6+
{
7+
readonly Script state = new(CoreModules.Preset_Complete);
8+
readonly DynValue closure;
9+
10+
public MoonSharpComparison(string source)
11+
{
12+
state.Globals["host_add"] = DynValue.NewCallback(
13+
(context, args) => DynValue.NewNumber(args[0].Number + args[1].Number)
14+
);
15+
closure = state.LoadString(source, codeFriendlyName: "comparison");
16+
}
17+
18+
public double Run() => state.Call(closure).Number;
19+
}
20+
21+
public sealed class NuLua52Comparison : IDisposable
22+
{
23+
readonly Lua52State state;
24+
readonly LuaReference closure;
25+
26+
public NuLua52Comparison(string source)
27+
{
28+
state = Lua52State.Create();
29+
state.OpenLibraries();
30+
state.GetGlobal("_VERSION");
31+
if (state.ToString(-1) != "Lua 5.2")
32+
throw new InvalidOperationException("Expected native Lua 5.2");
33+
state.SetTop(0);
34+
state.RegisterFunction(
35+
"host_add",
36+
(vm, args) =>
37+
{
38+
vm.PushNumber(args[0].Read<double>() + args[1].Read<double>());
39+
return 1;
40+
}
41+
);
42+
state.LoadString(source, "comparison");
43+
closure = state.Ref(state.RegistryIndex);
44+
}
45+
46+
public double Run()
47+
{
48+
// Use the public stack API and a retained registry reference, avoiding a results
49+
// array just as Lua-CSharp uses a caller-owned return buffer.
50+
state.PushValue(closure);
51+
state.Call(0, 1);
52+
var result = state.ToNumber(-1);
53+
state.SetTop(0);
54+
return result;
55+
}
56+
57+
public void Dispose()
58+
{
59+
state.Unref(closure);
60+
state.Dispose();
61+
}
62+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
using System.Reflection;
2+
3+
public sealed record ComparisonCase(string Name, string Category, string Source, double Expected)
4+
{
5+
public override string ToString() => Name;
6+
}
7+
8+
public static class ComparisonWorkloads
9+
{
10+
// ASCII input keeps the UTF-16, byte-string and native UTF-8 engines comparable.
11+
// Every invocation rebuilds its mutable Lua data and returns one numeric checksum.
12+
public static readonly ComparisonCase[] Cases =
13+
[
14+
new("numeric-loop", "VM", "local s=0; for i=1,20000 do s=s+i end; return s", 200010000),
15+
new(
16+
"calls-varargs",
17+
"VM",
18+
"local function f(a,...) local b,c=...; return a+b+c end; local s=0; for i=1,2000 do s=s+f(i,2,3) end; return s",
19+
2011000
20+
),
21+
new(
22+
"table-access",
23+
"Tables",
24+
"local t={}; for i=1,1000 do t[i]={x=i,y=i%7} end; local s=0; for i=1,1000 do local v=t[i]; s=s+v.x+v.y end; return s",
25+
503503
26+
),
27+
new(
28+
"metamethods",
29+
"VM",
30+
"local mt={__add=function(a,b) return a.x+b.x end,__index=function(t,k) return 5 end}; local a,b=setmetatable({x=3},mt),setmetatable({x=4},mt); local s=0; for i=1,2000 do s=s+(a+b)+a.missing end; return s",
31+
24000
32+
),
33+
new(
34+
"math",
35+
"Standard",
36+
"local abs,sqrt,min,max=math.abs,math.sqrt,math.min,math.max; local s=0; for i=1,2000 do s=s+min(abs(-i),sqrt(i))+max(i,3,7) end; return s",
37+
MathExpected()
38+
),
39+
new(
40+
"strings",
41+
"Standard",
42+
"local rep,reverse,sub,byte,concat=string.rep,string.reverse,string.sub,string.byte,table.concat; local n=0; local t={'abc',12,'def',34}; for i=1,400 do local s=sub(reverse(rep('abcd',8,':')),2,-2); n=n+byte(s)+#s+#concat(t,',') end; return n",
43+
59600
44+
),
45+
new(
46+
"patterns",
47+
"Standard",
48+
"local gsub=string.gsub; local n=0; for i=1,400 do local s=gsub('hello 123 world 456','(%a+)', '[%1]'); n=n+#s end; return n",
49+
9200
50+
),
51+
new(
52+
"table-sort",
53+
"Tables",
54+
"local t={}; for i=1,300 do t[i]=301-i end; table.sort(t); return t[150]",
55+
150
56+
),
57+
new(
58+
"coroutines",
59+
"Coroutines",
60+
"local co=coroutine.create(function() for i=1,500 do coroutine.yield(i) end end); local s=0; for i=1,500 do local ok,x=coroutine.resume(co); assert(ok); s=s+x end; return s",
61+
125250
62+
),
63+
new(
64+
"host-calls",
65+
"Interop",
66+
"local add=host_add; local s=0; for i=1,2000 do s=add(s,i) end; return s",
67+
2001000
68+
),
69+
new("nbody", "Mixed", LoadNBody(), -0.1690507623824094),
70+
];
71+
72+
public static readonly ComparisonCase[] CompilationCases =
73+
[
74+
Find("numeric-loop"),
75+
Find("nbody"),
76+
new(
77+
"control-flow",
78+
"Compiler",
79+
"local total=0;"
80+
+ string.Concat(
81+
Enumerable.Repeat(
82+
"do local function choose(value) if not value or value==false then return nil elseif value==true then return 1 else return value end end; "
83+
+ "local value=choose(2); repeat value=value-1 until value==0; while value<2 do value=value+1; if value==1 then goto skip end; total=total+value; ::skip:: end; "
84+
+ "for index=1,2 do if index==2 then break end; total=total+index end end;",
85+
40
86+
)
87+
)
88+
+ "return total",
89+
120
90+
),
91+
new(
92+
"identifiers",
93+
"Compiler",
94+
"local total=0;"
95+
+ string.Concat(
96+
Enumerable
97+
.Range(0, 80)
98+
.Select(i =>
99+
$"do local returnValue{i},functionName{i},ordinaryIdentifier{i}=1,2,3; total=total+returnValue{i}+functionName{i}+ordinaryIdentifier{i} end;"
100+
)
101+
)
102+
+ "return total",
103+
480
104+
),
105+
StringConstants(false),
106+
StringConstants(true),
107+
];
108+
109+
static ComparisonCase StringConstants(bool escaped)
110+
{
111+
var source = new System.Text.StringBuilder("local total=0;");
112+
var expected = 0;
113+
for (var i = 0; i < 80; i++)
114+
{
115+
var decoded = escaped
116+
? $"escaped value {i}:\tcolumn\nline\r\a\b\f\v\\\"'AAsuffix"
117+
: $"ordinary string value {i}: the quick brown fox jumps over the lazy dog";
118+
var literal = escaped
119+
? $"escaped value {i}:\\tcolumn\\nline\\r\\a\\b\\f\\v\\\\\\\"\\'\\x41\\065\\z suffix"
120+
: decoded;
121+
source.Append("do local value='").Append(literal).Append("'; total=total+#value end;");
122+
expected += decoded.Length;
123+
}
124+
source.Append("return total");
125+
return new(
126+
escaped ? "escaped-strings" : "plain-strings",
127+
"Compiler",
128+
source.ToString(),
129+
expected
130+
);
131+
}
132+
133+
public static ComparisonCase FindCompilation(string name) =>
134+
CompilationCases.Single(c => c.Name == name);
135+
136+
static double MathExpected()
137+
{
138+
double sum = 0;
139+
for (var i = 1; i <= 2000; i++)
140+
sum += Math.Sqrt(i) + Math.Max(i, 7);
141+
return sum;
142+
}
143+
144+
static string LoadNBody()
145+
{
146+
using var stream = Assembly
147+
.GetExecutingAssembly()
148+
.GetManifestResourceStream("Comparison.n-body.lua")!;
149+
using var reader = new StreamReader(stream);
150+
return reader.ReadToEnd().Replace("or 1000", "or 100") + "\nreturn energy(bodies,nbody)";
151+
}
152+
153+
public static ComparisonCase Find(string name) => Cases.Single(c => c.Name == name);
154+
155+
public static void Check(ComparisonCase workload, string engine, double actual)
156+
{
157+
var tolerance = Math.Max(1, Math.Abs(workload.Expected)) * 1e-10;
158+
if (!double.IsFinite(actual) || Math.Abs(actual - workload.Expected) > tolerance)
159+
throw new InvalidOperationException(
160+
$"{engine}/{workload.Name}: expected {workload.Expected:R}, got {actual:R}"
161+
);
162+
}
163+
}

0 commit comments

Comments
 (0)