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
4 changes: 2 additions & 2 deletions benchmarks/IspcSharp.Benchmarks/IspcSharp.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -10,7 +10,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace IspcSharp.Generators;
namespace IspcSharp.Generators.Analyzers;

/// <summary>
/// Warns about Array-of-Structs access patterns inside SPMD kernels, the single most
Expand All @@ -19,16 +19,8 @@ namespace IspcSharp.Generators;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AosAccessAnalyzer : DiagnosticAnalyzer
{
public static readonly DiagnosticDescriptor AosAccess = new(
"ISPC100",
"Array-of-Structs access in SPMD kernel",
"'{0}' accesses a field through an indexed element, an AoS pattern that becomes a per-lane gather. Restructure to Structure-of-Arrays (separate float[] per field, or SoaFloat2/SoaFloat3) for contiguous vector loads.",
"IspcSharp.Performance",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(AosAccess);
=> [Descriptors.AosAccess];

public override void Initialize(AnalysisContext context)
{
Expand All @@ -52,7 +44,7 @@ private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext ctx)
if (!IsInsideSpmdKernel(ma))
return;

ctx.ReportDiagnostic(Diagnostic.Create(AosAccess, ma.GetLocation(), ma.ToString()));
ctx.ReportDiagnostic(Diagnostic.Create(Descriptors.AosAccess, ma.GetLocation(), ma.ToString()));
}

private static bool IsInsideSpmdKernel(SyntaxNode node)
Expand Down
104 changes: 104 additions & 0 deletions src/IspcSharp.Generators/Analyzers/SpmdDiagnosticsAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using IspcSharp.Generators.Exceptions;
using IspcSharp.Generators.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace IspcSharp.Generators.Analyzers;

/// <summary>
/// Reports every [Spmd]/[SpmdFunction] diagnostic (ISPC001–ISPC005 shape/support errors and
/// the ISPC101–ISPC104 performance warnings)
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class SpmdDiagnosticsAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [
Descriptors.NotPartial,
Descriptors.BadShape,
Descriptors.Unsupported,
Descriptors.BadParam,
Descriptors.NoParallel,
Descriptors.GatherPerf,
Descriptors.ScatterPerf,
Descriptors.IntDividePerf,
Descriptors.DoubleConvertPerf
];

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(static compilationContext =>
{
var tables = new Lazy<(Dictionary<string, StructInfo> Structs, Dictionary<string, FunctionInfo> Functions)>(
() => BuildTables(compilationContext.Compilation));

compilationContext.RegisterSyntaxNodeAction(
ctx => AnalyzeMethod(ctx, tables),
SyntaxKind.MethodDeclaration);
});
}

private static void AnalyzeMethod(
SyntaxNodeAnalysisContext ctx,
Lazy<(Dictionary<string, StructInfo> Structs, Dictionary<string, FunctionInfo> Functions)> tables)
{
var method = (MethodDeclarationSyntax)ctx.Node;
bool isKernel = SpmdGenerator.HasAttribute(method.AttributeLists, "Spmd");
bool isFunction = SpmdGenerator.HasAttribute(method.AttributeLists, "SpmdFunction");
if (!isKernel && !isFunction)
return;

var (structMap, fnMap) = tables.Value;
try
{
_ = isKernel
? SpmdGenerator.GenerateKernelSource(
KernelInfo.From(method), method, structMap, fnMap, ctx.ReportDiagnostic)
: SpmdGenerator.GenerateFunctionSource(
FunctionInfo.From(method), method, structMap, fnMap, ctx.ReportDiagnostic);
}
catch (UnsupportedConstructException ex)
{
ctx.ReportDiagnostic(Diagnostic.Create(
Descriptors.Unsupported, ex.Location ?? method.GetLocation(),
method.Identifier.Text, ex.What));
}
}

/// <summary>
/// Syntax-only scan for [SpmdStruct]/[SpmdFunction] declarations across the compilation
/// (the same syntactic parse the generator's transforms use).
/// </summary>
private static (Dictionary<string, StructInfo> Structs, Dictionary<string, FunctionInfo> Functions) BuildTables(
Compilation compilation)
{
var structs = new List<StructInfo>();
var functions = new List<FunctionInfo>();
foreach (var tree in compilation.SyntaxTrees)
{
foreach (var node in tree.GetRoot().DescendantNodes())
{
switch (node)
{
case StructDeclarationSyntax s when SpmdGenerator.HasAttribute(s.AttributeLists, "SpmdStruct"):
if (StructInfo.From(s) is { } si)
structs.Add(si);
break;
case MethodDeclarationSyntax m when SpmdGenerator.HasAttribute(m.AttributeLists, "SpmdFunction"):
functions.Add(FunctionInfo.From(m));
break;
default:
break;
}
}
}

return (SpmdGenerator.BuildStructMap(structs), SpmdGenerator.BuildFunctionMap(functions));
}
}
32 changes: 32 additions & 0 deletions src/IspcSharp.Generators/Contexts/LoopContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace IspcSharp.Generators.Contexts;

