Skip to content
Merged
115 changes: 115 additions & 0 deletions sandbox/DotnetFiles/VerifyIsLessThanCodegen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#:sdk Microsoft.NET.Sdk
#:property TargetFramework=net10.0
#:property AllowUnsafeBlocks=true
#:project ../../src/SortAlgorithm

// Verify whether the type-specialized IsLessThan produces different codegen
// from the generic fallback (ComparableComparer<int>.Compare < 0).
//
// Run with: dotnet run sandbox/DotnetFiles/VerifyIsLessThanCodegen.cs
//
// To view JIT disassembly for specific methods, set:
// $env:DOTNET_JitDisasm = "WithSpecialization|WithoutSpecialization|ComparableOnly"
// dotnet run sandbox/DotnetFiles/VerifyIsLessThanCodegen.cs

using System.Diagnostics;
using System.Runtime.CompilerServices;
using SortAlgorithm.Algorithms;
using SortAlgorithm.Contexts;

// === Benchmark Setup ===
const int N = 100_000_000;
var rng = new Random(42);
var a = new int[1024];
var b = new int[1024];
for (int i = 0; i < a.Length; i++) { a[i] = rng.Next(); b[i] = rng.Next(); }

// Warmup
RunWithSpecialization(a, b, 1_000_000);
RunWithoutSpecialization(a, b, 1_000_000);
RunComparableOnly(a, b, 1_000_000);

// Benchmark
var sw = Stopwatch.StartNew();
var count1 = RunWithSpecialization(a, b, N);
var t1 = sw.Elapsed;

sw.Restart();
var count2 = RunWithoutSpecialization(a, b, N);
var t2 = sw.Elapsed;

sw.Restart();
var count3 = RunComparableOnly(a, b, N);
var t3 = sw.Elapsed;

Console.WriteLine($"=== IsLessThan codegen verification (N={N:N0}) ===");
Console.WriteLine($"WithSpecialization (Unsafe.As path) : {t1.TotalMilliseconds,8:F1} ms (count={count1})");
Console.WriteLine($"WithoutSpecialization (Compare < 0) : {t2.TotalMilliseconds,8:F1} ms (count={count2})");
Console.WriteLine($"ComparableOnly (raw CompareTo) : {t3.TotalMilliseconds,8:F1} ms (count={count3})");
Console.WriteLine();
Console.WriteLine($"Ratio without/with = {t2.TotalMilliseconds / t1.TotalMilliseconds:F3}");
Console.WriteLine($"Ratio comparable/with = {t3.TotalMilliseconds / t1.TotalMilliseconds:F3}");

if (Math.Abs(t2.TotalMilliseconds / t1.TotalMilliseconds - 1.0) < 0.05)
Console.WriteLine("\n=> Specialization makes NO measurable difference. JIT already optimizes CompareTo < 0.");
else if (t2.TotalMilliseconds > t1.TotalMilliseconds * 1.05)
Console.WriteLine("\n=> Specialization IS faster. The JIT does NOT fully optimize CompareTo < 0 in this context.");
else
Console.WriteLine("\n=> Specialization is slightly slower (noise or harmful).");

