Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ChibiRuby.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<Project Path="src\ChibiRuby.Debugger.Dap\ChibiRuby.Debugger.Dap.csproj" Type="Classic C#" />
<Project Path="src\ChibiRuby.Serializer.SourceGenerator\ChibiRuby.Serializer.SourceGenerator.csproj" Type="Classic C#" />
<Project Path="src\ChibiRuby.Serializer\ChibiRuby.Serializer.csproj" Type="Classic C#" />
<Project Path="src\ChibiRuby.JetPack\ChibiRuby.JetPack.csproj" Type="Classic C#" />
<Project Path="src\ChibiRuby.SourceGenerator\ChibiRuby.SourceGenerator.csproj" Type="Classic C#" />
<Project Path="src\ChibiRuby\ChibiRuby.csproj" Type="Classic C#" />
</Folder>
Expand Down
2 changes: 2 additions & 0 deletions sandbox/ChibiRuby.Benchmark/ChibiRuby.Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
</ItemGroup>

<ItemGroup>
Expand All @@ -27,6 +28,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\ChibiRuby.Compiler\ChibiRuby.Compiler.csproj" />
<ProjectReference Include="..\..\src\ChibiRuby.JetPack\ChibiRuby.JetPack.csproj" />
<ProjectReference Include="..\..\src\ChibiRuby.NIO\ChibiRuby.NIO.csproj" />
<ProjectReference Include="..\..\src\ChibiRuby\ChibiRuby.csproj" />
</ItemGroup>
Expand Down
75 changes: 75 additions & 0 deletions sandbox/ChibiRuby.Benchmark/OptcarrotAotCompiler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using ChibiRuby;
using ChibiRuby.JetPack;
using ChibiRuby.JetPack.Mrb2Cs;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace ChibiRuby.Benchmark;

// Runtime host for mrb2cs: generate the C# for an irep tree (Mrb2CsCompiler.Compile), Roslyn-compile it
// into an assembly, and register each body by fingerprint so a (re)parse of the same bytecode
// binds it. The source generation itself lives in ChibiRuby.JetPack (Mrb2Cs); this is just the
// "load it into the running process" half (the build-time path would csc + reference instead).
static class OptcarrotAotCompiler
{
public static int CompileAndRegister(MRubyState state, Irep root)
{
var result = Mrb2CsCompiler.Compile(state, root, "OptcarrotAotGenerated");
if (result.Methods.Count == 0)
{
return 0;
}

if (Environment.GetEnvironmentVariable("AOT_DUMP") is { Length: > 0 } dumpPath)
{
File.WriteAllText(dumpPath, result.Source);
}

var asm = Compile(result.Source);
var type = asm.GetType("OptcarrotAotGenerated")!;
foreach (var (name, fingerprint) in result.Methods)
{
var method = type.GetMethod(name, BindingFlags.Public | BindingFlags.Static)!;
var body = (CompiledRubyMethodBody)method.CreateDelegate(typeof(CompiledRubyMethodBody));
state.RegisterCompiledMethod(fingerprint, body);
}

return result.Methods.Count;
}

static Assembly Compile(string source)
{
var tree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest));
var refs = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
.Split(Path.PathSeparator)
.Where(p => p.Length > 0)
.Select(p => (MetadataReference)MetadataReference.CreateFromFile(p))
.ToList();
refs.Add(MetadataReference.CreateFromFile(typeof(MRubyState).Assembly.Location));

var compilation = CSharpCompilation.Create(
"OptcarrotAotGenerated",
new[] { tree },
refs,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release, allowUnsafe: true));

using var ms = new MemoryStream();
var emit = compilation.Emit(ms);
if (!emit.Success)
{
File.WriteAllText("/tmp/aot_fail.cs", source);
var errors = string.Join("\n", emit.Diagnostics
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Take(20)
.Select(d => d.ToString()));
throw new InvalidOperationException("mrb2cs generated code failed to compile:\n" + errors);
}

ms.Position = 0;
return Assembly.Load(ms.ToArray());
}
}
50 changes: 50 additions & 0 deletions sandbox/ChibiRuby.Benchmark/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,29 @@
return;
}

if (args is ["--quick-optcarrot-aot", ..])
{
var frames = args.Length >= 2 && int.TryParse(args[1], out var parsedAotFrames)
? parsedAotFrames
: 180;
var warmupRuns = args.Length >= 3 && int.TryParse(args[2], out var parsedAotWarmup)
? parsedAotWarmup
: 0;
using var loader = new RubyScriptLoader();
var compiled = loader.PreloadOptcarrotBenchmarkAot(frames, printResult: warmupRuns == 0);
Console.Error.WriteLine($"AOT-compiled methods: {compiled}");
for (var i = 0; i < warmupRuns; i++)
{
loader.RunChibiRuby();
}
if (warmupRuns > 0)
{
loader.PreloadOptcarrotRun(frames);
}
loader.RunChibiRuby();
return;
}

