Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
f868dc5
RubyGems: prepend the no-extension-build hook lazily, keep the instal…
Largo Oct 11, 2026
1efe273
Struct#initialize in C# for the common call; no lock to find an Integ…
Largo Oct 11, 2026
84d4047
OSR: instance variable reads in outlined loops, typed as numeric oper…
Largo Oct 11, 2026
c463a51
WIP: spinning fiber mailbox, cheap Fiber.current, fiber-free Enumerat…
Largo Oct 11, 2026
8db03bc
Bind keyword parameters with direct runtime calls
Largo Oct 11, 2026
3cac183
Precompiled dispatch for bare-identifier calls; cheaper dispatcher gu…
Largo Oct 11, 2026
5785ffe
Cache the GlobalVariable object per global variable reference
Largo Oct 11, 2026
b334b18
WIP: per-site storages for send/__send__/public_send and respond_to?
Largo Oct 11, 2026
2ae562d
Merge sp/startup-rubygems: keep the RubyGems installer stack out of boot
Largo Oct 12, 2026
cb00313
Merge sp/method-dispatch: precompiled dispatch for bare-identifier ca…
Largo Oct 12, 2026
c635c49
Skip the CLR stack walk for const_set locations while ruby4.rb boots
Largo Oct 12, 2026
bd5ef97
Blocks: call the compiled block directly, skip arity work for plain b…
Largo Oct 12, 2026
14be903
LightCompiler call-instruction cache hit; cheaper spans and args in t…
Largo Oct 11, 2026
09001d8
Look keyword arguments up by identity; build literal keyword hashes d…
Largo Oct 12, 2026
8df63f0
Give send/__send__/public_send and respond_to? per-site storages
Largo Oct 12, 2026
ed7d51b
Merge sp/ivars-attr: Struct#initialize in C#, lock-free instance-data…
Largo Oct 12, 2026
79b6de9
Merge sp/gvar-const: cache the GlobalVariable object per global varia…
Largo Oct 12, 2026
33a9d2b
Merge sp/blocks-yield: call the compiled block directly, skip arity w…
Largo Oct 12, 2026
2d39cbb
Merge sp/compile-pipeline: LightCompiler call-instruction cache, chea…
Largo Oct 12, 2026
fc23939
Merge sp/respond-send-mm: per-site storages for send/__send__/public_…
Largo Oct 12, 2026
488a16f
Merge sp/startup-prelude: skip the CLR stack walk for const_set locat…
Largo Oct 12, 2026
12c2c7f
Format raised exceptions' backtraces lazily; skip the prelude's set_b…
Largo Oct 12, 2026
c795799
WIP: map method bodies lazily, on first call
Largo Oct 12, 2026
a3319f9
WIP: bind respond_to?(:literal) to a constant and send(:literal) as a…
Largo Oct 12, 2026
4852971
WIP: prelude: alias keyword for alias_method, no guards on fresh Errn…
Largo Oct 12, 2026
8c4df44
WIP: prelude: set the Encoding constants from a recorded table
Largo Oct 12, 2026
6852c9a
WIP: point dispatchers at the compiled method body; keep line tracing…
Largo Oct 12, 2026
b452d06
Share call-site rules across receiver classes that resolve to the sam…
Largo Oct 12, 2026
1e9f217
Fiber: spinning C# mailbox, cheap Fiber.current; Enumerator#next over…
Largo Oct 12, 2026
069d5d0
Gate the keyword-argument binding speedups (8db03bcd, 09001d8e)
Largo Oct 12, 2026
ec97886
Precompile keyword calls to methods that take keywords
Largo Oct 12, 2026
0e70f13
Take apart the general keyword lowering's splat with direct runtime c…
Largo Oct 12, 2026
f91fe29
Build the empty default keyword hash without an item array
Largo Oct 12, 2026
a902e24
Blocks: no RubyBlockScope for a body that never uses it
Largo Oct 12, 2026
b9184c2
Merge sp/exceptions: format raised exceptions' backtraces lazily
Largo Oct 12, 2026
38de18d
Merge sp/fiber: spinning C# fiber mailbox, cheap Fiber.current, Enume…
Largo Oct 12, 2026
c5ab41c
Merge sp/kwargs: keyword-argument binding speedups
Largo Oct 12, 2026
2b0e3ad
Merge sp/blocks-yield: no RubyBlockScope for a block body that never …
Largo Oct 12, 2026
b8e2e6c
Merge sp/startup-binder: share call-site rules across receiver classe…
Largo Oct 12, 2026
00f6805
Merge sp/method-dispatch: point precompiled dispatchers at the compil…
Largo Oct 12, 2026
b8f98a4
FiberMailbox: spin up to 100us, pause after two misses, yield now and…
Largo Oct 12, 2026
aacf5f2
DLR: keep exception data in Exception.Data on .NET Core (0100-excepti…
Largo Oct 12, 2026
f20cae0
Reuse an interpreted frame's catch-site stack walk; cache :exception …
Largo Oct 12, 2026
19b24c7
Gate the keyword-call speedups (ec97886c, 0e70f13d, f91fe293)
Largo Oct 12, 2026
431297d
Share rules for Method#call and CLR receivers; fix a rebind loop
Largo Oct 12, 2026
c90d4a3
Class lookups without locks: a read cache for CLR types' classes, loc…
Largo Oct 12, 2026
d7994da
Merge sp/classof-cache: class lookups without locks (CLR class read c…
Largo Oct 12, 2026
7d31c2e
Merge sp/exceptions: reuse an interpreted frame's catch-site stack wa…
Largo Oct 12, 2026
b1f2a95
Merge sp/fiber: FiberMailbox spin tuning
Largo Oct 12, 2026
80f0145
Array: its own map/collect and select/filter, as in MRI; each without…
Largo Oct 12, 2026
24abeec
Merge sp/kwargs: more keyword-call speedups
Largo Oct 12, 2026
7ba5f3a
Merge sp/startup-binder: share rules for Method#call and CLR receiver…
Largo Oct 12, 2026
228ef04
Merge sp/startup-prelude: alias keyword instead of alias_method, chea…
Largo Oct 12, 2026
97e6ca8
Do not copy a block's arguments for a super it cannot make
Largo Oct 12, 2026
ca6b799
Take the lowered splat apart without reallocating; build **rest in on…
Largo Oct 12, 2026
de760e0
DLR: reach Exception._data directly instead of the virtual Data property
Largo Oct 12, 2026
370b4d7
Merge sp/compile-pipeline: map method bodies lazily, on first call
Largo Oct 12, 2026
06129b6
Gate the block and splat keyword speedups (97e6ca89, ca6b799b)
Largo Oct 12, 2026
87bfd70
Splat `f(**opts)` directly when opts is a Hash
Largo Oct 12, 2026
a98737a
Fiber: reuse the threads of finished fibers; release a fiber's mutexe…
Largo Oct 12, 2026
7ca39fc
Gate the `f(**opts)` splat speedup (87bfd70a)
Largo Oct 12, 2026
10499d0
Merge sp/blocks-yield: Array's own map/collect and select/filter; eac…
Largo Oct 12, 2026
4e2bfca
Merge sp/exceptions: reach Exception._data directly (DLR patch update)
Largo Oct 12, 2026
5e6f2e7
Merge sp/fiber: reuse the threads of finished fibers
Largo Oct 12, 2026
2a096fa
Merge sp/kwargs: f(**opts) splat speedup
Largo Oct 12, 2026
29aaec3
Bridge: f(**opts) splat assigns its argument slot (merge of compile-p…
Largo Oct 12, 2026
0c26d62
Merge `**h` into a keyword call's own hash in place
Largo Oct 12, 2026
5a1d9ba
Array#all?/any?/none?: walk the array, as MRI's do
Largo Oct 12, 2026
c6ea0f5
Attribute dispatchers: tell an exact RubyObject apart without the int…
Largo Oct 12, 2026
fbed1bd
Bind respond_to?(:literal) to a constant and send(:literal) as a dire…
Largo Oct 12, 2026
d7717dd
Merge sp/respond-send-mm: respond_to?(:literal) as a constant rule, s…
Largo Oct 12, 2026
eb8f1f5
Yield: a yield site in a loop reuses its BlockParam
Largo Oct 12, 2026
315bd23
Capture no backtrace for flow-control unwinders; snapshot interpreted…
Largo Oct 12, 2026
816e30f
Gate the in-place keyword splat merge (0c26d62d)
Largo Oct 12, 2026
3a6b66d
Don't share respond_to?/send rules across receiver classes
Largo Oct 12, 2026
8415312
Merge sp/blocks-yield: Array#all?/any?/none? walk the array; reuse a …
Largo Oct 12, 2026
37ae730
Merge sp/exceptions: no backtrace capture for internal flow-control u…
Largo Oct 12, 2026
3fdb08c
Merge sp/method-dispatch: attribute dispatcher fast path
Largo Oct 12, 2026
2e7cffe
Merge sp/kwargs: merge keyword splats into the call's own hash
Largo Oct 12, 2026
067bf1a
OSR: bind self into an outlined loop only when its body uses it
Largo Oct 12, 2026
b430571
Dispatcher guard: class and module receivers without the interface cast
Largo Oct 12, 2026
c5bb31e
Math: Float and Fixnum arguments in C#, the Ruby definitions for ever…
Largo Oct 12, 2026
88184fb
Regexp: work out once whether the pattern has a non-ASCII escape
Largo Oct 12, 2026
3bf02fb
Array#sort: Strings, Fixnums and Floats compared directly when <=> is…
Largo Oct 12, 2026
5ac5a36
Hash#each: one pass over the entries, no second lookup per key
Largo Oct 12, 2026
33e7775
Array literals: build the content array directly; adopt the one a lon…
Largo Oct 12, 2026
271979f
JIT: re-specialize after a method table change; module creation no lo…
Largo Oct 12, 2026
cb8a572
Smaller calls: keyword hashes without a bucket array; rare scope stat…
Largo Oct 12, 2026
fdeb554
Array#reverse_each and #rindex: a plain loop, and MRI's handling of a…
Largo Oct 12, 2026
843df62
String interpolation: literal parts made once; MRI's result encoding
Largo Oct 12, 2026
64f3cf2
WIP: method JIT specializes methods with keyword parameters
Largo Oct 13, 2026
61759b2
WIP: literal block Proc is its call's flow-control frame; smaller Pro…
Largo Oct 13, 2026
981aed3
WIP: JIT simple initializers - optional literal defaults, arity 6, st…
Largo Oct 13, 2026
0e995dd
Method JIT: specialize methods with keyword parameters
Largo Oct 13, 2026
15d8a02
WIP: tiered compiled regexes, Integer()/Float() String fast path, con…
Largo Oct 13, 2026
474bdc1
Literal block: its Proc is the call's flow-control frame; smaller Pro…
Largo Oct 13, 2026
76b0873
WIP: JIT keyword entry matches and binds in one call
Largo Oct 13, 2026
dfa68aa
WIP: MatchData group count cache + allocation-free offsets in StringS…
Largo Oct 13, 2026
bbc94ee
WIP: BlockParam state packed into 4 bytes (48 B)
Largo Oct 13, 2026
875e793
WIP: JIT initializers publish their opening ivar run in one slot array
Largo Oct 13, 2026
e19227e
JIT keyword entry: match and bind the keywords in one call
Largo Oct 13, 2026
5377669
WIP: a method's keyword prologue no longer turns a positional nil int…
Largo Oct 13, 2026
ba9d2aa
Keyword prologue of a method: a positional nil is not "no keywords"
Largo Oct 13, 2026
ef69ce4
Add strscan_tokens benchmark (StringScanner tokenizer loop)
Largo Oct 13, 2026
90d42db
WIP: Proc keeps its converter in the frame field (64 B)
Largo Oct 13, 2026
864fc28
WIP: keyword calls go straight into a specialized keyword method
Largo Oct 13, 2026
cf1ca9f
Regexp/StringScanner speedups for ruby-json: gate the three commits b…
Largo Oct 13, 2026
ee45fe8
WIP: StringScanner #skip/#match?/#exist?/#skip_until defer the MatchD…
Largo Oct 13, 2026
c308641
JIT simple initializers: optional literal defaults, arity 6, one-shot…
Largo Oct 13, 2026
e00db07
WIP: JIT array literals of up to five elements
Largo Oct 13, 2026
d630eb8
Proc 64 B, BlockParam 48 B: converter kept in the frame field, small …
Largo Oct 13, 2026
4cf8d3e
StringScanner: defer MatchData for length-answering scans; gate the c…
Largo Oct 13, 2026
0238aac
Receiverless keyword calls go straight into a specialized keyword method
Largo Oct 13, 2026
1790353
JIT: array literals of up to five elements (gates for the commit before)
Largo Oct 13, 2026
ff8cd43
Merge sp/w2-ruby-json: StringScanner deferred MatchData and literal s…
Largo Oct 13, 2026
fa27d2d
Merge sp/w2-block-pass: a literal block's Proc is its own flow-contro…
Largo Oct 13, 2026
4cfdac2
Merge sp/w2-object-alloc: JIT simple initializers (optional literal d…
Largo Oct 13, 2026
d6474b6
Merge sp/w2-jit-kwargs: method JIT specializes keyword methods; direc…
Largo Oct 13, 2026
b2beaa6
WIP: reuse block scopes (from sp/w2-block-scope) without growing Bloc…
Largo Oct 13, 2026
1d45fd0
WIP: keep the reuse bookkeeping off a block's first calls
Largo Oct 13, 2026
185b9be
Reuse block scopes across calls without slowing single-call blocks
Largo Oct 13, 2026
427206c
WIP: take a scope kept on the BlockParam inline
Largo Oct 13, 2026
dfbb66f
Gate the inline BlockParam scope reuse
Largo Oct 13, 2026
3ac3d35
WIP: take a block scope and its tuple together, after the safe point;…
Largo Oct 13, 2026
11e1c57
WIP: inline the block scope leave, test the capture only when parking
Largo Oct 13, 2026
f855805
WIP: no scope reuse for a block with a nested block literal
Largo Oct 13, 2026
78d4133
WIP: mark a scope captured where a non-literal Proc or an eval takes …
Largo Oct 13, 2026
19e596e
Gate the block scope reuse fix round (scope and tuple taken together,…
Largo Oct 13, 2026
844d2cf
Merge sp/w2b-block-scope: reuse a block's scope across the yields of …
Largo Oct 13, 2026
5525cc0
Merge branch 'master' into speedup
Largo Oct 13, 2026
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
59 changes: 59 additions & 0 deletions Build/dlr-patches/0100-compile-pipeline.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
diff --git a/src/core/Microsoft.Dynamic/Interpreter/Instructions/CallInstruction.cs b/src/core/Microsoft.Dynamic/Interpreter/Instructions/CallInstruction.cs
index 3b7bbb5..a979b25 100644
--- a/src/core/Microsoft.Dynamic/Interpreter/Instructions/CallInstruction.cs
+++ b/src/core/Microsoft.Dynamic/Interpreter/Instructions/CallInstruction.cs
@@ -25,6 +25,19 @@ namespace Microsoft.Scripting.Interpreter {

private static readonly Dictionary<MethodInfo, CallInstruction> _cache = new();

+ /// <summary>
+ /// The instruction <see cref="Create(MethodInfo, ParameterInfo[])"/> made and cached for <paramref name="info"/>
+ /// before, if any. A method is cached only once it is known to need none of the special handling Create
+ /// checks for (array accessors, by-ref parameters, value-type instance methods, dynamic methods, too many
+ /// arguments), so a hit can skip GetParameters() - which copies the parameter array on every call - and
+ /// those checks altogether.
+ /// </summary>
+ internal static bool TryGetCached(MethodInfo info, out CallInstruction instruction) {
+ lock (_cache) {
+ return _cache.TryGetValue(info, out instruction);
+ }
+ }
+
/// <exception cref="SecurityException">Instruction can't be created due to insufficient privileges.</exception>
public static CallInstruction Create(MethodInfo info) {
return Create(info, info.GetParameters());
diff --git a/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs b/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs
index 457ec85..84b88f7 100644
--- a/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs
+++ b/src/core/Microsoft.Dynamic/Interpreter/LightCompiler.cs
@@ -1153,6 +1153,19 @@ namespace Microsoft.Scripting.Interpreter {
private void CompileMethodCallExpression(Expression expr) {
var node = (MethodCallExpression)expr;

+ // A method compiled before has its instruction cached, which also says it takes no by-ref
+ // parameters and is not an instance method of a value type (see CallInstruction.TryGetCached).
+ if (CallInstruction.TryGetCached(node.Method, out CallInstruction cached)) {
+ if (!node.Method.IsStatic) {
+ Compile(node.Object);
+ }
+ foreach (var arg in node.Arguments) {
+ Compile(arg);
+ }
+ Instructions.Emit(cached);
+ return;
+ }
+
var parameters = node.Method.GetParameters();

// TODO:
@@ -1180,6 +1193,10 @@ namespace Microsoft.Scripting.Interpreter {
}

public void EmitCall(MethodInfo method) {
+ if (CallInstruction.TryGetCached(method, out CallInstruction cached)) {
+ Instructions.Emit(cached);
+ return;
+ }
EmitCall(method, method.GetParameters());
}

168 changes: 168 additions & 0 deletions Build/dlr-patches/0100-exceptions.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
diff --git a/src/core/Microsoft.Dynamic/Interpreter/InterpretedFrame.cs b/src/core/Microsoft.Dynamic/Interpreter/InterpretedFrame.cs
index 1a32e58..0a97958 100644
--- a/src/core/Microsoft.Dynamic/Interpreter/InterpretedFrame.cs
+++ b/src/core/Microsoft.Dynamic/Interpreter/InterpretedFrame.cs
@@ -146,10 +146,26 @@ namespace Microsoft.Scripting.Interpreter {

internal void SaveTraceToException(Exception exception) {
if (exception.GetData(typeof(InterpretedFrameInfo)) is null) {
- exception.SetData(typeof(InterpretedFrameInfo), GetStackTraceDebugInfo().ToArray());
+ exception.SetData(typeof(InterpretedFrameInfo), GetStackTraceDebugInfoArray());
}
}

+ // GetStackTraceDebugInfo().ToArray() without the iterator and the list it grows: every
+ // exception that passes an interpreted frame takes this snapshot of the whole chain.
+ private InterpretedFrameInfo[] GetStackTraceDebugInfoArray() {
+ int count = 0;
+ for (var frame = this; frame is not null; frame = frame.Parent) {
+ count++;
+ }
+
+ var result = new InterpretedFrameInfo[count];
+ int i = 0;
+ for (var frame = this; frame is not null; frame = frame.Parent) {
+ result[i++] = new InterpretedFrameInfo(frame.Name, frame.GetDebugInfo(frame.InstructionIndex));
+ }
+ return result;
+ }
+
public static InterpretedFrameInfo[] GetExceptionStackTrace(Exception exception) {
return exception.GetData(typeof(InterpretedFrameInfo)) as InterpretedFrameInfo[];
}
diff --git a/src/core/Microsoft.Dynamic/Utils/ExceptionUtils.cs b/src/core/Microsoft.Dynamic/Utils/ExceptionUtils.cs
index 7176c59..047827a 100644
--- a/src/core/Microsoft.Dynamic/Utils/ExceptionUtils.cs
+++ b/src/core/Microsoft.Dynamic/Utils/ExceptionUtils.cs
@@ -30,6 +30,131 @@ namespace Microsoft.Scripting.Utils {
public static void RemoveData(this Exception e, object key) {
e.Data.Remove(key);
}
+#elif NETCOREAPP
+ // Exception.Data's dictionary, as on the desktop: languages attach data to every exception they
+ // see go by (the interpreter its frames, IronRuby its exception state), several times per raise.
+ // The ConditionalWeakTable below costs a dependent handle per exception, which every GC has to
+ // scan, and a global lock plus a LINQ scan per lookup. The dictionary is not thread-safe, hence
+ // the lock on it - uncontended in practice.
+ //
+ // The dictionary is reached through Exception's own field where the runtime has it: the Data
+ // property is virtual, and the subclasses a language generates for its own exception classes
+ // override every virtual member to dispatch to the language first (IronRuby's do), which made a
+ // Data lookup cost microseconds. An exception type whose Data is not writable gets the table.
+ public static object GetData(this Exception e, object key) {
+ var data = GetDictionary(e, false);
+ if (data is null) {
+ return _hasTableData ? GetTableData(e, key) : null;
+ }
+ lock (data) {
+ return data[key];
+ }
+ }
+
+ public static void SetData(this Exception e, object key, object value) {
+ var data = GetDictionary(e, true);
+ if (data is null) {
+ SetTableData(e, key, value);
+ return;
+ }
+ lock (data) {
+ data[key] = value;
+ }
+ }
+
+ public static void RemoveData(this Exception e, object key) {
+ var data = GetDictionary(e, false);
+ if (data is null) {
+ if (_hasTableData) {
+ RemoveTableData(e, key);
+ }
+ return;
+ }
+ lock (data) {
+ data.Remove(key);
+ }
+ }
+
+ // null: no dictionary yet (and create is false), or one that cannot be written to
+ private static System.Collections.IDictionary GetDictionary(Exception e, bool create) {
+#if NET8_0_OR_GREATER
+ if (_dataFieldAvailable) {
+ try {
+ ref System.Collections.IDictionary field = ref DataField(e);
+ var dictionary = field;
+ if (dictionary is null) {
+ if (!create) {
+ return null;
+ }
+ Interlocked.CompareExchange(ref field, new System.Collections.Specialized.ListDictionary(), null);
+ dictionary = field;
+ }
+ return dictionary.IsReadOnly ? null : dictionary;
+ } catch (MissingFieldException) {
+ _dataFieldAvailable = false;
+ }
+ }
+#endif
+ var data = e.Data;
+ return (data is null || data.IsReadOnly) ? null : data;
+ }
+
+#if NET8_0_OR_GREATER
+ [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_data")]
+ private static extern ref System.Collections.IDictionary DataField(Exception e);
+
+ private static bool _dataFieldAvailable = true;
+#endif
+
+ private static volatile bool _hasTableData;
+
+ private static ConditionalWeakTable<Exception, List<KeyValuePair<object, object>>> _exceptionData;
+
+ private static void SetTableData(Exception e, object key, object value) {
+ _hasTableData = true;
+ if (_exceptionData is null) {
+ Interlocked.CompareExchange(ref _exceptionData, new ConditionalWeakTable<Exception, List<KeyValuePair<object, object>>>(), null);
+ }
+
+ lock (_exceptionData) {
+ var data = _exceptionData.GetOrCreateValue(e);
+ int index = data.FindIndex(entry => entry.Key == key);
+ if (index >= 0) {
+ data[index] = new KeyValuePair<object, object>(key, value);
+ } else {
+ data.Add(new KeyValuePair<object, object>(key, value));
+ }
+ }
+ }
+
+ private static object GetTableData(Exception e, object key) {
+ if (_exceptionData is null) {
+ return null;
+ }
+
+ lock (_exceptionData) {
+ if (!_exceptionData.TryGetValue(e, out List<KeyValuePair<object, object>> data)) {
+ return null;
+ }
+ return data.FirstOrDefault(entry => entry.Key == key).Value;
+ }
+ }
+
+ private static void RemoveTableData(Exception e, object key) {
+ if (_exceptionData is null) {
+ return;
+ }
+
+ lock (_exceptionData) {
+ if (!_exceptionData.TryGetValue(e, out List<KeyValuePair<object, object>> data)) {
+ return;
+ }
+ int index = data.FindIndex(entry => entry.Key == key);
+ if (index >= 0) {
+ data.RemoveAt(index);
+ }
+ }
+ }
#else
private static ConditionalWeakTable<Exception, List<KeyValuePair<object, object>>> _exceptionData;

144 changes: 143 additions & 1 deletion Src/Libraries/Builtins/ArrayOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,9 @@ internal static RubyArray SortInPlace(ComparisonStorage/*!*/ comparisonStorage,
var context = comparisonStorage.Context;

if (block == null) {
MergeSort(self, (x, y) => Protocols.Compare(comparisonStorage, x, y));
if (!TrySortWithoutDispatch(context, self)) {
MergeSort(self, (x, y) => Protocols.Compare(comparisonStorage, x, y));
}
} else {
object nonRefBreakResult = null;
try {
Expand All @@ -343,6 +345,146 @@ internal static RubyArray SortInPlace(ComparisonStorage/*!*/ comparisonStorage,
return self;
}

/// <summary>
/// sort without a block over an array of only Strings, only Fixnums or only Floats (no NaN) - the
/// common case - compares the elements directly instead of dispatching #<=> for every pair, as
/// long as the class's #<=> is still the built-in one and no element is an instance of a
/// subclass or has a singleton class. The comparisons are the ones those #<=> make, so the
/// order is the same; anything else goes the general way.
/// </summary>
private static bool TrySortWithoutDispatch(RubyContext/*!*/ context, RubyArray/*!*/ self) {
int count = self.Count;
// (#<=> called from a C# sort ignores refinements, as rb_funcall does in MRI's sort)
if (count < 2) {
return false;
}

object first = self[0];
if (first != null && first.GetType() == typeof(MutableString)) {
var stringClass = context.GetClass(typeof(MutableString));
var keys = new StringKey[count];
for (int i = 0; i < count; i++) {
object item = self[i];
if (item == null || item.GetType() != typeof(MutableString) || context.GetImmediateClassOf(item) != stringClass) {
return false;
}
keys[i] = new StringKey((MutableString)item);
}
if (!HasBuiltinComparison(stringClass)) {
return false;
}
SortByKeys(self, keys);
return true;
}

if (first is int) {
var keys = new int[count];
for (int i = 0; i < count; i++) {
object item = self[i];
if (!(item is int)) {
return false;
}
keys[i] = (int)item;
}
if (!HasBuiltinComparison(context.GetClassOf(first))) {
return false;
}
SortByKeys(self, keys);
return true;
}

if (first is double) {
var keys = new double[count];
for (int i = 0; i < count; i++) {
object item = self[i];
if (!(item is double) || Double.IsNaN((double)item)) {
return false;
}
keys[i] = (double)item;
}
if (!HasBuiltinComparison(context.GetClassOf(first))) {
return false;
}
SortByKeys(self, keys);
return true;
}

return false;
}

// A struct, so that the generic sort below is compiled for it and calls MutableString.CompareTo
// directly; over the class itself it would share one body and dispatch every comparison.
private struct StringKey : IComparable<StringKey> {
private readonly MutableString/*!*/ _value;
public StringKey(MutableString/*!*/ value) { _value = value; }
public int CompareTo(StringKey other) { return _value.CompareTo(other._value); }
}

// The same stable merge sort as MergeSort, over keys of one type compared by their own CompareTo
// (what String#<=>, Integer#<=> and Float#<=> do for two of them), carrying the elements along.
private static void SortByKeys<TKey>(RubyArray/*!*/ self, TKey[]/*!*/ keys) where TKey : IComparable<TKey> {
int count = keys.Length;
object[] items = new object[count];
self.CopyTo(items, 0);
MergeSortKeys(keys, items, new TKey[count], new object[count], 0, count);
for (int i = 0; i < count; i++) {
self[i] = items[i];
}
}

private static void MergeSortKeys<TKey>(TKey[]/*!*/ keys, object[]/*!*/ items, TKey[]/*!*/ keyBuffer, object[]/*!*/ itemBuffer,
int start, int length) where TKey : IComparable<TKey> {

if (length <= 12) {
// insertion sort, stable: an element moves left only past strictly greater ones
for (int i = start + 1; i < start + length; i++) {
TKey key = keys[i];
object item = items[i];
int j = i - 1;
while (j >= start && key.CompareTo(keys[j]) < 0) {
keys[j + 1] = keys[j];
items[j + 1] = items[j];
j--;
}
keys[j + 1] = key;
items[j + 1] = item;
}
return;
}

int half = length / 2;
MergeSortKeys(keys, items, keyBuffer, itemBuffer, start, half);
MergeSortKeys(keys, items, keyBuffer, itemBuffer, start + half, length - half);

int left = start, right = start + half, target = start;
int leftEnd = start + half, rightEnd = start + length;
while (left < leftEnd && right < rightEnd) {
if (keys[right].CompareTo(keys[left]) < 0) {
keyBuffer[target] = keys[right];
itemBuffer[target++] = items[right++];
} else {
keyBuffer[target] = keys[left];
itemBuffer[target++] = items[left++];
}
}
while (left < leftEnd) {
keyBuffer[target] = keys[left];
itemBuffer[target++] = items[left++];
}
while (right < rightEnd) {
keyBuffer[target] = keys[right];
itemBuffer[target++] = items[right++];
}

Array.Copy(keyBuffer, start, keys, start, length);
Array.Copy(itemBuffer, start, items, start, length);
}

private static bool HasBuiltinComparison(RubyClass/*!*/ cls) {
var method = cls.ResolveMethod("<=>", VisibilityContext.AllVisible);
return method.Found && method.Info is RubyLibraryMethodInfo;
}

/// <summary>
/// List&lt;T&gt;.Sort cannot be used here: it wraps whatever the comparer throws in an
/// InvalidOperationException (so a Ruby exception or a block's break never arrives
Expand Down
Loading
Loading