// === Method 1: Current code with Unsafe.As specialization ===
[MethodImpl(MethodImplOptions.NoInlining)]
static long RunWithSpecialization(int[] a, int[] b, int n)
{
long count = 0;
int mask = a.Length - 1;
for (int i = 0; i < n; i++)
{
// This mirrors the specialized path: Unsafe.As<T, int>(ref a) < Unsafe.As<T, int>(ref b)
if (WithSpecialization(a[i & mask], b[i & mask]))
count++;
}
return count;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool WithSpecialization(int x, int y) => x < y;

// === Method 2: Generic comparer fallback (ComparableComparer<int>.Compare < 0) ===
[MethodImpl(MethodImplOptions.NoInlining)]
static long RunWithoutSpecialization(int[] a, int[] b, int n)
{
long count = 0;
int mask = a.Length - 1;
for (int i = 0; i < n; i++)
{
if (WithoutSpecialization(a[i & mask], b[i & mask]))
count++;
}
return count;
}

// Simulates the generic comparer fallback: struct comparer → Compare → CompareTo → result < 0
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool WithoutSpecialization(int x, int y) => StructCompare(x, y) < 0;

// Mirrors ComparableComparer<int>.Compare — struct, AggressiveInlining, null check + CompareTo
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int StructCompare(int x, int y) => x.CompareTo(y);

// === Method 3: Direct int.CompareTo (no wrapper) ===
[MethodImpl(MethodImplOptions.NoInlining)]
static long RunComparableOnly(int[] a, int[] b, int n)
{
long count = 0;
int mask = a.Length - 1;
for (int i = 0; i < n; i++)
{
if (ComparableOnly(a[i & mask], b[i & mask]))
count++;
}
return count;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool ComparableOnly(int x, int y) => x.CompareTo(y) < 0;
120 changes: 120 additions & 0 deletions sandbox/DotnetFiles/VerifySortSpanSpecialization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#:sdk Microsoft.NET.Sdk
#:property TargetFramework=net10.0
#:property AllowUnsafeBlocks=true

// Faithfully reproduce SortSpan's comparison paths to verify specialization impact.
// Simulates: struct comparer → AggressiveInlining → CompareTo chain.
//
// Run: dotnet run sandbox/DotnetFiles/VerifySortSpanSpecialization.cs

using System.Diagnostics;
using System.Runtime.CompilerServices;

const int N = 100_000_000;
var rng = new Random(42);
var data = new int[4096];
for (int i = 0; i < data.Length; i++) data[i] = rng.Next();

// Warmup
Bench_Direct(data, 1_000_000);
Bench_CompareTo(data, 1_000_000);
Bench_StructComparer(data, 1_000_000);

// Actual runs (multiple iterations for stability)
double t1 = 0, t2 = 0, t3 = 0;
long c1 = 0, c2 = 0, c3 = 0;
const int runs = 5;
for (int r = 0; r < runs; r++)
{
var sw = Stopwatch.StartNew();
c1 = Bench_Direct(data, N);
t1 += sw.Elapsed.TotalMilliseconds;

sw.Restart();
c2 = Bench_CompareTo(data, N);
t2 += sw.Elapsed.TotalMilliseconds;

sw.Restart();
c3 = Bench_StructComparer(data, N);
t3 += sw.Elapsed.TotalMilliseconds;
}
t1 /= runs; t2 /= runs; t3 /= runs;

Console.WriteLine($"=== SortSpan comparison verification (N={N:N0}, {runs} runs avg) ===");
Console.WriteLine();
Console.WriteLine($"1. Unsafe.As<T,int> path (specialization) : {t1,8:F1} ms count={c1}");
Console.WriteLine($"2. int.CompareTo(int) < 0 (direct, no wrapper) : {t2,8:F1} ms count={c2}");
Console.WriteLine($"3. StructComparer.Compare(x,y) < 0 (full chain) : {t3,8:F1} ms count={c3}");
Console.WriteLine();
Console.WriteLine($"Ratio 2/1 (CompareTo / specialized) = {t2 / t1:F3}");
Console.WriteLine($"Ratio 3/1 (full chain / specialized) = {t3 / t1:F3}");
Console.WriteLine();

if (t3 / t1 > 1.10)
Console.WriteLine("=> CONFIRMED: Type specialization is effective. CompareTo < 0 does NOT fold to x < y.");
else
Console.WriteLine("=> Specialization makes no measurable difference.");

// === Path 1: Direct comparison (what Unsafe.As specialization produces) ===
[MethodImpl(MethodImplOptions.NoInlining)]
static long Bench_Direct(int[] data, int n)
{
long count = 0;
int mask = data.Length - 1;
for (int i = 0; i < n; i++)
{
if (DirectLessThan(data[i & mask], data[(i + 1) & mask]))
count++;
}
return count;
}

// After JIT specialization: Unsafe.As<T,int>(ref a) < Unsafe.As<T,int>(ref b) → a < b
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool DirectLessThan(int a, int b) => a < b;

// === Path 2: int.CompareTo < 0 (what the fallback produces after inlining) ===
[MethodImpl(MethodImplOptions.NoInlining)]
static long Bench_CompareTo(int[] data, int n)
{
long count = 0;
int mask = data.Length - 1;
for (int i = 0; i < n; i++)
{
if (CompareToLessThan(data[i & mask], data[(i + 1) & mask]))
count++;
}
return count;
}

// int.CompareTo(int) → {-1, 0, 1} → < 0
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool CompareToLessThan(int a, int b) => a.CompareTo(b) < 0;

// === Path 3: Full chain through struct comparer (mirrors SortSpan's actual path) ===
[MethodImpl(MethodImplOptions.NoInlining)]
static long Bench_StructComparer(int[] data, int n)
{
long count = 0;
int mask = data.Length - 1;
var comparer = new MyStructComparer();
for (int i = 0; i < n; i++)
{
if (StructComparerLessThan(data[i & mask], data[(i + 1) & mask], comparer))
count++;
}
return count;
}

// Mirrors: _comparer.Compare(a, b) < 0 where _comparer is struct
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool StructComparerLessThan<TComparer>(int a, int b, TComparer comparer)
where TComparer : IComparer<int>
=> comparer.Compare(a, b) < 0;

// Mirrors ComparableComparer<int>: readonly struct, IComparer<int>, AggressiveInlining
readonly struct MyStructComparer : IComparer<int>
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Compare(int x, int y) => x.CompareTo(y);
}
9 changes: 8 additions & 1 deletion src/SortAlgorithm/Algorithms/ComparableComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@

namespace SortAlgorithm.Algorithms;

/// <summary>
/// Marker interface for <see cref="ComparableComparer{T}"/>.
/// Allows <see cref="SortSpan{T,TComparer,TContext}"/> to detect the comparer type without
/// referencing <c>ComparableComparer&lt;T&gt;</c> from an unconstrained generic context.
/// </summary>
internal interface IComparableComparer { }

/// <summary>
/// A high-performance struct comparer that uses IComparable&lt;T&gt;.CompareTo for comparison.
/// This struct is used internally by convenience overloads to achieve maximum performance
Expand All @@ -26,7 +33,7 @@ namespace SortAlgorithm.Algorithms;
/// - Using ComparableComparer&lt;int&gt; (struct): ~0ns overhead, same as direct CompareTo
/// </para>
/// </remarks>
internal readonly struct ComparableComparer<T> : IComparer<T> where T : IComparable<T>
internal readonly struct ComparableComparer<T> : IComparer<T>, IComparableComparer where T : IComparable<T>
{
/// <summary>
/// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
Expand Down
Loading
Loading