Skip to content
Merged
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
31 changes: 31 additions & 0 deletions sandbox/ChibiRuby.Benchmark/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using ChibiRuby;
using ChibiRuby.Benchmark;

// Profiling-friendly mode: warm up the VM, then run the measured optcarrot
Expand Down Expand Up @@ -73,6 +74,36 @@
// return;
// }

// Quick wall-clock runner for a single ruby script (no BenchmarkDotNet):
// --quick-script <file.rb> [iterations] [warmups]
// Prints per-iteration milliseconds and the median.
if (args is ["--quick-script", var scriptFile, ..])
{
var iterations = args.Length >= 3 && int.TryParse(args[2], out var it) ? it : 10;
var warmups = args.Length >= 4 && int.TryParse(args[3], out var w) ? w : 3;

using var loader = new RubyScriptLoader();
loader.PreloadScriptFromFile(scriptFile);
for (var i = 0; i < warmups; i++)
{
loader.RunChibiRuby();
}
var times = new double[iterations];
MRubyValue lastResult = default;
for (var i = 0; i < iterations; i++)
{
var sw = Stopwatch.StartNew();
lastResult = loader.RunChibiRuby();
sw.Stop();
times[i] = sw.Elapsed.TotalMilliseconds;
}
Array.Sort(times);
var median = times[iterations / 2];
Console.WriteLine($"result: {lastResult}");
Console.WriteLine($"median: {median:F3} ms (min {times[0]:F3} / max {times[^1]:F3}, n={iterations})");
return;
}

if (args is ["--quick-optcarrot", ..])
{
var frames = args.Length >= 2 && int.TryParse(args[1], out var parsedFrames)
Expand Down
85 changes: 85 additions & 0 deletions sandbox/ChibiRuby.Benchmark/ruby/bm_ivar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# ivar-access microbenchmark: an object with many instance variables
# (like optcarrot's PPU, which has ~79) where the hot ones sit late in
# insertion order — worst case for a linear key scan.
class IvarHeavy
def initialize
@pad0 = 0
@pad1 = 1
@pad2 = 2
@pad3 = 3
@pad4 = 4
@pad5 = 5
@pad6 = 6
@pad7 = 7
@pad8 = 8
@pad9 = 9
@pad10 = 10
@pad11 = 11
@pad12 = 12
@pad13 = 13
@pad14 = 14
@pad15 = 15
@pad16 = 16
@pad17 = 17
@pad18 = 18
@pad19 = 19
@pad20 = 20
@pad21 = 21
@pad22 = 22
@pad23 = 23
@pad24 = 24
@pad25 = 25
@pad26 = 26
@pad27 = 27
@pad28 = 28
@pad29 = 29
@pad30 = 30
@pad31 = 31
@pad32 = 32
@pad33 = 33
@pad34 = 34
@pad35 = 35
@pad36 = 36
@pad37 = 37
@pad38 = 38
@pad39 = 39
@pad40 = 40
@pad41 = 41
@pad42 = 42
@pad43 = 43
@pad44 = 44
@pad45 = 45
@pad46 = 46
@pad47 = 47
@pad48 = 48
@pad49 = 49
@pad50 = 50
@pad51 = 51
@pad52 = 52
@pad53 = 53
@pad54 = 54
@pad55 = 55
@pad56 = 56
@pad57 = 57
@pad58 = 58
@pad59 = 59
@x = 0
@y = 1
@z = 2
end

def step(n)
i = 0
while i < n
@x = @x + @y + @z
@y = @z + i
@z = @x - @y
@x = @x & 0xffff
i += 1
end
@x
end
end

h = IvarHeavy.new
h.step(400_000)
17 changes: 17 additions & 0 deletions src/ChibiRuby/Irep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ public class Irep
public Irep[] Children { get; init; } = [];
public CatchHandler[] CatchHandlers { get; init; } = [];

ushort[]? variableSlotCache;

/// <summary>
/// Per-symbol-index inline cache used by variable-access opcodes (GetIV/SetIV,
/// GetGV/SetGV, GetConst). Holds the last slot where the symbol was found in the
/// target <see cref="VariableTable"/>. Entries are guesses: every use is verified
/// against the table, so a stale value only costs the fallback lookup.
/// </summary>
internal ushort[] VariableSlotCache
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => variableSlotCache ?? CreateVariableSlotCache();
}

[MethodImpl(MethodImplOptions.NoInlining)]
ushort[] CreateVariableSlotCache() => variableSlotCache = new ushort[Symbols.Length];