/// <summary>
/// Tracks break/continue state for the innermost loop being emitted.
/// For uniform loops (plain C# for), break/continue map to C# keywords.
/// For varying loops (mask iteration), break/continue manipulate masks.
/// </summary>
internal readonly struct LoopContext
{
public readonly string BreakMask;
public readonly string ContinueMask;
public readonly string LoopMask;
public readonly bool IsUniform;

public LoopContext(string breakMask, string continueMask, string loopMask)
{
BreakMask = breakMask;
ContinueMask = continueMask;
LoopMask = loopMask;
IsUniform = false;
}

private LoopContext(bool isUniform)
{
BreakMask = "";
ContinueMask = "";
LoopMask = "";
IsUniform = isUniform;
}

public static LoopContext Uniform() => new(true);
}
33 changes: 33 additions & 0 deletions src/IspcSharp.Generators/Contexts/ScaffoldContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using IspcSharp.Generators.Models;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace IspcSharp.Generators.Contexts;

internal sealed class ScaffoldContext(
CommonForEachStatementSyntax fe,
string loopVar,
string startExpr,
string endExpr,
string vectorBody,
string scalarBody,
List<ReductionInfo> reductions,
string laneCountExpr,
bool doubleMode,
bool longMode,
int unroll,
bool hasLaneReturns)
{
public readonly CommonForEachStatementSyntax Fe = fe;
public readonly string LoopVar = loopVar;
public readonly string StartExpr = startExpr;
public readonly string EndExpr = endExpr;
public readonly string VectorBody = vectorBody;
public readonly string ScalarBody = scalarBody;
public readonly string LaneCountExpr = laneCountExpr;
public readonly List<ReductionInfo> Reductions = reductions;
public readonly bool DoubleMode = doubleMode;
public readonly bool LongMode = longMode;
public readonly bool HasLaneReturns = hasLaneReturns;
public readonly int Unroll = unroll;
}
59 changes: 59 additions & 0 deletions src/IspcSharp.Generators/Descriptors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.CodeAnalysis;

namespace IspcSharp.Generators;

