summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/inc/corinfo.h3
-rw-r--r--src/jit/importer.cpp81
-rw-r--r--src/jit/optimizer.cpp9
-rw-r--r--src/vm/jitinterface.cpp23
-rw-r--r--src/vm/method.cpp2
-rw-r--r--src/vm/mscorlib.h2
-rw-r--r--tests/src/JIT/Performance/CodeQuality/Span/Indexer.cs1021
-rw-r--r--tests/src/JIT/Performance/CodeQuality/Span/Indexer.csproj44
-rw-r--r--tests/src/JIT/config/benchmark/project.json1
9 files changed, 1180 insertions, 6 deletions
diff --git a/src/inc/corinfo.h b/src/inc/corinfo.h
index cbc4464e1d..97f395800e 100644
--- a/src/inc/corinfo.h
+++ b/src/inc/corinfo.h
@@ -213,7 +213,6 @@ TODO: Talk about initializing strutures before use
#define SELECTANY extern __declspec(selectany)
#endif
-// Update this one
SELECTANY const GUID JITEEVersionIdentifier = { /* f00b3f49-ddd2-49be-ba43-6e49ffa66959 */
0xf00b3f49,
0xddd2,
@@ -959,6 +958,8 @@ enum CorInfoIntrinsics
CORINFO_INTRINSIC_GetManagedThreadId,
CORINFO_INTRINSIC_ByReference_Ctor,
CORINFO_INTRINSIC_ByReference_Value,
+ CORINFO_INTRINSIC_Span_GetItem,
+ CORINFO_INTRINSIC_ReadOnlySpan_GetItem,
CORINFO_INTRINSIC_Count,
CORINFO_INTRINSIC_Illegal = -1, // Not a true intrinsic,
diff --git a/src/jit/importer.cpp b/src/jit/importer.cpp
index 989130a247..a07c55c62d 100644
--- a/src/jit/importer.cpp
+++ b/src/jit/importer.cpp
@@ -3620,6 +3620,87 @@ GenTreePtr Compiler::impIntrinsic(GenTreePtr newobjThis,
retNode = field;
break;
}
+ case CORINFO_INTRINSIC_Span_GetItem:
+ case CORINFO_INTRINSIC_ReadOnlySpan_GetItem:
+ {
+ // Have index, stack pointer-to Span<T> s on the stack. Expand to:
+ //
+ // For Span<T>
+ // Comma
+ // BoundsCheck(index, s->_length)
+ // s->_pointer + index * sizeof(T)
+ //
+ // For ReadOnlySpan<T>
+ // Comma
+ // BoundsCheck(index, s->_length)
+ // *(s->_pointer + index * sizeof(T))
+ //
+ // Signature should show one class type parameter, which
+ // we need to examine.
+ assert(sig->sigInst.classInstCount == 1);
+ CORINFO_CLASS_HANDLE spanElemHnd = sig->sigInst.classInst[0];
+ const unsigned elemSize = info.compCompHnd->getClassSize(spanElemHnd);
+ assert(elemSize > 0);
+
+ const bool isReadOnly = (intrinsicID == CORINFO_INTRINSIC_ReadOnlySpan_GetItem);
+
+ JITDUMP("\nimpIntrinsic: Expanding %sSpan<T>.get_Item, T=%s, sizeof(T)=%u\n", isReadOnly ? "ReadOnly" : "",
+ info.compCompHnd->getClassName(spanElemHnd), elemSize);
+
+ GenTreePtr index = impPopStack().val;
+ GenTreePtr ptrToSpan = impPopStack().val;
+ GenTreePtr indexClone = nullptr;
+ GenTreePtr ptrToSpanClone = nullptr;
+
+#if defined(DEBUG)
+ if (verbose)
+ {
+ printf("with ptr-to-span\n");
+ gtDispTree(ptrToSpan);
+ printf("and index\n");
+ gtDispTree(index);
+ }
+#endif // defined(DEBUG)
+
+ // We need to use both index and ptr-to-span twice, so clone or spill.
+ index = impCloneExpr(index, &indexClone, NO_CLASS_HANDLE, (unsigned)CHECK_SPILL_ALL,
+ nullptr DEBUGARG("Span.get_Item index"));
+ ptrToSpan = impCloneExpr(ptrToSpan, &ptrToSpanClone, NO_CLASS_HANDLE, (unsigned)CHECK_SPILL_ALL,
+ nullptr DEBUGARG("Span.get_Item ptrToSpan"));
+
+ // Bounds check
+ CORINFO_FIELD_HANDLE lengthHnd = info.compCompHnd->getFieldInClass(clsHnd, 1);
+ const unsigned lengthOffset = info.compCompHnd->getFieldOffset(lengthHnd);
+ GenTreePtr length = gtNewFieldRef(TYP_INT, lengthHnd, ptrToSpan, lengthOffset, false);
+ GenTreePtr boundsCheck = new (this, GT_ARR_BOUNDS_CHECK)
+ GenTreeBoundsChk(GT_ARR_BOUNDS_CHECK, TYP_VOID, index, length, SCK_RNGCHK_FAIL);
+
+ // Element access
+ GenTreePtr indexIntPtr = impImplicitIorI4Cast(indexClone, TYP_I_IMPL);
+ GenTreePtr sizeofNode = gtNewIconNode(elemSize);
+ GenTreePtr mulNode = gtNewOperNode(GT_MUL, TYP_I_IMPL, indexIntPtr, sizeofNode);
+ CORINFO_FIELD_HANDLE ptrHnd = info.compCompHnd->getFieldInClass(clsHnd, 0);
+ const unsigned ptrOffset = info.compCompHnd->getFieldOffset(ptrHnd);
+ GenTreePtr data = gtNewFieldRef(TYP_BYREF, ptrHnd, ptrToSpanClone, ptrOffset, false);
+ GenTreePtr result = gtNewOperNode(GT_ADD, TYP_BYREF, data, mulNode);
+
+ // Prepare result
+ var_types resultType = JITtype2varType(sig->retType);
+
+ if (isReadOnly)
+ {
+ result = gtNewOperNode(GT_IND, resultType, result);
+ }
+ else
+ {
+ assert(resultType == result->TypeGet());
+ }
+
+ retNode = gtNewOperNode(GT_COMMA, resultType, boundsCheck, result);
+
+ break;
+ }
+
default:
/* Unknown intrinsic */
break;
diff --git a/src/jit/optimizer.cpp b/src/jit/optimizer.cpp
index c18ebc55d0..710dac540c 100644
--- a/src/jit/optimizer.cpp
+++ b/src/jit/optimizer.cpp
@@ -7638,6 +7638,15 @@ bool Compiler::optExtractArrIndex(GenTreePtr tree, ArrIndex* result, unsigned lh
{
return false;
}
+
+ // For span we may see gtArrLen is a local var or local field.
+ // We won't try and extract those.
+ const genTreeOps arrayOp = arrBndsChk->gtArrLen->gtOper;
+
+ if ((arrayOp == GT_LCL_VAR) || (arrayOp == GT_LCL_FLD))
+ {
+ return false;
+ }
if (arrBndsChk->gtArrLen->gtGetOp1()->gtOper != GT_LCL_VAR)
{
return false;
diff --git a/src/vm/jitinterface.cpp b/src/vm/jitinterface.cpp
index a82cf00448..5ef7700896 100644
--- a/src/vm/jitinterface.cpp
+++ b/src/vm/jitinterface.cpp
@@ -8623,7 +8623,7 @@ CorInfoIntrinsics CEEInfo::getIntrinsicID(CORINFO_METHOD_HANDLE methodHnd,
else
{
MethodTable * pMT = method->GetMethodTable();
- if (pMT->IsByRefLike() && pMT->GetModule()->IsSystem())
+ if (pMT->GetModule()->IsSystem() && pMT->IsByRefLike())
{
if (pMT->HasSameTypeDefAs(g_pByReferenceClass))
{
@@ -8637,10 +8637,25 @@ CorInfoIntrinsics CEEInfo::getIntrinsicID(CORINFO_METHOD_HANDLE methodHnd,
_ASSERTE(strcmp(method->GetName(), "get_Value") == 0);
result = CORINFO_INTRINSIC_ByReference_Value;
}
- *pMustExpand = true;
+ if (pMustExpand != nullptr)
+ {
+ *pMustExpand = true;
+ }
+ }
+ else if (pMT->HasSameTypeDefAs(MscorlibBinder::GetClass(CLASS__SPAN)))
+ {
+ if (method->HasSameMethodDefAs(MscorlibBinder::GetMethod(METHOD__SPAN__GET_ITEM)))
+ {
+ result = CORINFO_INTRINSIC_Span_GetItem;
+ }
+ }
+ else if (pMT->HasSameTypeDefAs(MscorlibBinder::GetClass(CLASS__READONLY_SPAN)))
+ {
+ if (method->HasSameMethodDefAs(MscorlibBinder::GetMethod(METHOD__READONLY_SPAN__GET_ITEM)))
+ {
+ result = CORINFO_INTRINSIC_ReadOnlySpan_GetItem;
+ }
}
-
- // TODO-SPAN: Span<T> intrinsics for optimizations
}
}
diff --git a/src/vm/method.cpp b/src/vm/method.cpp
index a72b07b404..77a6a0d37f 100644
--- a/src/vm/method.cpp
+++ b/src/vm/method.cpp
@@ -2404,7 +2404,7 @@ BOOL MethodDesc::IsFCallOrIntrinsic()
if (IsFCall() || IsArray())
return TRUE;
- // Intrinsic methods on ByReference<T> or Span<T>
+ // Intrinsic methods on ByReference<T>, Span<T>, or ReadOnlySpan<T>
MethodTable * pMT = GetMethodTable();
if (pMT->IsByRefLike() && pMT->GetModule()->IsSystem())
return TRUE;
diff --git a/src/vm/mscorlib.h b/src/vm/mscorlib.h
index 4b82de0a2c..87927f687d 100644
--- a/src/vm/mscorlib.h
+++ b/src/vm/mscorlib.h
@@ -653,7 +653,9 @@ DEFINE_CLASS(NULLABLE, System, Nullable`1)
DEFINE_CLASS(BYREFERENCE, System, ByReference`1)
DEFINE_CLASS(SPAN, System, Span`1)
+DEFINE_METHOD(SPAN, GET_ITEM, get_Item, NoSig)
DEFINE_CLASS(READONLY_SPAN, System, ReadOnlySpan`1)
+DEFINE_METHOD(READONLY_SPAN, GET_ITEM, get_Item, NoSig)
// Keep this in sync with System.Globalization.NumberFormatInfo
DEFINE_CLASS_U(Globalization, NumberFormatInfo, NumberFormatInfo)
diff --git a/tests/src/JIT/Performance/CodeQuality/Span/Indexer.cs b/tests/src/JIT/Performance/CodeQuality/Span/Indexer.cs
new file mode 100644
index 0000000000..4a7264ba92
--- /dev/null
+++ b/tests/src/JIT/Performance/CodeQuality/Span/Indexer.cs
@@ -0,0 +1,1021 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using Xunit;
+using Microsoft.Xunit.Performance;
+
+[assembly: OptimizeForBenchmarks]
+[assembly: MeasureInstructionsRetired]
+
+namespace Span
+{
+ class Sink
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static Sink NewSink() { return new Sink(); }
+
+ public byte b;
+ public int i;
+ }
+
+ [AttributeUsage(AttributeTargets.Method, Inherited = false)]
+ class CategoryAttribute : Attribute
+ {
+ public CategoryAttribute(string name)
+ {
+ _name = name;
+ }
+ string _name;
+ public string Name => _name;
+ }
+
+ public class IndexerBench
+ {
+ const int Iterations = 1000000;
+ const int DefaultLength = 1024;
+ const byte Expected = 70;
+ static bool HasFailure = false;
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Ref(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestRef(s);
+ }
+ return result;
+ },
+ "Ref({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestRef(Span<byte> data)
+ {
+ ref byte p = ref data.DangerousGetPinnableReference();
+ int length = data.Length;
+ byte x = 0;
+
+ for (var idx = 0; idx < length; idx++)
+ {
+ x ^= Unsafe.Add(ref p, idx);
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Fixed1(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestFixed1(s);
+ }
+ return result;
+ },
+ "Fixed1({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static unsafe byte TestFixed1(Span<byte> data)
+ {
+ fixed (byte* pData = &data.DangerousGetPinnableReference())
+ {
+ int length = data.Length;
+ byte x = 0;
+ byte* p = pData;
+
+ for (var idx = 0; idx < length; idx++)
+ {
+ x ^= *(p + idx);
+ }
+
+ return x;
+ }
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Fixed2(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestFixed2(s);
+ }
+ return result;
+ },
+ "Fixed2({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static unsafe byte TestFixed2(Span<byte> data)
+ {
+ fixed (byte* pData = &data.DangerousGetPinnableReference())
+ {
+ int length = data.Length;
+ byte x = 0;
+
+ for (var idx = 0; idx < length; idx++)
+ {
+ x ^= pData[idx];
+ }
+
+ return x;
+ }
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Indexer1(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestIndexer1(s);
+ }
+ return result;
+ },
+ "Indexer1({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestIndexer1(Span<byte> data)
+ {
+ int length = data.Length;
+ byte x = 0;
+
+ for (var idx = 0; idx < length; idx++)
+ {
+ x ^= data[idx];
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Indexer2(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestIndexer2(s);
+ }
+ return result;
+ },
+ "Indexer2({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestIndexer2(Span<byte> data)
+ {
+ byte x = 0;
+
+ for (var idx = 0; idx < data.Length; idx++)
+ {
+ x ^= data[idx];
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Indexer3(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestIndexer3(s);
+ }
+ return result;
+ },
+ "Indexer3({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestIndexer3(Span<byte> data)
+ {
+ Span<byte> data2 = data;
+
+ byte x = 0;
+
+ for (var idx = 0; idx < data2.Length; idx++)
+ {
+ x ^= data2[idx];
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount=Iterations / 10)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Indexer4(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ int inner = Math.Max(1, (innerIterationCount / 10));
+ for (int i = 0; i < inner ; ++i)
+ {
+ result = TestIndexer4(s, 10);
+ }
+ return result;
+ },
+ "Indexer4({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestIndexer4(Span<byte> data, int iterations)
+ {
+ byte x = 0;
+ int length = data.Length;
+
+ // This does more or less the same work as TestIndexer1
+ // but is expressed as a loop nest.
+ for (int i = 0; i < iterations; i++)
+ {
+ x = 0;
+
+ for (var idx = 0; idx < length; idx++)
+ {
+ x ^= data[idx];
+ }
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Indexer5(int length)
+ {
+ byte[] a = GetData(length);
+ int z = 0;
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestIndexer5(s, out z);
+ }
+ return result;
+ },
+ "Indexer5({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestIndexer5(Span<byte> data, out int z)
+ {
+ byte x = 0;
+ z = -1;
+
+ // Write to z here should not be able to modify
+ // the span.
+ for (var idx = 0; idx < data.Length; idx++)
+ {
+ x ^= data[idx];
+ z = idx;
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void Indexer6(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestIndexer6(s);
+ }
+ return result;
+ },
+ "Indexer6({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestIndexer6(Span<byte> data)
+ {
+ byte x = 0;
+ Sink s = Sink.NewSink();
+
+ // Write to s.i here should not be able to modify
+ // the span.
+ for (var idx = 0; idx < data.Length; idx++)
+ {
+ x ^= data[idx];
+ s.i = 0;
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void ReadOnlyIndexer1(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestReadOnlyIndexer1(s);
+ }
+ return result;
+ },
+ "ReadOnlyIndexer1({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestReadOnlyIndexer1(ReadOnlySpan<byte> data)
+ {
+ int length = data.Length;
+ byte x = 0;
+
+ for (var idx = 0; idx < length; idx++)
+ {
+ x ^= data[idx];
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination")]
+ public static void ReadOnlyIndexer2(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ ReadOnlySpan<byte> s = new ReadOnlySpan<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestReadOnlyIndexer2(s);
+ }
+ return result;
+ },
+ "ReadOnlyIndexer2({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestReadOnlyIndexer2(ReadOnlySpan<byte> data)
+ {
+ byte x = 0;
+
+ for (var idx = 0; idx < data.Length; idx++)
+ {
+ x ^= data[idx];
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination w/ writes")]
+ public static void WriteViaIndexer1(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestWriteViaIndexer1(s);
+ }
+ return result;
+ },
+ "WriteViaIndexer1({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestWriteViaIndexer1(Span<byte> data)
+ {
+ byte q = data[0];
+
+ for (var idx = 1; idx < data.Length; idx++)
+ {
+ data[0] ^= data[idx];
+ }
+
+ byte x = data[0];
+ data[0] = q;
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Indexer in-loop bounds check elimination w/ writes")]
+ public static void WriteViaIndexer2(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestWriteViaIndexer2(s, 0, length);
+ }
+ return result;
+ },
+ "WriteViaIndexer2({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestWriteViaIndexer2(Span<byte> data, int start, int end)
+ {
+ byte x = 0;
+
+ for (var idx = start; idx < end; idx++)
+ {
+ // Bounds checks are redundant
+ byte b = data[idx];
+ x ^= b;
+ data[idx] = b;
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Span known size bounds check elimination")]
+ public static void KnownSizeArray(int length)
+ {
+ if (length != 1024)
+ {
+ throw new Exception("test requires 1024 byte length");
+ }
+
+ Invoke((int innerIterationCount) =>
+ {
+ byte result = TestKnownSizeArray(innerIterationCount);
+ return result;
+ },
+ "KnownSizeArray({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestKnownSizeArray(int innerIterationCount)
+ {
+ byte[] a = new byte[1024];
+ SetData(a);
+ Span<byte> data = new Span<byte>(a);
+ byte x = 0;
+
+ for (int i = 0; i < innerIterationCount; i++)
+ {
+ x = 0;
+ for (var idx = 0; idx < data.Length; idx++)
+ {
+ x ^= data[idx];
+ }
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Span known size bounds check elimination")]
+ public static void KnownSizeCtor(int length)
+ {
+ if (length < 1024)
+ {
+ throw new Exception("test requires at least 1024 byte length");
+ }
+
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ byte result = TestKnownSizeCtor(a, innerIterationCount);
+ return result;
+ },
+ "KnownSizeCtor({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestKnownSizeCtor(byte[] a, int innerIterationCount)
+ {
+ Span<byte> data = new Span<byte>(a, 0, 1024);
+ byte x = 0;
+
+ for (int i = 0; i < innerIterationCount; i++)
+ {
+ x = 0;
+ for (var idx = 0; idx < data.Length; idx++)
+ {
+ x ^= data[idx];
+ }
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Span known size bounds check elimination")]
+ public static void KnownSizeCtor2(int length)
+ {
+ if (length < 1024)
+ {
+ throw new Exception("test requires at least 1024 byte length");
+ }
+
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ byte result = TestKnownSizeCtor2(a, innerIterationCount);
+ return result;
+ },
+ "KnownSizeCtor2({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestKnownSizeCtor2(byte[] a, int innerIterationCount)
+ {
+ Span<byte> data1 = new Span<byte>(a, 0, 512);
+ Span<byte> data2 = new Span<byte>(a, 512, 512);
+ byte x = 0;
+
+ for (int i = 0; i < innerIterationCount; i++)
+ {
+ x = 0;
+ for (var idx = 0; idx < data1.Length; idx++)
+ {
+ x ^= data1[idx];
+ }
+ for (var idx = 0; idx < data2.Length; idx++)
+ {
+ x ^= data2[idx];
+ }
+ }
+
+ return x;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Same index in-loop redundant bounds check elimination")]
+ public static void SameIndex1(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestSameIndex1(s, 0, length);
+ }
+ return result;
+ },
+ "SameIndex1({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestSameIndex1(Span<byte> data, int start, int end)
+ {
+ byte x = 0;
+ byte y = 0;
+
+ for (var idx = start; idx < end; idx++)
+ {
+ x ^= data[idx];
+ y ^= data[idx];
+ }
+
+ byte t = (byte)(y ^ x ^ y);
+
+ return t;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Same index in-loop redundant bounds check elimination")]
+ public static void SameIndex2(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestSameIndex2(s, ref s[0], 0, length);
+ }
+ return result;
+ },
+ "SameIndex2({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestSameIndex2(Span<byte> data, ref byte b, int start, int end)
+ {
+ byte x = 0;
+ byte y = 0;
+ byte ye = 121;
+ byte q = data[0];
+
+ for (var idx = start; idx < end; idx++)
+ {
+ // Bounds check is redundant, but values are not CSEs.
+ x ^= data[idx];
+ b = 1;
+ y ^= data[idx];
+ }
+
+ byte t = (byte)(y ^ x ^ ye);
+ data[0] = q;
+
+ return t;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Covered index in-loop redundant bounds check elimination")]
+ public static void CoveredIndex1(int length)
+ {
+ if (length < 100)
+ {
+ throw new Exception("test requires at least 100 byte length");
+ }
+
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestCoveredIndex1(s, 0, length);
+ }
+ return result;
+ },
+ "CoveredIndex1({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestCoveredIndex1(Span<byte> data, int start, int end)
+ {
+ byte x = 0;
+ byte y = 0;
+
+ for (var idx = start; idx < end - 100; idx++)
+ {
+ x ^= data[idx + 100];
+ y ^= data[idx];
+ }
+
+ for (var idx = end - 100; idx < end; idx++)
+ {
+ y ^= data[idx];
+ x ^= data[idx - 100];
+ }
+
+ byte r = (byte)(x ^ y ^ x);
+
+ return r;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Covered index in-loop redundant bounds check elimination")]
+ public static void CoveredIndex2(int length)
+ {
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestCoveredIndex2(s, 0, length);
+ }
+ return result;
+ },
+ "CoveredIndex2({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestCoveredIndex2(Span<byte> data, int start, int end)
+ {
+ byte x = 0;
+ byte y = 0;
+
+ for (var idx = start; idx < end; idx++)
+ {
+ x ^= data[idx];
+
+ if (idx != 100)
+ {
+ // Should be able to eliminate this bounds check
+ y ^= data[0];
+ }
+ }
+
+ byte r = (byte)(y ^ x ^ y);
+
+ return r;
+ }
+
+ [Benchmark(InnerIterationCount = Iterations)]
+ [InlineData(DefaultLength)]
+ [Category("Covered index in-loop redundant bounds check elimination")]
+ public static void CoveredIndex3(int length)
+ {
+ if (length < 50)
+ {
+ throw new Exception("test requires at least 100 byte length");
+ }
+
+ byte[] a = GetData(length);
+
+ Invoke((int innerIterationCount) =>
+ {
+ Span<byte> s = new Span<byte>(a);
+ byte result = 0;
+ for (int i = 0; i < innerIterationCount; ++i)
+ {
+ result = TestCoveredIndex3(s, 0, length);
+ }
+ return result;
+ },
+ "CoveredIndex3({0})", length);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static byte TestCoveredIndex3(Span<byte> data, int start, int end)
+ {
+ byte x = 0;
+ byte y = 0;
+ byte z = 0;
+
+ for (var idx = start; idx < end; idx++)
+ {
+ x ^= data[idx];
+
+ if (idx != 100)
+ {
+ y ^= data[50];
+ // Should be able to eliminate this bounds check
+ z ^= data[25];
+ }
+ }
+
+ byte r = (byte)(z ^ y ^ x ^ y ^ z);
+
+ return r;
+ }
+
+ // Invoke routine to abstract away the difference between running under xunit-perf vs running from the
+ // command line. Inner loop to be measured is taken as an Func<int, byte>, and invoked passing the number
+ // of iterations that the inner loop should execute.
+ static void Invoke(Func<int, byte> innerLoop, string nameFormat, params object[] nameArgs)
+ {
+ if (IsXunitInvocation)
+ {
+ foreach (var iteration in Benchmark.Iterations)
+ using (iteration.StartMeasurement())
+ innerLoop((int)Benchmark.InnerIterationCount);
+ }
+ else
+ {
+ if (DoWarmUp)
+ {
+ // Run some warm-up iterations before measuring
+ innerLoop(CommandLineInnerIterationCount);
+ // Clear the flag since we're now warmed up (caller will
+ // reset it before calling new code)
+ DoWarmUp = false;
+ }
+
+ // Now do the timed run of the inner loop.
+ Stopwatch sw = Stopwatch.StartNew();
+ byte check = innerLoop(CommandLineInnerIterationCount);
+ sw.Stop();
+
+ // Print result.
+ string name = String.Format(nameFormat, nameArgs);
+ double timeInMs = sw.Elapsed.TotalMilliseconds;
+ Console.Write("{0,25}: {1,7:F2}ms", name, timeInMs);
+
+ bool failed = (check != Expected);
+ if (failed)
+ {
+ Console.Write(" -- failed to validate, got {0} expected {1}", check, Expected);
+ HasFailure = true;
+ }
+ Console.WriteLine();
+ }
+ }
+
+ static byte[] GetData(int size)
+ {
+ byte[] data = new byte[size];
+ SetData(data);
+ return data;
+ }
+
+ static void SetData(byte[] data)
+ {
+ Random Rnd = new Random(42);
+ Rnd.NextBytes(data);
+ }
+
+ static bool IsXunitInvocation = true;
+ static int CommandLineInnerIterationCount = 1;
+ static bool DoWarmUp;
+
+ public static void Usage()
+ {
+ Console.WriteLine(" pass -bench for benchmark mode w/default iterations");
+ Console.WriteLine(" pass [#iterations] for benchmark mode w/iterations");
+ Console.WriteLine();
+ }
+
+ public static int Main(string[] args)
+ {
+ if (args.Length > 0)
+ {
+ if (args[0].Equals("-bench"))
+ {
+ CommandLineInnerIterationCount = Iterations;
+ }
+ else
+ {
+ bool parsed = Int32.TryParse(args[0], out CommandLineInnerIterationCount);
+ if (!parsed)
+ {
+ Usage();
+ return -1;
+ }
+ }
+
+ Console.WriteLine("Running as command line perf test: {0} iterations",
+ CommandLineInnerIterationCount);
+ Console.WriteLine();
+ }
+ else
+ {
+ Console.WriteLine("Running as correctness test: {0} iterations",
+ CommandLineInnerIterationCount);
+ Usage();
+ }
+
+ // When we call into Invoke, it'll need to know this isn't xunit-perf running
+ IsXunitInvocation = false;
+
+ // Discover what tests to run via reflection
+ TypeInfo t = typeof(IndexerBench).GetTypeInfo();
+
+ var testsByCategory = new Dictionary<string, List<MethodInfo>>();
+
+ // Do a first pass to find out what categories of benchmarks we have.
+ foreach(MethodInfo m in t.DeclaredMethods)
+ {
+ BenchmarkAttribute benchAttr = m.GetCustomAttribute<BenchmarkAttribute>();
+ if (benchAttr != null)
+ {
+ string category = "none";
+ CategoryAttribute categoryAttr = m.GetCustomAttribute<CategoryAttribute>();
+ if (categoryAttr != null)
+ {
+ category = categoryAttr.Name;
+ }
+
+ List<MethodInfo> tests = null;
+
+ if (!testsByCategory.ContainsKey(category))
+ {
+ tests = new List<MethodInfo>();
+ testsByCategory.Add(category, tests);
+ }
+ else
+ {
+ tests = testsByCategory[category];
+ }
+
+ tests.Add(m);
+ }
+ }
+
+ foreach(string categoryName in testsByCategory.Keys)
+ {
+ Console.WriteLine("**** {0} ****", categoryName);
+
+ foreach(MethodInfo m in testsByCategory[categoryName])
+ {
+ // Request a warm-up iteration before measuring this benchmark method.
+ DoWarmUp = true;
+
+ // Get the benchmark to measure as a delegate taking the number of inner-loop iterations to run
+ var invokeMethod = m.CreateDelegate(typeof(Action<int>)) as Action<int>;
+
+ // All the benchmarks methods in this test use [InlineData] to specify how many times and with
+ // what arguments they should be run.
+ foreach (InlineDataAttribute dataAttr in m.GetCustomAttributes<InlineDataAttribute>())
+ {
+ foreach (object[] data in dataAttr.GetData(m))
+ {
+ // All the benchmark methods in this test take a single int parameter
+ invokeMethod((int)data[0]);
+ }
+ }
+ }
+
+ Console.WriteLine();
+ }
+
+ if (HasFailure)
+ {
+ Console.WriteLine("Some tests failed validation");
+ return -1;
+ }
+
+ return 100;
+ }
+ }
+}
diff --git a/tests/src/JIT/Performance/CodeQuality/Span/Indexer.csproj b/tests/src/JIT/Performance/CodeQuality/Span/Indexer.csproj
new file mode 100644
index 0000000000..a871713d1d
--- /dev/null
+++ b/tests/src/JIT/Performance/CodeQuality/Span/Indexer.csproj
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <!-- Always use latest Roslyn compiler -->
+ <Import Project="..\..\..\..\..\..\Tools\net46\roslyn\build\Microsoft.Net.Compilers.props"/>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{95DFC527-4DC1-495E-97D7-E94EE1F7140D}</ProjectGuid>
+ <OutputType>Exe</OutputType>
+ <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
+ <NuGetTargetMoniker>.NETStandard,Version=v1.4</NuGetTargetMoniker>
+ </PropertyGroup>
+ <!-- Default configurations to help VS understand the configurations -->
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ </PropertyGroup>
+ <PropertyGroup>
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+ <ItemGroup>
+ <CodeAnalysisDependentAssemblyPaths Condition=" '$(VS100COMNTOOLS)' != '' " Include="$(VS100COMNTOOLS)..\IDE\PrivateAssemblies">
+ <Visible>False</Visible>
+ </CodeAnalysisDependentAssemblyPaths>
+ </ItemGroup>
+ <ItemGroup>
+ <Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Indexer.cs" />
+ </ItemGroup>
+ <PropertyGroup>
+ <ProjectJson>$(JitPackagesConfigFileDirectory)benchmark\project.json</ProjectJson>
+ <ProjectLockJson>$(JitPackagesConfigFileDirectory)benchmark\project.lock.json</ProjectLockJson>
+ </PropertyGroup>
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.targets))\dir.targets" />
+ <PropertyGroup Condition=" '$(MsBuildProjectDirOverride)' != '' ">
+ </PropertyGroup>
+</Project>
diff --git a/tests/src/JIT/config/benchmark/project.json b/tests/src/JIT/config/benchmark/project.json
index 3b218fc203..09e5d8c418 100644
--- a/tests/src/JIT/config/benchmark/project.json
+++ b/tests/src/JIT/config/benchmark/project.json
@@ -18,6 +18,7 @@
"System.Reflection.Extensions": "4.4.0-beta-24913-02",
"System.Reflection.TypeExtensions": "4.4.0-preview1-25214-03",
"System.Runtime": "4.4.0-beta-24913-02",
+ "System.Runtime.CompilerServices.Unsafe": "4.4.0-preview1-25210-01",
"System.Runtime.Extensions": "4.4.0-beta-24913-02",
"System.Runtime.Numerics": "4.4.0-beta-24913-02",
"System.Text.RegularExpressions": "4.4.0-beta-24913-02",