/// <summary>
/// Source-position information recovered from the .mrb file's DBG section, if it was
/// emitted (controlled by <c>MRubyCompiler.Compile(..., debugInfo: true)</c>). Null
Expand Down
6 changes: 6 additions & 0 deletions src/ChibiRuby/MRubyState.Class.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ struct MethodCacheEntry
public Symbol MethodId;
public MRubyMethod Method;
public RClass? DefiningClass;

/// <summary>
/// Slot guess for <see cref="MRubyMethod.TrivialGetterIVarSymbol"/> lookups.
/// Verified against the receiver's table on every use, so staleness is harmless.
/// </summary>
public ushort TrivialGetterSlot;
}

public RClass SingletonClassOf(MRubyValue value)
Expand Down
97 changes: 82 additions & 15 deletions src/ChibiRuby/MRubyState.Vm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,30 @@ internal MRubyValue EvalUnder(MRubyValue self, RProc block, RClass c)
return self;
}

/// <summary>
/// Execute irep assuming the Stack values are placed
/// </summary>
// Inline-cache miss paths for variable-access opcodes. Kept out of line so the
// dispatch switch's hot case bodies stay small; the caches store the found slot,
// and every cached slot is re-verified by VariableTable.TryGetAt/TrySetAt.

[MethodImpl(MethodImplOptions.NoInlining)]
static MRubyValue GetVariableMiss(VariableTable table, Symbol id, ushort[] slotCache, int cacheIndex)
{
slotCache[cacheIndex] = unchecked((ushort)table.GetWithSlot(id, out var value));
return value;
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void SetVariableMiss(VariableTable table, Symbol id, MRubyValue value, ushort[] slotCache, int cacheIndex)
{
slotCache[cacheIndex] = unchecked((ushort)table.SetWithSlot(id, value));
}

[MethodImpl(MethodImplOptions.NoInlining)]
static MRubyValue TrivialGetterMiss(ref MethodCacheEntry entry, VariableTable table)
{
entry.TrivialGetterSlot = unchecked((ushort)table.GetWithSlot(entry.Method.TrivialGetterIVarSymbol, out var value));
return value;
}

/// <summary>
/// OP_SYMBOL slow path: intern the pool string and memoize it on the irep.
/// A cache owned by another state is replaced wholesale (owner id and symbol
Expand All @@ -511,6 +532,9 @@ static Symbol PoolSymbolMiss(MRubyState state, Irep irep, int poolIndex)
return symbol;
}

