From 51ea38c438a36484cc917307ff69da39773db279 Mon Sep 17 00:00:00 2001 From: Jeff Schwartz Date: Fri, 27 Feb 2026 17:00:46 -0800 Subject: [PATCH] Add simd-pattern-matching and simd-vector-math skills Add SKILL.md and eval.yaml for both SIMD skills under the new plugins/dotnet/skills/ and tests/dotnet/ directory structure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills/simd-pattern-matching/SKILL.md | 120 +++++ .../dotnet/skills/simd-vector-math/SKILL.md | 122 +++++ tests/dotnet/simd-pattern-matching/eval.yaml | 397 +++++++++++++++++ tests/dotnet/simd-vector-math/eval.yaml | 415 ++++++++++++++++++ 4 files changed, 1054 insertions(+) create mode 100644 plugins/dotnet/skills/simd-pattern-matching/SKILL.md create mode 100644 plugins/dotnet/skills/simd-vector-math/SKILL.md create mode 100644 tests/dotnet/simd-pattern-matching/eval.yaml create mode 100644 tests/dotnet/simd-vector-math/eval.yaml diff --git a/plugins/dotnet/skills/simd-pattern-matching/SKILL.md b/plugins/dotnet/skills/simd-pattern-matching/SKILL.md new file mode 100644 index 0000000000..f0790eb344 --- /dev/null +++ b/plugins/dotnet/skills/simd-pattern-matching/SKILL.md @@ -0,0 +1,120 @@ +--- +name: simd-pattern-matching +description: Optimizes scalar byte/string pattern matching in .NET 8+ with cross-platform Vector128/Vector256 SIMD intrinsics. Transforms hot-path scalar code into vectorized implementations — never generates greenfield. +--- + +# SIMD Pattern Matching Optimization + +> **STOP — Not everything is a SIMD opportunity.** String processing on small collections (`ToLower`, `Trim`, `Sort`, dedup on < 20 items), operations covered by framework APIs (`Span.IndexOf`, `SearchValues`, `System.Text.Ascii`), and code without large byte/char buffer loops are NOT candidates for manual SIMD. Using `Vector256` for ASCII lowercasing when `System.Text.Ascii.ToLower()` exists is harmful — it adds complexity for zero benefit. If the code does not contain a scalar loop over a byte/char buffer ≥ 64 bytes, report "no optimization opportunity" and stop immediately. + +> **Early exit:** If the code is simply reimplementing a framework API (`Span.IndexOf`, `MemoryExtensions.IndexOfAny`, `SearchValues`, etc.), replace with the API call and stop. Those are already SIMD-optimized internally. This skill is for cases that need manual vectorization. + +Scan an existing .NET 8+ codebase for scalar pattern matching code on hot paths that would benefit from manual SIMD vectorization using cross-platform `Vector128`/`Vector256` intrinsics. Focus on byte-level operations over large buffers where no existing framework API covers the operation. If no SIMD-eligible candidates exist, report that and stop. + +## Decision Gate (mandatory — do this FIRST, before writing any code) + +1. Does the code contain a scalar loop over a byte or char buffer? **If NO → stop, report "no optimization opportunity"** +2. Are the buffers ≥ 64 bytes on the hot path? **If NO → stop** +3. Is the operation already covered by a framework API (`IndexOf`, `Contains`, `SearchValues`, `System.Text.Ascii`)? **If YES → use that API instead and stop** +4. Is this string/object processing on small collections (< 20 items), not bulk buffer scanning? **If YES → stop, this skill does not apply** + +State your assessment before implementing: `[SIMD CANDIDATE: , Category ]` or `[NO SIMD OPPORTUNITY: ]`. Do NOT proceed to implementation without stating one of these. + +## When to Use + +- Character-class membership counting/classification in tight loops over large buffers (e.g., counting alphanumeric bytes, multi-range byte classification) +- Custom byte-range validation on large buffers (e.g., verifying all bytes are printable ASCII, valid hex digits) +- Approximate matching (Levenshtein distance in loops, edit-distance thresholding, pattern ≤ 63 bytes) +- Bulk byte scanning with custom logic that no framework API covers (e.g., multi-range classification, nibble-based lookup) + +## When NOT to Use — Report "no opportunity" instead + +- No existing scalar code to optimize (greenfield) +- Operation is already covered by a framework API (`IndexOf`, `Contains`, `SequenceEqual`, `SearchValues`) — use the API instead +- Code already uses `Vector256`/`Vector128`/`TensorPrimitives`/`Vector`/`SearchValues` +- Buffers consistently < 64 bytes +- Regex with back-tracking/capture groups/lookahead +- String processing on small collections (< 20 items) where HashSet/sort dominates +- `ReadOnlySpan` UTF-16 that can't convert to byte processing +- Code that doesn't process large byte/char buffers in loops + +## Pattern Categories + +**A — Character class membership:** `if` chains testing byte/char ranges in tight loops (`c >= 'a' && c <= 'z'`), lookup table arrays indexed by byte value. SIMD range comparison or nibble-lookup approach. + +**B — Byte-range validation:** Loops checking if every byte satisfies a condition (e.g., all printable ASCII, all valid Base64). SIMD subtract-and-compare for unsigned range checks across full vectors. + +**C — Approximate matching:** Levenshtein distance in loops, edit-distance thresholding (only if pattern ≤ 63 bytes). + +**D — Bulk byte counting/scanning:** Counting byte occurrences, scanning with custom multi-condition logic that no single framework API covers. + +## Transformation Rules + +### Assessment (do this quickly) +- **First:** Check if an existing framework API covers the operation — if so, use it and stop (early exit, no manual SIMD needed) +- Skip if buffer typically < 64 bytes +- Skip if already uses `Vector256`/`Vector128`/`TensorPrimitives`/`SearchValues` + +### Required imports (for manual SIMD only) +```csharp +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +``` + +Do NOT use platform-specific imports (`System.Runtime.Intrinsics.X86`, `System.Runtime.Intrinsics.Arm`). Use the cross-platform `Vector128`/`Vector256` APIs instead. + +### Dispatch pattern + +Use cross-platform hardware acceleration checks. Do NOT use platform-specific checks like `Avx2.IsSupported`, `Sse42.IsSupported`, or `AdvSimd.IsSupported`. + +```csharp +if (!Vector128.IsHardwareAccelerated || buffer.Length < Vector128.Count) +{ + // scalar fallback (never delete this path) +} +else if (Vector256.IsHardwareAccelerated && buffer.Length >= Vector256.Count) +{ + // Vector256 code path +} +else +{ + // Vector128 code path +} +``` + +### SIMD operations reference + +Use cross-platform `Vector128`/`Vector256` operations: +- **Create/broadcast:** `Vector128.Create(value)`, `Vector256.Create(value)` +- **Load/store:** `Vector128.LoadUnsafe(ref src, offset)`, `Vector256.StoreUnsafe(vec, ref dst, offset)` +- **Comparison:** `Vector128.Equals(a, b)`, `Vector128.GreaterThan(a, b)`, `Vector128.LessThan(a, b)` +- **Bitwise:** operators `&`, `|`, `^`, `~` +- **Mask extraction:** `vec.ExtractMostSignificantBits()` → `uint` bitmask +- **Population count:** `BitOperations.PopCount(mask)` for counting matches +- **Shuffle:** `Vector128.Shuffle(vec, indices)` for nibble-lookup tables + +### Vectorized range comparison +Broadcast range bounds → subtract lower bound (wraps for out-of-range bytes in unsigned arithmetic) → compare against range width. For validation: check all lanes pass. For counting: `ExtractMostSignificantBits` → `PopCount`. + +### Nibble-lookup for character classes +Build two 16-entry lookup tables (high/low nibble). `Vector128.Shuffle` with nibble index → AND results → `ExtractMostSignificantBits` + `PopCount` for counting. + +### Memory access pattern +Head (scalar for pre-vector bytes) → Body (`Vector256.LoadUnsafe` / `Vector128.LoadUnsafe`) → Tail (overlapping last-vector for idempotent operations, or scalar remainder). Always use `ref MemoryMarshal.GetReference(span)` and `LoadUnsafe(ref T, nuint elementOffset)`. + +## Validation (required) + +```bash +dotnet build -c Release -warnaserror +dotnet test -c Release +``` + +If no tests exist for the method, add tests for: empty input, input < vector width, match at start/end, no match, input of exactly one vector width. **All existing tests must pass.** + +## Key Rules +- Preserve original method signature — drop-in replacement +- Never delete scalar code — it's the fallback +- Use cross-platform `Vector128`/`Vector256` APIs — never platform-specific intrinsics (`Avx2`, `Sse42`, `AdvSimd`) +- If no SIMD candidates found, report "no optimization opportunity" and explain why +- Skip categories with no matching code — don't generate from scratch diff --git a/plugins/dotnet/skills/simd-vector-math/SKILL.md b/plugins/dotnet/skills/simd-vector-math/SKILL.md new file mode 100644 index 0000000000..1f8d229b49 --- /dev/null +++ b/plugins/dotnet/skills/simd-vector-math/SKILL.md @@ -0,0 +1,122 @@ +--- +name: simd-vector-math +description: Optimizes scalar float/double vector math in .NET 8+ with cross-platform Vector128/Vector256 SIMD intrinsics. Transforms hot-path scalar code into vectorized implementations — never generates greenfield. +--- + +# SIMD Vector Math Optimization + +> **STOP — Not everything is a SIMD opportunity.** String processing (`ToLower`, `Trim`, `Sort`), small collections (< 20 items), and operations covered by framework APIs (`System.Text.Ascii`, `TensorPrimitives`, `SearchValues`) are NOT candidates for manual SIMD. Using `Vector256` for ASCII lowercasing when `System.Text.Ascii.ToLower()` exists is harmful — it adds complexity for zero benefit. If the code does not contain a scalar loop over `float[]`/`double[]`/`byte[]` arrays ≥ 16 elements, report "no optimization opportunity" and stop immediately. + +> **Early exit:** If the code is computing a standard operation covered by `TensorPrimitives` (dot product, cosine similarity, softmax, element-wise add/multiply, etc.), replace with the `TensorPrimitives` call and stop. Those are already SIMD-optimized internally. This skill is for cases that need manual vectorization. + +Scan an existing .NET 8+ codebase for scalar floating-point vector math on hot paths that would benefit from manual SIMD vectorization using cross-platform `Vector128`/`Vector256` intrinsics. Focus on operations where no existing framework API covers the computation. If no SIMD-eligible candidates exist, report that and stop. + +## Decision Gate (mandatory — do this FIRST, before writing any code) + +1. Does the code contain a scalar loop over numeric arrays (`float[]`, `double[]`, `byte[]`, `int[]`)? **If NO → stop, report "no optimization opportunity"** +2. Are the arrays ≥ 16 elements on the hot path? **If NO → stop** +3. Is the operation already covered by `TensorPrimitives` or another framework API? **If YES → use that API instead and stop** +4. Is this string/object processing, not numeric array math? **If YES → stop, this skill does not apply** + +State your assessment before implementing: `[SIMD CANDIDATE: , Category ]` or `[NO SIMD OPPORTUNITY: ]`. Do NOT proceed to implementation without stating one of these. + +## When to Use + +- Multi-array fused computations with 3+ input arrays where no single `TensorPrimitives` call applies (e.g., weighted distance, fused multiply-accumulate with per-element parameters) +- Cross-type conversions combined with arithmetic (e.g., quantized int8→float dequantization with scale/offset) +- Custom distance metrics or similarity functions not covered by `TensorPrimitives` +- Domain-specific float processing with no suitable framework API (e.g., specialized quantization, custom activation functions) + +## When NOT to Use — Report "no opportunity" instead + +- No existing scalar code to optimize (greenfield) +- Operation is covered by `TensorPrimitives` (dot product, cosine similarity, softmax, element-wise arithmetic, etc.) — use it instead +- Code already uses `TensorPrimitives`, `Vector256`, `Vector128`, or `Vector` +- Vectors consistently < 16 floats (64 bytes) +- Bottleneck is memory bandwidth, not compute +- String/object processing with small collections — no float array math +- Code requires `decimal` or arbitrary-precision arithmetic + +## Pattern Categories + +**A — Multi-input fused computations:** Operations on 3+ parallel arrays that require a single pass for efficiency (e.g., weighted Euclidean distance: `sum += w[i] * (a[i] - b[i])²`). No `TensorPrimitives` method handles these directly. + +**B — Cross-type conversions:** Converting between integer and float types combined with arithmetic (e.g., quantized dequantization: `output[i] = (quantized[i] - zeroPoint) * scale`). Requires SIMD widening and type conversion intrinsics. + +**C — Custom distance/similarity metrics:** Domain-specific distance functions not in `TensorPrimitives` (e.g., Mahalanobis distance, weighted Minkowski distance). + +**D — Batch lookup/gather:** Embedding table lookups, scatter/gather, quantized dequantization. No framework API typically exists. + +## Transformation Rules + +### Assessment (do this quickly) +- **First:** Check if `TensorPrimitives` or another existing API covers the operation — if so, use it and stop (early exit, no manual SIMD needed) +- Skip if vectors typically < 16 floats +- Skip if already uses `TensorPrimitives`/`Vector256`/`Vector` + +### Required imports +```csharp +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +``` + +Do NOT use platform-specific imports (`System.Runtime.Intrinsics.X86`, `System.Runtime.Intrinsics.Arm`). Use the cross-platform `Vector128`/`Vector256` APIs instead. + +### Dispatch pattern + +Use cross-platform hardware acceleration checks. Do NOT use platform-specific checks like `Avx2.IsSupported`, `Fma.IsSupported`, or `AdvSimd.IsSupported`. + +```csharp +if (!Vector128.IsHardwareAccelerated || data.Length < Vector128.Count) +{ + // scalar fallback (never delete this path) +} +else if (Vector256.IsHardwareAccelerated && data.Length >= Vector256.Count) +{ + // Vector256 code path +} +else +{ + // Vector128 code path +} +``` + +### SIMD operations reference + +Use cross-platform `Vector128`/`Vector256` operations: +- **Create/broadcast:** `Vector128.Create(value)` to broadcast scalar to all lanes +- **Load/store:** `Vector128.LoadUnsafe(ref src, offset)`, `Vector128.StoreUnsafe(vec, ref dst, offset)` +- **Arithmetic:** operators `+`, `-`, `*`, `/` on vector types +- **FMA:** `Vector128.MultiplyAddEstimate(a, b, c)` for fused multiply-add +- **Min/Max:** `Vector128.Min(a, b)`, `Vector128.Max(a, b)` +- **Horizontal sum:** `Vector128.Sum(vec)` for final reduction +- **Type conversion:** `Vector128.WidenLower(v)` / `Vector128.WidenUpper(v)` for widening, `Vector128.ConvertToSingle(intVec)` for int→float + +### Multi-input fused computation pattern +Load from multiple arrays in the same loop, perform fused arithmetic, accumulate into vector accumulator(s). Use `Vector128.Sum` for final horizontal reduction. Always handle the loop remainder with scalar code for accumulations (do NOT use overlapping-vector to avoid double-counting). + +### Cross-type conversion pattern +For byte→float: Load `Vector128` (16 bytes) → `WidenLower`/`WidenUpper` to `Vector128` → widen again to `Vector128` → `ConvertToSingle` to `Vector128`. Process 4 float vectors per byte vector load. + +### Memory access pattern +Process data in vector-width chunks. For accumulations (reductions): use scalar remainder for remaining elements after the last full vector. For element-wise transforms: overlapping last-vector is safe if the operation is idempotent. Always use `ref MemoryMarshal.GetReference(span)` and `LoadUnsafe(ref T, nuint elementOffset)`. + +Use `Assert.Equal(expected, actual, precision: 5)` for float comparisons in tests, since SIMD may reorder floating-point additions. + +## Validation (required) + +```bash +dotnet build -c Release -warnaserror +dotnet test -c Release +``` + +If no tests exist, add tests for: empty array, array < vector width, exactly one vector width, large array, special values (0, NaN). **All existing tests must pass.** + +## Key Rules +- Preserve original method signature — drop-in replacement +- Never delete scalar code — it's the fallback +- Use cross-platform `Vector128`/`Vector256` APIs — never platform-specific intrinsics (`Avx2`, `Fma`, `Sse`, `AdvSimd`) +- If no SIMD candidates found, report "no optimization opportunity" and explain why +- Skip categories with no matching code — don't generate from scratch diff --git a/tests/dotnet/simd-pattern-matching/eval.yaml b/tests/dotnet/simd-pattern-matching/eval.yaml new file mode 100644 index 0000000000..126ab13b4b --- /dev/null +++ b/tests/dotnet/simd-pattern-matching/eval.yaml @@ -0,0 +1,397 @@ +scenarios: + - name: "Optimize printable ASCII validation" + prompt: "This .NET 8 project has a method that validates whether all bytes in a buffer are printable ASCII (0x20-0x7E). It works correctly but profiling shows it's a bottleneck when validating multi-megabyte network payloads. Please optimize IsAllPrintableAscii for maximum throughput on large inputs. Keep the same public API and make sure all existing tests still pass." + setup: + files: + - path: "AsciiCheck/AsciiCheck.csproj" + content: | + + + net8.0 + enable + enable + + + - path: "AsciiCheck/AsciiValidator.cs" + content: | + namespace AsciiCheck; + + public static class AsciiValidator + { + /// + /// Returns true if every byte in is printable ASCII + /// (0x20 through 0x7E inclusive). + /// + public static bool IsAllPrintableAscii(ReadOnlySpan data) + { + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + if (b < 0x20 || b > 0x7E) + return false; + } + return true; + } + } + - path: "AsciiCheck.Tests/AsciiCheck.Tests.csproj" + content: | + + + net8.0 + enable + enable + false + true + + + + + + + + + + + - path: "AsciiCheck.Tests/AsciiValidatorTests.cs" + content: | + using AsciiCheck; + + namespace AsciiCheck.Tests; + + public class AsciiValidatorTests + { + [Fact] + public void EmptyInput_ReturnsTrue() + => Assert.True(AsciiValidator.IsAllPrintableAscii(ReadOnlySpan.Empty)); + + [Fact] + public void AllPrintable_ReturnsTrue() + => Assert.True(AsciiValidator.IsAllPrintableAscii("Hello, World! 123"u8)); + + [Fact] + public void ContainsNull_ReturnsFalse() + => Assert.False(AsciiValidator.IsAllPrintableAscii("Hello\0World"u8)); + + [Fact] + public void ContainsNewline_ReturnsFalse() + => Assert.False(AsciiValidator.IsAllPrintableAscii("Hello\nWorld"u8)); + + [Fact] + public void ContainsHighByte_ReturnsFalse() + { + byte[] data = { 0x41, 0x42, 0xFF, 0x43 }; + Assert.False(AsciiValidator.IsAllPrintableAscii(data)); + } + + [Fact] + public void BoundaryLow_0x1F_Invalid() + { + byte[] data = { 0x1F }; + Assert.False(AsciiValidator.IsAllPrintableAscii(data)); + } + + [Fact] + public void BoundaryLow_0x20_Valid() + { + byte[] data = { 0x20 }; + Assert.True(AsciiValidator.IsAllPrintableAscii(data)); + } + + [Fact] + public void BoundaryHigh_0x7E_Valid() + { + byte[] data = { 0x7E }; + Assert.True(AsciiValidator.IsAllPrintableAscii(data)); + } + + [Fact] + public void BoundaryHigh_0x7F_Invalid() + { + byte[] data = { 0x7F }; + Assert.False(AsciiValidator.IsAllPrintableAscii(data)); + } + + [Fact] + public void LargeValidBuffer() + { + var data = new byte[8192]; + data.AsSpan().Fill(0x41); // 'A' + Assert.True(AsciiValidator.IsAllPrintableAscii(data)); + } + + [Fact] + public void LargeBuffer_InvalidAtEnd() + { + var data = new byte[8192]; + data.AsSpan().Fill(0x41); + data[^1] = 0x01; + Assert.False(AsciiValidator.IsAllPrintableAscii(data)); + } + + [Fact] + public void ShortInput_BelowVectorWidth() + => Assert.True(AsciiValidator.IsAllPrintableAscii("Hi"u8)); + } + assertions: + - type: "output_contains" + value: "Vector" + - type: "exit_success" + rubric: + - "Identifies the scalar byte-by-byte range check as a SIMD vectorization candidate (Category B — byte-range validation)" + - "Introduces cross-platform Vector128/Vector256 SIMD intrinsics for the range comparison" + - "Preserves the existing public API signature (IsAllPrintableAscii with ReadOnlySpan parameter)" + - "Handles edge cases correctly (empty input, short input below vector width, invalid byte at buffer boundaries)" + - "Preserves scalar fallback path for short inputs or non-accelerated hardware" + - "Existing unit tests still pass after the optimization" + timeout: 180 + + - name: "Optimize scalar character class counting" + prompt: "This .NET 8 project has a method that counts ASCII alphanumeric bytes in a buffer. It works correctly but profiling shows it's a bottleneck when processing large log files (100MB+). Please optimize CountAlphanumeric for better throughput. All existing tests must continue to pass." + setup: + files: + - path: "CharCount/CharCount.csproj" + content: | + + + net8.0 + enable + enable + + + - path: "CharCount/AlphanumericCounter.cs" + content: | + namespace CharCount; + + public static class AlphanumericCounter + { + /// + /// Counts how many bytes in are ASCII alphanumeric + /// (a-z, A-Z, 0-9). + /// + public static int CountAlphanumeric(ReadOnlySpan data) + { + int count = 0; + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + if ((b >= (byte)'a' && b <= (byte)'z') || + (b >= (byte)'A' && b <= (byte)'Z') || + (b >= (byte)'0' && b <= (byte)'9')) + { + count++; + } + } + return count; + } + } + - path: "CharCount.Tests/CharCount.Tests.csproj" + content: | + + + net8.0 + enable + enable + false + true + + + + + + + + + + + - path: "CharCount.Tests/AlphanumericCounterTests.cs" + content: | + using CharCount; + + namespace CharCount.Tests; + + public class AlphanumericCounterTests + { + [Fact] + public void EmptyInput_ReturnsZero() + => Assert.Equal(0, AlphanumericCounter.CountAlphanumeric(ReadOnlySpan.Empty)); + + [Fact] + public void AllAlphanumeric() + => Assert.Equal(12, AlphanumericCounter.CountAlphanumeric("Hello12World"u8)); + + [Fact] + public void NoAlphanumeric() + => Assert.Equal(0, AlphanumericCounter.CountAlphanumeric("!@#$%^&*()"u8)); + + [Fact] + public void MixedContent() + { + byte[] mixed = { (byte)'a', (byte)'.', (byte)'b', (byte)'\t', + (byte)'c', (byte)'!', (byte)'d', (byte)'9', (byte)'e' }; + Assert.Equal(6, AlphanumericCounter.CountAlphanumeric(mixed)); + } + + [Fact] + public void LargeBuffer() + { + var data = new byte[8192]; + // Fill with 'A' (all alphanumeric) + data.AsSpan().Fill((byte)'A'); + Assert.Equal(8192, AlphanumericCounter.CountAlphanumeric(data)); + } + + [Fact] + public void ShortInput_UnderVectorWidth() + => Assert.Equal(3, AlphanumericCounter.CountAlphanumeric("a1b"u8)); + + [Fact] + public void BoundaryValues() + { + // Test chars just outside alphanumeric ranges + byte[] edge = { (byte)'/' , (byte)'0', (byte)'9', (byte)':', + (byte)'@', (byte)'A', (byte)'Z', (byte)'[', + (byte)'`', (byte)'a', (byte)'z', (byte)'{' }; + // '0','9','A','Z','a','z' are alphanumeric = 6 + Assert.Equal(6, AlphanumericCounter.CountAlphanumeric(edge)); + } + } + assertions: + - type: "output_contains" + value: "Vector" + - type: "exit_success" + rubric: + - "Identifies the scalar byte-by-byte loop as a SIMD vectorization candidate (Category A — character class membership)" + - "Introduces cross-platform Vector128/Vector256 SIMD intrinsics for the range checks" + - "Does not use platform-specific intrinsics (Avx2, Sse42, AdvSimd)" + - "Preserves the existing public API signature" + - "Handles inputs shorter than one vector width correctly" + - "Existing unit tests still pass after the optimization" + timeout: 180 + + - name: "No optimization opportunity — config file parser" + prompt: "This .NET 8 project parses simple key=value config files. It's used at application startup to load settings. Please review the code and optimize for performance if possible. All existing tests must continue to pass." + setup: + files: + - path: "ConfigParser/ConfigParser.csproj" + content: | + + + net8.0 + enable + enable + + + - path: "ConfigParser/SimpleConfig.cs" + content: | + namespace ConfigParser; + + public static class SimpleConfig + { + /// + /// Parses lines of "key=value" text into a dictionary. + /// Ignores blank lines and lines starting with '#'. + /// + public static Dictionary Parse(string content) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var rawLine in content.Split('\n')) + { + var line = rawLine.Trim(); + if (line.Length == 0 || line[0] == '#') + continue; + + int eq = line.IndexOf('='); + if (eq < 0) continue; + + var key = line[..eq].Trim(); + var value = line[(eq + 1)..].Trim(); + + if (key.Length > 0) + result[key] = value; + } + + return result; + } + } + - path: "ConfigParser.Tests/ConfigParser.Tests.csproj" + content: | + + + net8.0 + enable + enable + false + true + + + + + + + + + + + - path: "ConfigParser.Tests/SimpleConfigTests.cs" + content: | + using ConfigParser; + + namespace ConfigParser.Tests; + + public class SimpleConfigTests + { + [Fact] + public void ParsesKeyValue() + { + var cfg = SimpleConfig.Parse("host=localhost\nport=8080"); + Assert.Equal("localhost", cfg["host"]); + Assert.Equal("8080", cfg["port"]); + } + + [Fact] + public void IgnoresComments() + { + var cfg = SimpleConfig.Parse("# comment\nkey=value"); + Assert.Single(cfg); + Assert.Equal("value", cfg["key"]); + } + + [Fact] + public void IgnoresBlankLines() + { + var cfg = SimpleConfig.Parse("\n\nkey=value\n\n"); + Assert.Single(cfg); + } + + [Fact] + public void TrimsWhitespace() + { + var cfg = SimpleConfig.Parse(" key = value "); + Assert.Equal("value", cfg["key"]); + } + + [Fact] + public void EmptyInput() + { + var cfg = SimpleConfig.Parse(""); + Assert.Empty(cfg); + } + + [Fact] + public void CaseInsensitiveKeys() + { + var cfg = SimpleConfig.Parse("Key=one\nKEY=two"); + Assert.Single(cfg); + Assert.Equal("two", cfg["key"]); + } + } + assertions: + - type: "exit_success" + rubric: + - "Correctly identifies that this code has no meaningful SIMD optimization opportunity" + - "Explains why SIMD is not applicable (small string inputs, branching logic, dictionary overhead dominates)" + - "Does not introduce unnecessary SIMD code that adds complexity without benefit" + - "Existing tests still pass — code is not broken by unnecessary changes" + timeout: 180 + diff --git a/tests/dotnet/simd-vector-math/eval.yaml b/tests/dotnet/simd-vector-math/eval.yaml new file mode 100644 index 0000000000..e7571679cb --- /dev/null +++ b/tests/dotnet/simd-vector-math/eval.yaml @@ -0,0 +1,415 @@ +scenarios: + - name: "Optimize weighted Euclidean distance" + prompt: "This .NET 8 project has a weighted Euclidean distance method used in a recommendation engine that computes similarity scores with per-dimension feature weights. The vectors are typically 512-dimensional float arrays and the method is called thousands of times per query. It's currently a bottleneck. Please optimize WeightedEuclideanDistance for maximum throughput. Keep the same public API and make sure all existing tests still pass." + setup: + files: + - path: "WeightedDist/WeightedDist.csproj" + content: | + + + net8.0 + enable + enable + + + - path: "WeightedDist/VectorOps.cs" + content: | + namespace WeightedDist; + + public static class VectorOps + { + /// + /// Computes the weighted Euclidean distance between two float arrays. + /// Each dimension's squared difference is multiplied by the corresponding weight. + /// + public static float WeightedEuclideanDistance(ReadOnlySpan a, ReadOnlySpan b, ReadOnlySpan weights) + { + if (a.Length != b.Length || a.Length != weights.Length) + throw new ArgumentException("All arrays must have the same length."); + + float sum = 0f; + for (int i = 0; i < a.Length; i++) + { + float diff = a[i] - b[i]; + sum += weights[i] * diff * diff; + } + return MathF.Sqrt(sum); + } + } + - path: "WeightedDist.Tests/WeightedDist.Tests.csproj" + content: | + + + net8.0 + enable + enable + false + true + + + + + + + + + + + - path: "WeightedDist.Tests/VectorOpsTests.cs" + content: | + using WeightedDist; + + namespace WeightedDist.Tests; + + public class VectorOpsTests + { + [Fact] + public void SameVectors_ReturnsZero() + { + var v = new float[] { 1f, 2f, 3f }; + var w = new float[] { 1f, 1f, 1f }; + Assert.Equal(0f, VectorOps.WeightedEuclideanDistance(v, v, w), 5); + } + + [Fact] + public void UnitWeights_MatchesStandardL2() + { + var a = new float[] { 0f, 0f }; + var b = new float[] { 3f, 4f }; + var w = new float[] { 1f, 1f }; + Assert.Equal(5f, VectorOps.WeightedEuclideanDistance(a, b, w), 5); + } + + [Fact] + public void ZeroWeight_IgnoresDimension() + { + var a = new float[] { 0f, 0f }; + var b = new float[] { 100f, 4f }; + var w = new float[] { 0f, 1f }; + Assert.Equal(4f, VectorOps.WeightedEuclideanDistance(a, b, w), 5); + } + + [Fact] + public void DifferentLengths_Throws() + { + Assert.Throws(() => + VectorOps.WeightedEuclideanDistance(new float[] { 1f }, new float[] { 1f, 2f }, new float[] { 1f })); + } + + [Fact] + public void LargeVectors() + { + var rng = new Random(42); + var a = Enumerable.Range(0, 512).Select(_ => (float)(rng.NextDouble() * 2 - 1)).ToArray(); + var b = Enumerable.Range(0, 512).Select(_ => (float)(rng.NextDouble() * 2 - 1)).ToArray(); + var w = Enumerable.Range(0, 512).Select(_ => (float)rng.NextDouble()).ToArray(); + + float expected = 0f; + for (int i = 0; i < a.Length; i++) { float d = a[i] - b[i]; expected += w[i] * d * d; } + expected = MathF.Sqrt(expected); + + Assert.Equal(expected, VectorOps.WeightedEuclideanDistance(a, b, w), 2); + } + + [Fact] + public void ShortVector_BelowVectorWidth() + { + var a = new float[] { 1f, 0f }; + var b = new float[] { 0f, 1f }; + var w = new float[] { 4f, 9f }; + // sqrt(4*1 + 9*1) = sqrt(13) + Assert.Equal(MathF.Sqrt(13f), VectorOps.WeightedEuclideanDistance(a, b, w), 5); + } + + [Fact] + public void EmptyArrays_ReturnsZero() + { + Assert.Equal(0f, VectorOps.WeightedEuclideanDistance( + ReadOnlySpan.Empty, ReadOnlySpan.Empty, ReadOnlySpan.Empty), 5); + } + } + assertions: + - type: "output_contains" + value: "Vector" + - type: "exit_success" + rubric: + - "Identifies the scalar 3-array fused loop as a SIMD vectorization candidate (Category A — multi-input fused computation)" + - "Introduces cross-platform Vector128/Vector256 SIMD intrinsics to process all three arrays in a single vectorized pass" + - "Preserves the existing public API signature (WeightedEuclideanDistance with ReadOnlySpan parameters)" + - "Handles tail elements correctly (array lengths not divisible by vector width)" + - "Preserves scalar fallback path for short inputs or non-accelerated hardware" + - "Existing unit tests still pass after the optimization" + timeout: 180 + + - name: "Optimize quantized dequantization" + prompt: "This .NET 8 project has a method that converts quantized byte values to float arrays using a scale factor and zero point. It's used in an ML inference pipeline to dequantize model weights and activations. The arrays are typically 1024-4096 elements and the method is called thousands of times per inference batch. It's currently a bottleneck. Please optimize Dequantize for maximum throughput. Keep the same public API and make sure all existing tests still pass." + setup: + files: + - path: "Dequantize/Dequantize.csproj" + content: | + + + net8.0 + enable + enable + + + - path: "Dequantize/QuantOps.cs" + content: | + namespace Dequantize; + + public static class QuantOps + { + /// + /// Dequantizes byte values to float using: output[i] = (quantized[i] - zeroPoint) * scale + /// + public static void Dequantize(ReadOnlySpan quantized, Span output, float scale, byte zeroPoint) + { + if (quantized.Length != output.Length) + throw new ArgumentException("Input and output must have the same length."); + + for (int i = 0; i < quantized.Length; i++) + { + output[i] = (quantized[i] - zeroPoint) * scale; + } + } + } + - path: "Dequantize.Tests/Dequantize.Tests.csproj" + content: | + + + net8.0 + enable + enable + false + true + + + + + + + + + + + - path: "Dequantize.Tests/QuantOpsTests.cs" + content: | + using Dequantize; + + namespace Dequantize.Tests; + + public class QuantOpsTests + { + [Fact] + public void BasicDequantize() + { + byte[] input = { 100, 128, 200 }; + float[] output = new float[3]; + QuantOps.Dequantize(input, output, 0.1f, 128); + Assert.Equal(-2.8f, output[0], 4); + Assert.Equal(0f, output[1], 4); + Assert.Equal(7.2f, output[2], 4); + } + + [Fact] + public void ZeroPointAtZero() + { + byte[] input = { 0, 1, 255 }; + float[] output = new float[3]; + QuantOps.Dequantize(input, output, 0.5f, 0); + Assert.Equal(0f, output[0], 5); + Assert.Equal(0.5f, output[1], 5); + Assert.Equal(127.5f, output[2], 5); + } + + [Fact] + public void DifferentLengths_Throws() + { + Assert.Throws(() => + QuantOps.Dequantize(new byte[] { 1, 2 }, new float[3], 1f, 0)); + } + + [Fact] + public void EmptyInput_NoOp() + { + QuantOps.Dequantize(ReadOnlySpan.Empty, Span.Empty, 1f, 0); + } + + [Fact] + public void LargeBuffer() + { + var rng = new Random(42); + var input = new byte[2048]; + rng.NextBytes(input); + var output = new float[2048]; + byte zp = 128; + float scale = 0.05f; + + QuantOps.Dequantize(input, output, scale, zp); + + for (int i = 0; i < input.Length; i++) + { + float expected = (input[i] - zp) * scale; + Assert.Equal(expected, output[i], 4); + } + } + + [Fact] + public void ShortInput_BelowVectorWidth() + { + byte[] input = { 130, 126 }; + float[] output = new float[2]; + QuantOps.Dequantize(input, output, 1f, 128); + Assert.Equal(2f, output[0], 5); + Assert.Equal(-2f, output[1], 5); + } + + [Fact] + public void AllSameValue() + { + byte[] input = { 128, 128, 128, 128 }; + float[] output = new float[4]; + QuantOps.Dequantize(input, output, 0.1f, 128); + Assert.All(output, v => Assert.Equal(0f, v, 5)); + } + } + assertions: + - type: "output_contains" + value: "Vector" + - type: "exit_success" + rubric: + - "Identifies the scalar byte-to-float conversion loop as a SIMD vectorization candidate (Category B — cross-type conversion)" + - "Introduces cross-platform Vector128/Vector256 SIMD intrinsics with byte-to-float widening and type conversion" + - "Preserves the existing public API signature (Dequantize with ReadOnlySpan, Span parameters)" + - "Handles tail elements correctly (array lengths not divisible by vector width)" + - "Preserves scalar fallback path for short inputs or non-accelerated hardware" + - "Existing unit tests still pass after the optimization" + timeout: 180 + + - name: "No optimization opportunity — string processing service" + prompt: "This .NET 8 project has a text processing service that deduplicates and sorts a list of tags. It's used at API request time to normalize user-provided tag lists. The tag lists are typically 5-20 items. Please review the code and optimize it for performance with SIMD if possible. All existing tests must continue to pass." + setup: + files: + - path: "TagService/TagService.csproj" + content: | + + + net8.0 + enable + enable + + + - path: "TagService/TagNormalizer.cs" + content: | + namespace TagService; + + public static class TagNormalizer + { + /// + /// Normalizes a list of tags by trimming whitespace, converting to lowercase, + /// removing duplicates, and sorting alphabetically. + /// + public static List NormalizeTags(IEnumerable tags) + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var result = new List(); + + foreach (var tag in tags) + { + var trimmed = tag.Trim(); + if (trimmed.Length == 0) + continue; + + var lower = trimmed.ToLowerInvariant(); + if (seen.Add(lower)) + { + result.Add(lower); + } + } + + result.Sort(StringComparer.Ordinal); + return result; + } + } + - path: "TagService.Tests/TagService.Tests.csproj" + content: | + + + net8.0 + enable + enable + false + true + + + + + + + + + + + - path: "TagService.Tests/TagNormalizerTests.cs" + content: | + using TagService; + + namespace TagService.Tests; + + public class TagNormalizerTests + { + [Fact] + public void EmptyInput_ReturnsEmpty() + { + var result = TagNormalizer.NormalizeTags(Array.Empty()); + Assert.Empty(result); + } + + [Fact] + public void RemovesDuplicates_CaseInsensitive() + { + var result = TagNormalizer.NormalizeTags(new[] { "CSharp", "csharp", "CSHARP" }); + Assert.Single(result); + Assert.Equal("csharp", result[0]); + } + + [Fact] + public void TrimsWhitespace() + { + var result = TagNormalizer.NormalizeTags(new[] { " dotnet ", " azure " }); + Assert.Equal(new[] { "azure", "dotnet" }, result); + } + + [Fact] + public void SkipsEmptyAndWhitespace() + { + var result = TagNormalizer.NormalizeTags(new[] { "", " ", "valid" }); + Assert.Single(result); + Assert.Equal("valid", result[0]); + } + + [Fact] + public void SortsAlphabetically() + { + var result = TagNormalizer.NormalizeTags(new[] { "zebra", "apple", "mango" }); + Assert.Equal(new[] { "apple", "mango", "zebra" }, result); + } + + [Fact] + public void CombinedBehavior() + { + var result = TagNormalizer.NormalizeTags(new[] { " B ", "a", "b", "", "C" }); + Assert.Equal(new[] { "a", "b", "c" }, result); + } + } + assertions: + - type: "exit_success" + rubric: + - "Correctly identifies that this code has no meaningful SIMD vector math optimization opportunity" + - "Explains why SIMD is not applicable (string processing, small collections of 5-20 items, no floating-point array math, HashSet/sort overhead dominates)" + - "Does not introduce unnecessary SIMD code or Vector256/Vector128 intrinsics that add complexity without benefit" + - "Does not generate greenfield SIMD implementations where no scalar vector math candidate exists" + - "Existing tests still pass — code is not broken by unnecessary changes" + timeout: 180 +