diff options
-rw-r--r-- | src/inc/corinfo.h | 3 | ||||
-rw-r--r-- | src/jit/importer.cpp | 81 | ||||
-rw-r--r-- | src/jit/optimizer.cpp | 9 | ||||
-rw-r--r-- | src/vm/jitinterface.cpp | 23 | ||||
-rw-r--r-- | src/vm/method.cpp | 2 | ||||
-rw-r--r-- | src/vm/mscorlib.h | 2 | ||||
-rw-r--r-- | tests/src/JIT/Performance/CodeQuality/Span/Indexer.cs | 1021 | ||||
-rw-r--r-- | tests/src/JIT/Performance/CodeQuality/Span/Indexer.csproj | 44 | ||||
-rw-r--r-- | tests/src/JIT/config/benchmark/project.json | 1 |
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", |