/// <summary>
/// Execute irep assuming the Stack values are placed
/// </summary>
internal MRubyValue Execute(Irep irep, int pc, int stackKeep, RException? injectedRaise = null)
{
Exception = null;
Expand Down Expand Up @@ -648,15 +672,28 @@ internal MRubyValue Execute(Irep irep, int pc, int stackKeep, RException? inject
Unsafe.Add(ref registers, a) = MRubyValue.False;
goto Next;
case OpCode.GetGV:
{
Markers.GetGV();
bb = OperandBB.Read(ref sequence, ref callInfo.ProgramCounter);
Unsafe.Add(ref registers, bb.A) = globalVariables.Get(Unsafe.Add(ref symbols, bb.B));
var slotCache = irep.VariableSlotCache;
if (!globalVariables.TryGetAt(slotCache[bb.B], Unsafe.Add(ref symbols, bb.B), out var value))
{
value = GetVariableMiss(globalVariables, Unsafe.Add(ref symbols, bb.B), slotCache, bb.B);
}
Unsafe.Add(ref registers, bb.A) = value;
goto Next;
}
case OpCode.SetGV:
{
Markers.SetGV();
bb = OperandBB.Read(ref sequence, ref callInfo.ProgramCounter);
globalVariables.Set(Unsafe.Add(ref symbols, bb.B), Unsafe.Add(ref registers, bb.A));
var slotCache = irep.VariableSlotCache;
if (!globalVariables.TrySetAt(slotCache[bb.B], Unsafe.Add(ref symbols, bb.B), Unsafe.Add(ref registers, bb.A)))
{
SetVariableMiss(globalVariables, Unsafe.Add(ref symbols, bb.B), Unsafe.Add(ref registers, bb.A), slotCache, bb.B);
}
goto Next;
}
case OpCode.GetSV:
Markers.GetSV();
bb = OperandBB.Read(ref sequence, ref callInfo.ProgramCounter);
Expand All @@ -668,18 +705,30 @@ internal MRubyValue Execute(Irep irep, int pc, int stackKeep, RException? inject
globalVariables.Set(Unsafe.Add(ref symbols, bb.B), Unsafe.Add(ref registers, bb.A));
goto Next;
case OpCode.GetIV:
{
Markers.GetIV();
bb = OperandBB.Read(ref sequence, ref callInfo.ProgramCounter);
Unsafe.Add(ref registers, bb.A) = Unsafe.Add(ref registers, 0).As<RObject>().InstanceVariables.Get(Unsafe.Add(ref symbols, bb.B));
var table = Unsafe.Add(ref registers, 0).As<RObject>().InstanceVariables;
var slotCache = irep.VariableSlotCache;
if (!table.TryGetAt(slotCache[bb.B], Unsafe.Add(ref symbols, bb.B), out var value))
{
value = GetVariableMiss(table, Unsafe.Add(ref symbols, bb.B), slotCache, bb.B);
}
Unsafe.Add(ref registers, bb.A) = value;
goto Next;
}
case OpCode.SetIV:
{
Markers.SetIV();
bb = OperandBB.Read(ref sequence, ref callInfo.ProgramCounter);
Unsafe.Add(ref registers, 0)
.As<RObject>()
.InstanceVariables.Set(Unsafe.Add(ref symbols, bb.B),
Unsafe.Add(ref registers, bb.A));
var table = Unsafe.Add(ref registers, 0).As<RObject>().InstanceVariables;
var slotCache = irep.VariableSlotCache;
if (!table.TrySetAt(slotCache[bb.B], Unsafe.Add(ref symbols, bb.B), Unsafe.Add(ref registers, bb.A)))
{
SetVariableMiss(table, Unsafe.Add(ref symbols, bb.B), Unsafe.Add(ref registers, bb.A), slotCache, bb.B);
}
goto Next;
}
case OpCode.GetCV:
Markers.GetCV();
bb = OperandBB.Read(ref sequence, ref callInfo.ProgramCounter);
Expand All @@ -698,17 +747,31 @@ internal MRubyValue Execute(Irep irep, int pc, int stackKeep, RException? inject
{
var id = Unsafe.Add(ref symbols, bb.B);
var c = callInfo.Proc?.Scope?.TargetClass ?? ObjectClass;
if (c.ClassInstanceVariables.TryGet(id, out var value))
var slotCache = irep.VariableSlotCache;
if (c.ClassInstanceVariables.TryGetAt(slotCache[bb.B], id, out var value))
{
registerA = value;
goto Next;
}

GetConstSlowPath(
this, ref registerA, ref callInfo, id, c);
GetConstMiss(
this, ref registerA, ref callInfo, id, c, slotCache, bb.B);

goto Next;

[MethodImpl(MethodImplOptions.NoInlining)]
static void GetConstMiss(MRubyState state, ref MRubyValue registerA, ref MRubyCallInfo callInfo, Symbol id, RClass c, ushort[] slotCache, int cacheIndex)
{
var slot = c.ClassInstanceVariables.GetWithSlot(id, out var found);
if (slot >= 0)
{
slotCache[cacheIndex] = unchecked((ushort)slot);
registerA = found;
return;
}
GetConstSlowPath(state, ref registerA, ref callInfo, id, c);
}

[MethodImpl(MethodImplOptions.NoInlining)]
static void GetConstSlowPath(MRubyState state, ref MRubyValue registerA, ref MRubyCallInfo callInfo, Symbol id, RClass c)
{
Expand Down Expand Up @@ -1571,8 +1634,12 @@ static VmSignal RaiseIf(MRubyState state, ref MRubyCallInfo callInfo, ref MRubyV
if (ce.Class == selfObj.Class && ce.MethodId == mid &&
ce.Method.TrivialGetterIVarSymbol.Value != 0)
{
Unsafe.Add(ref registers, bbb.A) = selfObj.InstanceVariables.Get(
ce.Method.TrivialGetterIVarSymbol);
var getterTable = selfObj.InstanceVariables;
if (!getterTable.TryGetAt(ce.TrivialGetterSlot, ce.Method.TrivialGetterIVarSymbol, out var getterValue))
{
getterValue = TrivialGetterMiss(ref ce, getterTable);
}
Unsafe.Add(ref registers, bbb.A) = getterValue;
goto Next;
}
}
Expand Down
Loading
Loading