if (args is ["--quick-optcarrot-mruby", ..])
{
var frames = args.Length >= 2 && int.TryParse(args[1], out var parsedFrames)
Expand All @@ -104,6 +127,33 @@
return;
}

if (args is [ "--quick-variable-table-allocation", .. ])
{
var objectCount = args.Length >= 2 && int.TryParse(args[1], out var parsed)
? parsed
: 100_000;
VariableTableAllocationCounter.Run(objectCount);
return;
}

if (args is [ "--quick-script", var scriptName, .. ])
{
var iterations = args.Length >= 3 && int.TryParse(args[2], out var parsedIterations)
? parsedIterations
: 1;
RubyScriptLoader.RunQuickScript(scriptName, iterations);
return;
}

if (args is [ "--quick-script-aot", var aotScriptName, .. ])
{
var iterations = args.Length >= 3 && int.TryParse(args[2], out var parsedAotIterations)
? parsedAotIterations
: 1;
RubyScriptLoader.RunQuickScript(aotScriptName, iterations, aot: true);
return;
}

BenchmarkSwitcher.FromAssembly(Assembly.GetEntryAssembly()!).Run(args);

[Config(typeof(BenchmarkConfig))]
Expand Down
126 changes: 109 additions & 17 deletions sandbox/ChibiRuby.Benchmark/RubyScriptLoader.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using ChibiRuby.Compiler;
using ChibiRuby.JetPack;

namespace ChibiRuby.Benchmark;

Expand Down Expand Up @@ -50,27 +53,100 @@ public RubyScriptLoader()
mrbStateNative = NativeMethods.MrbOpen();
}

static void RegisterMathModule(MRubyState state)
public static void RunQuickScript(string scriptName, int iterations)
{
state.DefineModule(state.Intern("Math"u8), mod =>
RunQuickScript(scriptName, iterations, aot: false);
}

public static void RunQuickScript(string scriptName, int iterations, bool aot)
{
if (iterations < 1)
{
mod.DefineClassMethod(state.Intern("sqrt"u8), new MRubyMethod((s, self) =>
{
var value = s.GetArgumentAsFloatAt(0);
return System.Math.Sqrt(value);
}));
iterations = 1;
}

mod.DefineClassMethod(state.Intern("cos"u8), new MRubyMethod((s, self) =>
{
var value = s.GetArgumentAsFloatAt(0);
return System.Math.Cos(value);
}));
using var loader = new RubyScriptLoader();
if (aot)
{
var compiled = loader.PreloadScriptFromFileAot(scriptName);
Console.Error.WriteLine($"AOT-compiled methods: {compiled}");
}
else
{
loader.PreloadScriptFromFile(scriptName);
}

mod.DefineClassMethod(state.Intern("sin"u8), new MRubyMethod((s, self) =>
{
var value = s.GetArgumentAsFloatAt(0);
return System.Math.Sin(value);
}));
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

var allocatedBefore = GC.GetAllocatedBytesForCurrentThread();
var gc0 = GC.CollectionCount(0);
var gc1 = GC.CollectionCount(1);
var gc2 = GC.CollectionCount(2);
var pauseBefore = GC.GetTotalPauseDuration();
var stopwatch = Stopwatch.StartNew();
var result = MRubyValue.Nil;
for (var i = 0; i < iterations; i++)
{
result = loader.RunChibiRuby();
}
stopwatch.Stop();
var allocatedBytes = GC.GetAllocatedBytesForCurrentThread() - allocatedBefore;
var pauseMs = (GC.GetTotalPauseDuration() - pauseBefore).TotalMilliseconds;
var wallMs = stopwatch.Elapsed.TotalMilliseconds;

Console.WriteLine(
$"ChibiRuby quick script={scriptName} aot={aot} iterations={iterations} elapsedMs={wallMs:F3} allocatedBytes={allocatedBytes} allocatedBytesPerIteration={(double)allocatedBytes / iterations:F0} result={result}");
Console.WriteLine(
$"[gc] server={System.Runtime.GCSettings.IsServerGC} gen0={GC.CollectionCount(0) - gc0} gen1={GC.CollectionCount(1) - gc1} gen2={GC.CollectionCount(2) - gc2} pauseMs={pauseMs:F1} pausePct={(wallMs > 0 ? pauseMs / wallMs * 100 : 0):F1}%");
}

// AOT variant of PreloadScriptFromFile: compile the script, execute it once so the
// class hierarchy exists (lets the codegen resolve self-sends for devirtualize+inline),
// then AOT-compile every statically-compilable method to C#, register the bodies by
// fingerprint and bind them to the irep tree. Subsequent RunChibiRuby() calls hit the
// compiled bodies. Returns the number of methods compiled.
public int PreloadScriptFromFileAot(string fileName)
{
var source = ReadBytes(fileName);
currentChibiRubyIrep = CompileChibiRubySource(Encoding.UTF8.GetString(source));
mrubyCSState.Execute(currentChibiRubyIrep);
var compiled = OptcarrotAotCompiler.CompileAndRegister(mrubyCSState, currentChibiRubyIrep);
mrubyCSState.BindCompiledMethods(currentChibiRubyIrep);
return compiled;
}

static void RegisterMathModule(MRubyState state)
{
state.DefineModule(state.Intern("Math"u8), mod =>
{
mod.DefineClassMethod(state.Intern("sqrt"u8), new MRubyMethod(
(s, self) =>
{
var value = s.GetArgumentAsFloatAt(0);
return System.Math.Sqrt(value);
},
(s, self, argument) => System.Math.Sqrt(s.AsFloat(argument)),
(_, _, argument) => System.Math.Sqrt(argument)));

mod.DefineClassMethod(state.Intern("cos"u8), new MRubyMethod(
(s, self) =>
{
var value = s.GetArgumentAsFloatAt(0);
return System.Math.Cos(value);
},
(s, self, argument) => System.Math.Cos(s.AsFloat(argument)),
(_, _, argument) => System.Math.Cos(argument)));

mod.DefineClassMethod(state.Intern("sin"u8), new MRubyMethod(
(s, self) =>
{
var value = s.GetArgumentAsFloatAt(0);
return System.Math.Sin(value);
},
(s, self, argument) => System.Math.Sin(s.AsFloat(argument)),
(_, _, argument) => System.Math.Sin(argument)));
});
}

Expand Down Expand Up @@ -124,6 +200,22 @@ public void PreloadOptcarrotRun(int frames = 180, bool printResult = true)
currentChibiRubyIrep = CompileChibiRubySource(BuildOptcarrotRunSource(frames, printResult));
}