internal static class Descriptors
{
internal static readonly DiagnosticDescriptor NotPartial = new(
"ISPC001", "Containing type must be partial",
"[Spmd] method '{0}': the containing type must be declared 'partial' so generated code can be added",
"IspcSharp", DiagnosticSeverity.Error, true);

internal static readonly DiagnosticDescriptor BadShape = new(
"ISPC002", "Unsupported [Spmd] method shape",
"[Spmd] method '{0}': body must be [optional float/int/double locals], one 'foreach (var i in Spmd.Range(...))' or 'foreach (var (x, y) in Spmd.Range2D(...))' loop, then [optional trailing statements]",
"IspcSharp", DiagnosticSeverity.Error, true);

internal static readonly DiagnosticDescriptor Unsupported = new(
"ISPC003", "Construct not vectorizable",
"[Spmd] method '{0}': {1} is not supported by the SPMD vectorizer (v0.2 subset). Rewrite it, or use the IspcSharp runtime API directly for full control.",
"IspcSharp", DiagnosticSeverity.Error, true);

internal static readonly DiagnosticDescriptor BadParam = new(
"ISPC004", "Unsupported parameter type",
"[Spmd] method '{0}': parameter '{1}' has unsupported type '{2}'. Supported: float[]/int[]/double[], Span<T>/ReadOnlySpan<T> of float/int/double, and uniform float/int/double.",
"IspcSharp", DiagnosticSeverity.Error, true);

internal static readonly DiagnosticDescriptor NoParallel = new(
"ISPC005", "Parallel variant skipped",
"[Spmd] method '{0}': {0}_ParallelSimd was not generated: {1}",
"IspcSharp", DiagnosticSeverity.Info, true);

internal static readonly DiagnosticDescriptor GatherPerf = new(
"ISPC101", "Gather (non-contiguous load) in SPMD kernel",
"'{0}' uses a lane-varying index, lowered to a per-lane gather (Memory.Gather), not a contiguous vector load. Make it contiguous (loop-variable/affine index, a transposed or SoA layout, or presorted indices) to avoid the gather.",
"IspcSharp.Performance", DiagnosticSeverity.Warning, true);

internal static readonly DiagnosticDescriptor ScatterPerf = new(
"ISPC102", "Scatter (non-contiguous store) in SPMD kernel",
"'{0}' is a lane-varying indexed store, lowered to a per-active-lane scatter (Memory.Scatter); .NET has no hardware scatter instruction. Restructure to a contiguous write where possible.",
"IspcSharp.Performance", DiagnosticSeverity.Warning, true);

internal static readonly DiagnosticDescriptor IntDividePerf = new(
"ISPC103", "Per-lane integer divide in SPMD kernel",
"integer '{0}' has no SIMD instruction and runs as a per-lane scalar loop. Expect scalar-ish throughput here; if the divisor is a constant power of two use a shift/mask instead.",
"IspcSharp.Performance", DiagnosticSeverity.Warning, true);

internal static readonly DiagnosticDescriptor DoubleConvertPerf = new(
"ISPC104", "Double↔integer conversion in SPMD kernel",
"'{0}' converts between double and 64-bit integer lanes ((int)/(long) cast), which has no encoding before AVX-512DQ and runs per-lane on AVX2 (Zen 1–3, Haswell–Comet Lake). Keep the value in double, or move the conversion out of the hot loop.",
"IspcSharp.Performance", DiagnosticSeverity.Warning, true);

internal static readonly DiagnosticDescriptor AosAccess = new(
"ISPC100",
"Array-of-Structs access in SPMD kernel",
"'{0}' accesses a field through an indexed element, an AoS pattern that becomes a per-lane gather. Restructure to Structure-of-Arrays (separate float[] per field, or SoaFloat2/SoaFloat3) for contiguous vector loads.",
"IspcSharp.Performance",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
63 changes: 63 additions & 0 deletions src/IspcSharp.Generators/EquatableReadOnlyList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace IspcSharp.Generators;

/// <summary>
/// A read-only list with structural (sequence) equality. Incremental-pipeline models must be
/// equatable for the driver to cache them; a plain <see cref="List{T}"/> or
/// <see cref="System.Collections.Immutable.ImmutableArray{T}"/> compares by reference and
/// defeats caching, so models hold their collections through this wrapper instead.
/// </summary>
internal readonly struct EquatableReadOnlyList<T>(IReadOnlyList<T>? collection)
: IEquatable<EquatableReadOnlyList<T>>, IReadOnlyList<T>
{
private IReadOnlyList<T> Collection => collection ?? [];

public T this[int index] => Collection[index];

public int Count => Collection.Count;

public bool Equals(EquatableReadOnlyList<T> other)
=> this.SequenceEqual(other);

public override bool Equals(object? obj)
=> obj is EquatableReadOnlyList<T> other && Equals(other);

public override int GetHashCode()
{
unchecked
{
int hash = 17;
foreach (var item in Collection)
hash = (hash * 31) + (item?.GetHashCode() ?? 0);
return hash;
}
}

/// <summary>
/// Index of the first item matching <paramref name="predicate"/>, or -1.
/// </summary>
public int FindIndex(Func<T, bool> predicate)
{
for (int i = 0; i < Collection.Count; i++)
{
if (predicate(Collection[i]))
return i;
}

return -1;
}

public IEnumerator<T> GetEnumerator() => Collection.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

public static bool operator ==(EquatableReadOnlyList<T> left, EquatableReadOnlyList<T> right)
=> left.Equals(right);

public static bool operator !=(EquatableReadOnlyList<T> left, EquatableReadOnlyList<T> right)
=> !left.Equals(right);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;
using Microsoft.CodeAnalysis;

namespace IspcSharp.Generators.Exceptions;

internal sealed class UnsupportedConstructException(string what, Location? loc = null) : Exception
{
public string What { get; } = what;
public Location? Location { get; } = loc;
}
6 changes: 3 additions & 3 deletions src/IspcSharp.Generators/IspcSharp.Generators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<!-- Development Dependencies -->
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.102" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.301" PrivateAssets="All" />
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="All" />
</ItemGroup>

Expand Down
Loading
Loading