// Like PreloadOptcarrotBenchmark but AOT-compiles every statically-compilable
// optcarrot method to C# first, registering bodies by fingerprint and binding them
// to the definitions tree before it executes. Returns the number of methods compiled.
public int PreloadOptcarrotBenchmarkAot(int frames = 180, bool printResult = true)
{
var definitions = CompileChibiRubySource(BuildOptcarrotDefinitionsSource());
// Define the classes first so the AOT compiler can resolve self-sends against
// the real class hierarchy (for devirtualize+inline), then compile + bind.
mrubyCSState.Execute(definitions);
var compiled = OptcarrotAotCompiler.CompileAndRegister(mrubyCSState, definitions);
mrubyCSState.BindCompiledMethods(definitions);

PreloadOptcarrotRun(frames, printResult);
return compiled;
}

public MRubyValue RunChibiRuby()
{
return mrubyCSState.Execute(currentChibiRubyIrep!);
Expand Down
56 changes: 56 additions & 0 deletions sandbox/ChibiRuby.Benchmark/VariableTableAllocationBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using BenchmarkDotNet.Attributes;

namespace ChibiRuby.Benchmark;

[Config(typeof(BenchmarkConfig))]
public class VariableTableAllocationBenchmark
{
[Params(10_000)]
public int ObjectCount { get; set; }

[Benchmark(Baseline = true)]
public long FourIvarsInline() => VariableTableAllocationWorkload.FourIvarsInline(ObjectCount);

[Benchmark]
public long FiveIvarsPromoted() => VariableTableAllocationWorkload.FiveIvarsPromoted(ObjectCount);
}

static class VariableTableAllocationWorkload
{
static readonly Symbol X = new(10_001);
static readonly Symbol Y = new(10_002);
static readonly Symbol Z = new(10_003);
static readonly Symbol W = new(10_004);
static readonly Symbol Extra = new(10_005);

public static long FourIvarsInline(int objectCount)
{
long sum = 0;
for (var i = 0; i < objectCount; i++)
{
var table = new VariableTable();
table.Set(X, new MRubyValue(i));
table.Set(Y, new MRubyValue(i + 1));
table.Set(Z, new MRubyValue(i + 2));
table.Set(W, new MRubyValue(i + 3));
sum += table.Get(W).IntegerValue;
}
return sum;
}

public static long FiveIvarsPromoted(int objectCount)
{
long sum = 0;
for (var i = 0; i < objectCount; i++)
{
var table = new VariableTable();
table.Set(X, new MRubyValue(i));
table.Set(Y, new MRubyValue(i + 1));
table.Set(Z, new MRubyValue(i + 2));
table.Set(W, new MRubyValue(i + 3));
table.Set(Extra, new MRubyValue(i + 4));
sum += table.Get(Extra).IntegerValue;
}
return sum;
}
}
Loading
Loading