Units for Span.Contains (dotnet/corefx#32293)
authorGrant <grant@jesanna.com>
Tue, 18 Sep 2018 01:35:15 +0000 (18:35 -0700)
committerAhson Khan <ahkha@microsoft.com>
Tue, 18 Sep 2018 01:35:15 +0000 (18:35 -0700)
* Units for Span.Contains

* More callsites

* More callsites with Linq

* CR fixes

* Moved Span<char>.Contains tests to StringTests

* Revert incompatible change

* Another revert

* Remove Linq scan false positive

* Remove redundant comments

* Undo risky Linq resolution

* Remove dupe units

* Undo comment

* New units broken by cross compilation

* Reverts

* Another revert

* Revert

* CR fixes

* More fixes

* Fixes

* Perf tests

* Build breaks

* Benchmark baseline

* Fix tautology

* Rename tests

* CR fixes

* Add more test lengths

* InnerCount can be smaller

Commit migrated from https://github.com/dotnet/corefx/commit/12eb4a3ad0275ebd66cec52e4cdecb421f1b2535

18 files changed:
src/libraries/System.Configuration.ConfigurationManager/src/System/Configuration/StringAttributeCollection.cs
src/libraries/System.Data.SqlClient/src/System/Data/SqlClient/SNI/SNIProxy.cs
src/libraries/System.Diagnostics.EventLog/src/System/Diagnostics/SharedUtils.cs
src/libraries/System.Diagnostics.PerformanceCounter/src/System/Diagnostics/SharedUtils.cs
src/libraries/System.Drawing.Common/src/System/Drawing/Printing/PrintingServices.Unix.cs
src/libraries/System.IO.FileSystem.DriveInfo/src/System/IO/DriveInfo.Unix.cs
src/libraries/System.IO.Packaging/src/System/IO/Packaging/PackUriHelper.cs
src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/ExpressionStringBuilder.cs
src/libraries/System.Memory/tests/Performance/Perf.Span.Contains.cs [new file with mode: 0644]
src/libraries/System.Memory/tests/Performance/System.Memory.Performance.Tests.csproj
src/libraries/System.Memory/tests/ReadOnlySpan/Contains.T.cs [new file with mode: 0644]
src/libraries/System.Memory/tests/ReadOnlySpan/Contains.byte.cs [new file with mode: 0644]
src/libraries/System.Memory/tests/System.Memory.Tests.csproj
src/libraries/System.Net.HttpListener/src/System/Net/ServiceNameStore.cs
src/libraries/System.Private.Xml/src/System/Xml/Serialization/SoapReflectionImporter.cs
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs
src/libraries/System.Runtime/tests/System/StringTests.netcoreapp.cs
src/libraries/System.Transactions.Local/src/System/Transactions/TransactionsEtwProvider.cs

index 0aa5644..294ff4c 100644 (file)
@@ -78,7 +78,7 @@ namespace System.Configuration
 
         private static void ThrowIfContainsDelimiter(string value)
         {
-            if (value.Contains(","))
+            if (value.Contains(",")) // string.Contains(char) is .NetCore2.1+ specific
                 throw new ConfigurationErrorsException(string.Format(SR.Config_base_value_cannot_contain, ","));
         }
 
@@ -144,4 +144,4 @@ namespace System.Configuration
             return copy;
         }
     }
-}
\ No newline at end of file
+}
index a50eb09..26aedd0 100644 (file)
@@ -3,7 +3,9 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Diagnostics;
+#if !netcoreapp
 using System.Linq;
+#endif
 using System.Net;
 using System.Net.Security;
 using System.Net.Sockets;
@@ -584,7 +586,12 @@ namespace System.Data.SqlClient.SNI
             _dataSourceAfterTrimmingProtocol = (firstIndexOfColon > -1) && ConnectionProtocol != DataSource.Protocol.None
                 ? _workingDataSource.Substring(firstIndexOfColon + 1).Trim() : _workingDataSource;
 
-            if (_dataSourceAfterTrimmingProtocol.Contains("/")) // Pipe paths only allow back slashes
+            // Pipe paths only allow back slashes
+#if netcoreapp
+            if (_dataSourceAfterTrimmingProtocol.Contains('/')) // string.Contains(char) is .NetCore2.1+ specific
+#else
+            if (_dataSourceAfterTrimmingProtocol.Contains("/"))
+#endif
             {
                 if (ConnectionProtocol == DataSource.Protocol.None)
                     ReportSNIError(SNIProviders.INVALID_PROV);
@@ -780,7 +787,7 @@ namespace System.Data.SqlClient.SNI
             if (_dataSourceAfterTrimmingProtocol.StartsWith(PipeBeginning) || ConnectionProtocol == Protocol.NP)
             {
                 // If the data source is "np:servername"
-                if (!_dataSourceAfterTrimmingProtocol.Contains(BackSlashSeparator))
+                if (!_dataSourceAfterTrimmingProtocol.Contains(BackSlashSeparator)) // string.Contains(char) is .NetCore2.1+ specific. Else uses Linq (perf warning)
                 {
                     PipeHostName = ServerName = _dataSourceAfterTrimmingProtocol;
                     InferLocalServerName();
index 2acca1e..1a20bb2 100644 (file)
@@ -155,7 +155,7 @@ namespace System.Diagnostics
                                     {
 
                                         string majorVersion = majorVersions[i];
-                                        if (majorVersion.Length > 1 && majorVersion[0] == 'v' && majorVersion.Contains("."))
+                                        if (majorVersion.Length > 1 && majorVersion[0] == 'v' && majorVersion.Contains(".")) // string.Contains(char) is .NetCore2.1+ specific
                                         {
                                             int[] currentVersion = new int[] { -1, -1, -1 };
 
index 2d1e9cb..bb04c97 100644 (file)
@@ -218,7 +218,7 @@ namespace System.Diagnostics
                                         string majorVersion = majorVersions[i];
 
                                         // If this looks like a key of the form v{something}.{something}, we should see if it's a usable build.
-                                        if (majorVersion.Length > 1 && majorVersion[0] == 'v' && majorVersion.Contains("."))
+                                        if (majorVersion.Length > 1 && majorVersion[0] == 'v' && majorVersion.Contains(".")) // string.Contains(char) is .NetCore2.1+ specific
                                         {
                                             int[] currentVersion = new int[] { -1, -1, -1 };
 
index 8d3af91..9f6f98d 100644 (file)
@@ -451,7 +451,7 @@ namespace System.Drawing.Printing
             int x_resolution, y_resolution;
             try
             {
-                if (resolution.Contains("x"))
+                if (resolution.Contains("x")) // string.Contains(char) is .NetCore2.1+ specific
                 {
                     string[] resolutions = resolution.Split(new[] { 'x' });
                     x_resolution = Convert.ToInt32(resolutions[0]);
index a94fdef..27e8f9c 100644 (file)
@@ -23,7 +23,7 @@ namespace System.IO
 
         private static string NormalizeDriveName(string driveName)
         {
-            if (driveName.Contains("\0"))
+            if (driveName.Contains("\0")) // string.Contains(char) is .NetCore2.1+ specific
             {
                 throw new ArgumentException(SR.Format(SR.Arg_InvalidDriveChars, driveName), nameof(driveName));
             }
index b660602..17d7691 100644 (file)
@@ -472,7 +472,7 @@ namespace System.IO.Packaging
 
         private static ArgumentException GetExceptionIfFragmentPresent(string partName)
         {
-            if (partName.Contains("#"))
+            if (partName.Contains("#")) // string.Contains(char) is .NetCore2.1+ specific
                 return new ArgumentException(SR.PartUriCannotHaveAFragment);
             else
                 return null;
index a72b3ae..3fdde09 100644 (file)
@@ -428,7 +428,7 @@ namespace System.Linq.Expressions
         protected internal override Expression VisitMemberInit(MemberInitExpression node)
         {
             if (node.NewExpression.ArgumentCount == 0 &&
-                node.NewExpression.Type.Name.Contains("<"))
+                node.NewExpression.Type.Name.Contains('<'))
             {
                 // anonymous type constructor
                 Out("new");
diff --git a/src/libraries/System.Memory/tests/Performance/Perf.Span.Contains.cs b/src/libraries/System.Memory/tests/Performance/Perf.Span.Contains.cs
new file mode 100644 (file)
index 0000000..859d00a
--- /dev/null
@@ -0,0 +1,318 @@
+// 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.Collections.Generic;
+using System.Runtime.InteropServices;
+using Microsoft.Xunit.Performance;
+using Xunit;
+
+namespace System.Memory.Tests
+{
+    // Adapted from Perf.Span.IndexOf.cs
+    public class Perf_Span_Contains
+    {
+        private const int InnerCount = 50_000;
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(5)]
+        [InlineData(10)]
+        [InlineData(100)]
+        [InlineData(1000)]
+        [InlineData(2000)]
+        public void SpanContainsChar(int size)
+        {
+            Span<char> charSpan = new char[size];
+            charSpan[size / 2] = '5';
+
+            bool found = true;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (!charSpan.Contains('5'))
+                            found = false;
+                    }
+                }
+            }
+
+            Assert.True(found);
+        }
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(5)]
+        [InlineData(10)]
+        [InlineData(100)]
+        [InlineData(1000)]
+        [InlineData(2000)]
+        public void SpanIndexOfChar(int size)
+        {
+            Span<char> charSpan = new char[size];
+            charSpan[size / 2] = '5';
+
+            bool found = true;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (charSpan.IndexOf('5') < 0)
+                            found = false;
+                    }
+                }
+            }
+
+            Assert.True(found);
+        }
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(5)]
+        [InlineData(10)]
+        [InlineData(100)]
+        [InlineData(1000)]
+        [InlineData(2000)]
+        public void SpanContainsCharAsBytes(int size)
+        {
+            Span<char> charSpan = new char[size];
+            charSpan[size / 2] = '5';
+            Span<byte> byteSpan = MemoryMarshal.AsBytes(charSpan);
+
+            bool found = true;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (!charSpan.Contains('5')) // '5' = 53
+                            found = false;
+                    }
+                }
+            }
+
+            Assert.True(found);
+        }
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(5)]
+        [InlineData(10)]
+        [InlineData(100)]
+        [InlineData(1000)]
+        [InlineData(2000)]
+        public void StringContainsChar(int size)
+        {
+            string str = new string('0', size / 2) + "5";
+            if (size > 1)
+            {
+                str += new string('0', size / 2 - 1);
+            }
+
+            bool found = true;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (!str.Contains('5'))
+                            found = false;
+                    }
+                }
+            }
+
+            Assert.True(found);
+        }
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [InlineData(1)]
+        [InlineData(2)]
+        [InlineData(5)]
+        [InlineData(10)]
+        [InlineData(100)]
+        [InlineData(1000)]
+        [InlineData(2000)]
+        public void StringIndexOfChar(int size)
+        {
+            string str = new string('0', size / 2) + "5";
+            if (size > 1)
+            {
+                str += new string('0', size / 2 - 1);
+            }
+
+            bool found = true;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (str.IndexOf('5') < 0)
+                            found = false;
+                    }
+                }
+            }
+
+            Assert.True(found);
+        }
+
+        private static string GenerateInputString(char source, int count, char replaceChar, int replacePos)
+        {
+            char[] str = new char[count];
+            for (int i = 0; i < count; i++)
+            {
+                str[i] = replaceChar;
+            }
+            str[replacePos] = replaceChar;
+
+            return new string(str);
+        }
+
+        public static IEnumerable<object[]> s_indexTestData = new List<object[]>
+        {
+            new object[] { "string1", '1' },
+            new object[] { "foobardzsdzs", 'z' },
+            new object[] { "StrIng", "I" },
+            new object[] { "\u3060", '\u305F' },
+            new object[] { "ABCDE", 'c' },
+            new object[] { "More Test's", '\'' },
+            new object[] { "Hello WorldbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbareallyreallylongHello WorldbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbareallyreallylongHello Worldbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbareallyreallylong!xyz", '~' },
+            new object[] { "Hello WorldbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbareallyreallylongHello WorldbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbareallyreallylongHello Worldbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbareallyreallylong!xyz", 'w' },
+            new object[] { "Hello Worldbbbbbbbbbbbbbbcbbbbbbbbbbbbbbbbbbba!", 'y' },
+            new object[] { GenerateInputString('A', 10, '5', 5), '5' },
+            new object[] { GenerateInputString('A', 100, 'X', 70), 'x' },
+            new object[] { GenerateInputString('A', 100, 'X', 70), 'x' },
+            new object[] { GenerateInputString('A', 1000, 'X', 500), 'X' },
+            new object[] { GenerateInputString('\u3060', 1000, 'x', 500), 'x' },
+            new object[] { GenerateInputString('\u3060', 100, '\u3059', 50), '\u3059' }
+        };
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [MemberData(nameof(s_indexTestData))]
+        public ulong ContainsChar_StringAsSpan(string input, char value)
+        {
+            var count = 0UL;
+
+            ReadOnlySpan<char> inputSpan = input.AsSpan();
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (inputSpan.Contains(value))
+                            count++;
+                    }
+                }
+            }
+
+            return count;
+        }
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [MemberData(nameof(s_indexTestData))]
+        public ulong ContainsChar_String(string input, char value)
+        {
+            var count = 0UL;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (input.Contains(value))
+                            count++;
+                    }
+                }
+            }
+
+            return count;
+        }
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [MemberData(nameof(s_indexTestData))]
+        public ulong ContainsChar_StringLinq(string input, char value)
+        {
+            var count = 0UL;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (Linq.Enumerable.Contains(input, value))
+                            count++;
+                    }
+                }
+            }
+
+            return count;
+        }
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [MemberData(nameof(s_indexTestData))]
+        public ulong ContainsChar_StringIndexOf(string input, char value)
+        {
+            var count = 0UL;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        if (input.IndexOf(value) >= 0)
+                            count++;
+                    }
+                }
+            }
+
+            return count;
+        }
+
+        [Benchmark(InnerIterationCount = InnerCount)]
+        [MemberData(nameof(s_indexTestData))]
+        public ulong ContainsChar_Baseline(string input, char value)
+        {
+            var count = 0UL;
+
+            foreach (BenchmarkIteration iteration in Benchmark.Iterations)
+            {
+                using (iteration.StartMeasurement())
+                {
+                    for (int i = 0; i < Benchmark.InnerIterationCount; i++)
+                    {
+                        for (var j = 0; j < input.Length; j++)
+                        {
+                            if (input[j] == value)
+                            {
+                                count++;
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+
+            return count;
+        }
+    }
+}
index ff5b70d..217f59b 100644 (file)
@@ -1,9 +1,14 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <ProjectGuid>{18482C55-6B57-41E8-BBC4-383B9E2C7AA2}</ProjectGuid>
+    <IncludePartialFacadeTests Condition="'$(TargetGroup)' == 'netcoreapp' OR '$(TargetGroup)' == 'uap'">true</IncludePartialFacadeTests>
     <Configurations>netcoreapp-Debug;netcoreapp-Release;netstandard-Debug;netstandard-Release</Configurations>
   </PropertyGroup>
+  <ItemGroup Condition="'$(IncludePartialFacadeTests)' == 'true'">
+    <!-- Tests specific to the fast span -->
+    <Compile Include="Perf.Span.Contains.cs" />
+  </ItemGroup>
   <ItemGroup>
     <Compile Include="Perf.Base64EncodeDecode.cs" />
     <Compile Include="Perf.Memory.Slice.cs" />
diff --git a/src/libraries/System.Memory/tests/ReadOnlySpan/Contains.T.cs b/src/libraries/System.Memory/tests/ReadOnlySpan/Contains.T.cs
new file mode 100644 (file)
index 0000000..e48ca72
--- /dev/null
@@ -0,0 +1,190 @@
+// 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 Xunit;
+
+namespace System.SpanTests
+{
+    // Adapted from IndexOf.T.cs
+    public static partial class ReadOnlySpanTests // .Contains<T>
+    {
+        [Fact]
+        public static void ZeroLengthContains()
+        {
+            ReadOnlySpan<int> span = new ReadOnlySpan<int>(Array.Empty<int>());
+
+            bool found = span.Contains(0);
+            Assert.False(found);
+        }
+
+        [Fact]
+        public static void TestContains()
+        {
+            for (int length = 0; length < 32; length++)
+            {
+                int[] a = new int[length];
+                for (int i = 0; i < length; i++)
+                {
+                    a[i] = 10 * (i + 1);
+                }
+                ReadOnlySpan<int> span = new ReadOnlySpan<int>(a);
+
+                for (int targetIndex = 0; targetIndex < length; targetIndex++)
+                {
+                    int target = a[targetIndex];
+                    bool found = span.Contains(target);
+                    Assert.True(found);
+                }
+            }
+        }
+
+        [Fact]
+        public static void TestMultipleContains()
+        {
+            for (int length = 2; length < 32; length++)
+            {
+                int[] a = new int[length];
+                for (int i = 0; i < length; i++)
+                {
+                    a[i] = 10 * (i + 1);
+                }
+
+                a[length - 1] = 5555;
+                a[length - 2] = 5555;
+
+                ReadOnlySpan<int> span = new ReadOnlySpan<int>(a);
+                bool found = span.Contains(5555);
+                Assert.True(found);
+            }
+        }
+
+        [Fact]
+        public static void OnNoMatchForContainsMakeSureEveryElementIsCompared()
+        {
+            for (int length = 0; length < 100; length++)
+            {
+                TIntLog log = new TIntLog();
+
+                TInt[] a = new TInt[length];
+                for (int i = 0; i < length; i++)
+                {
+                    a[i] = new TInt(10 * (i + 1), log);
+                }
+                ReadOnlySpan<TInt> span = new ReadOnlySpan<TInt>(a);
+                bool found = span.Contains(new TInt(9999, log));
+                Assert.False(found);
+
+                // Since we asked for a non-existent value, make sure each element of the array was compared once.
+                // (Strictly speaking, it would not be illegal for IndexOf to compare an element more than once but
+                // that would be a non-optimal implementation and a red flag. So we'll stick with the stricter test.)
+                Assert.Equal(a.Length, log.Count);
+                foreach (TInt elem in a)
+                {
+                    int numCompares = log.CountCompares(elem.Value, 9999);
+                    Assert.True(numCompares == 1, $"Expected {numCompares} == 1 for element {elem.Value}.");
+                }
+            }
+        }
+
+        [Fact]
+        public static void MakeSureNoChecksForContainsGoOutOfRange()
+        {
+            const int GuardValue = 77777;
+            const int GuardLength = 50;
+
+            void checkForOutOfRangeAccess(int x, int y)
+            {
+                if (x == GuardValue || y == GuardValue)
+                    throw new Exception("Detected out of range access in IndexOf()");
+            }
+
+            for (int length = 0; length < 100; length++)
+            {
+                TInt[] a = new TInt[GuardLength + length + GuardLength];
+                for (int i = 0; i < a.Length; i++)
+                {
+                    a[i] = new TInt(GuardValue, checkForOutOfRangeAccess);
+                }
+
+                for (int i = 0; i < length; i++)
+                {
+                    a[GuardLength + i] = new TInt(10 * (i + 1), checkForOutOfRangeAccess);
+                }
+
+                ReadOnlySpan<TInt> span = new ReadOnlySpan<TInt>(a, GuardLength, length);
+                bool found = span.Contains(new TInt(9999, checkForOutOfRangeAccess));
+                Assert.False(found);
+            }
+        }
+
+        [Fact]
+        public static void ZeroLengthContains_String()
+        {
+            ReadOnlySpan<string> span = new ReadOnlySpan<string>(Array.Empty<string>());
+            bool found = span.Contains("a");
+            Assert.False(found);
+        }
+
+        [Fact]
+        public static void TestMatchContains_String()
+        {
+            for (int length = 0; length < 32; length++)
+            {
+                string[] a = new string[length];
+                for (int i = 0; i < length; i++)
+                {
+                    a[i] = (10 * (i + 1)).ToString();
+                }
+                ReadOnlySpan<string> span = new ReadOnlySpan<string>(a);
+
+                for (int targetIndex = 0; targetIndex < length; targetIndex++)
+                {
+                    string target = a[targetIndex];
+                    bool found = span.Contains(target);
+                    Assert.True(found);
+                }
+            }
+        }
+
+        [Fact]
+        public static void TestNoMatchContains_String()
+        {
+            var rnd = new Random(42);
+            for (int length = 0; length <= byte.MaxValue; length++)
+            {
+                string[] a = new string[length];
+                string target = (rnd.Next(0, 256)).ToString();
+                for (int i = 0; i < length; i++)
+                {
+                    string val = (i + 1).ToString();
+                    a[i] = val == target ? (target + 1) : val;
+                }
+                ReadOnlySpan<string> span = new ReadOnlySpan<string>(a);
+
+                bool found = span.Contains(target);
+                Assert.False(found);
+            }
+        }
+
+        [Fact]
+        public static void TestMultipleMatchContains_String()
+        {
+            for (int length = 2; length < 32; length++)
+            {
+                string[] a = new string[length];
+                for (int i = 0; i < length; i++)
+                {
+                    a[i] = (10 * (i + 1)).ToString();
+                }
+
+                a[length - 1] = "5555";
+                a[length - 2] = "5555";
+
+                ReadOnlySpan<string> span = new ReadOnlySpan<string>(a);
+                bool found = span.Contains("5555");
+                Assert.True(found);
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Memory/tests/ReadOnlySpan/Contains.byte.cs b/src/libraries/System.Memory/tests/ReadOnlySpan/Contains.byte.cs
new file mode 100644 (file)
index 0000000..b234e84
--- /dev/null
@@ -0,0 +1,159 @@
+// 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.Numerics;
+using Xunit;
+
+namespace System.SpanTests
+{
+    // Adapted from IndexOf.byte.cs
+    public static partial class ReadOnlySpanTests // .Contains<Byte>
+    {
+        [Fact]
+        public static void ZeroLengthContains_Byte()
+        {
+            ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(Array.Empty<byte>());
+
+            bool found = span.Contains<byte>(0);
+            Assert.False(found);
+        }
+
+        [Fact]
+        public static void DefaultFilledContains_Byte()
+        {
+            for (int length = 0; length <= byte.MaxValue; length++)
+            {
+                byte[] a = new byte[length];
+                ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(a);
+
+                for (int i = 0; i < length; i++)
+                {
+                    byte target0 = default;
+
+                    bool found = span.Contains(target0);
+                    Assert.True(found);
+                }
+            }
+        }
+
+        [Fact]
+        public static void TestContains_Byte()
+        {
+            for (int length = 0; length <= byte.MaxValue; length++)
+            {
+                byte[] a = new byte[length];
+                for (int i = 0; i < length; i++)
+                {
+                    a[i] = (byte)(i + 1);
+                }
+                ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(a);
+
+                for (int targetIndex = 0; targetIndex < length; targetIndex++)
+                {
+                    byte target = a[targetIndex];
+
+                    bool found = span.Contains(target);
+                    Assert.True(found);
+                }
+            }
+        }
+
+        [Fact]
+        public static void TestNotContains_Byte()
+        {
+            var rnd = new Random(42);
+            for (int length = 0; length <= byte.MaxValue; length++)
+            {
+                byte[] a = new byte[length];
+                byte target = (byte)rnd.Next(0, 256);
+                for (int i = 0; i < length; i++)
+                {
+                    byte val = (byte)(i + 1);
+                    a[i] = val == target ? (byte)(target + 1) : val;
+                }
+                ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(a);
+
+                bool found = span.Contains(target);
+                Assert.False(found);
+            }
+        }
+
+        [Fact]
+        public static void TestAlignmentNotContains_Byte()
+        {
+            byte[] array = new byte[4 * Vector<byte>.Count];
+            for (var i = 0; i < Vector<byte>.Count; i++)
+            {
+                var span = new ReadOnlySpan<byte>(array, i, 3 * Vector<byte>.Count);
+
+                bool found = span.Contains((byte)'1');
+                Assert.False(found);
+
+                span = new ReadOnlySpan<byte>(array, i, 3 * Vector<byte>.Count - 3);
+
+                found = span.Contains((byte)'1');
+                Assert.False(found);
+            }
+        }
+
+        [Fact]
+        public static void TestAlignmentContains_Byte()
+        {
+            byte[] array = new byte[4 * Vector<byte>.Count];
+            for (int i = 0; i < array.Length; i++)
+            {
+                array[i] = 5;
+            }
+            for (var i = 0; i < Vector<byte>.Count; i++)
+            {
+                var span = new ReadOnlySpan<byte>(array, i, 3 * Vector<byte>.Count);
+
+                bool found = span.Contains<byte>(5);
+                Assert.True(found);
+
+                span = new ReadOnlySpan<byte>(array, i, 3 * Vector<byte>.Count - 3);
+
+                found = span.Contains<byte>(5);
+                Assert.True(found);
+            }
+        }
+
+        [Fact]
+        public static void TestMultipleContains_Byte()
+        {
+            for (int length = 2; length <= byte.MaxValue; length++)
+            {
+                byte[] a = new byte[length];
+                for (int i = 0; i < length; i++)
+                {
+                    byte val = (byte)(i + 1);
+                    a[i] = val == 200 ? (byte)201 : val;
+                }
+
+                a[length - 1] = 200;
+                a[length - 2] = 200;
+
+                ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(a);
+
+                bool found = span.Contains<byte>(200);
+                Assert.True(found);
+            }
+        }
+
+        [Fact]
+        public static void MakeSureNoContainsChecksGoOutOfRange_Byte()
+        {
+            for (int length = 0; length <= byte.MaxValue; length++)
+            {
+                byte[] a = new byte[length + 2];
+                a[0] = 99;
+                a[length + 1] = 99;
+                ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(a, 1, length);
+
+                bool found = span.Contains<byte>(99);
+                Assert.False(found);
+            }
+        }
+    }
+}
index bf075b5..5cf4374 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
     <ProjectGuid>{15DB0DCC-68B4-4CFB-8BD2-F26BCCAF5A3F}</ProjectGuid>
@@ -11,6 +11,8 @@
     <Compile Include="MemoryMarshal\AsReadOnlyRef.cs" />
     <Compile Include="MemoryMarshal\CreateSpan.cs" />
     <Compile Include="MemoryMarshal\CreateReadOnlySpan.cs" />
+    <Compile Include="ReadOnlySpan\Contains.byte.cs" />
+    <Compile Include="ReadOnlySpan\Contains.T.cs" />
     <Compile Include="Span\Reflection.cs" />
   </ItemGroup>
   <ItemGroup>
index 9c4631d..260955d 100644 (file)
@@ -304,7 +304,7 @@ namespace System.Net
                     return Array.Empty<string>();
                 }
             }
-            else if (!hostname.Contains("."))
+            else if (!hostname.Contains('.'))
             {
                 // for a dotless name, try to resolve the FQDN.  If the caller doesn't have DNS permission
                 // or the query fails for some reason, add only the dotless name.
index 252d4f9..34f4f2d 100644 (file)
@@ -11,7 +11,6 @@ namespace System.Xml.Serialization
     using System.Collections;
     using System.ComponentModel;
     using System.Threading;
-    using System.Linq;
     using System.Xml;
     using System.Xml.Serialization;
 
index 421d7e7..f9004db 100644 (file)
@@ -12,7 +12,6 @@ namespace System.Xml.Serialization
     using System.Globalization;
     using System.Threading;
     using System.Diagnostics;
-    using System.Linq;
     using System.Collections.Generic;
     using System.Xml.Extensions;
     using System.Xml;
index ce06534..b53c26d 100644 (file)
@@ -107,6 +107,9 @@ namespace System.Tests
         public static void Contains(string s, char value, bool expected)
         {
             Assert.Equal(expected, s.Contains(value));
+
+            ReadOnlySpan<char> span = s.AsSpan();
+            Assert.Equal(expected, span.Contains(value));
         }
 
         [Theory]
@@ -256,7 +259,9 @@ namespace System.Tests
             Assert.False("".AsSpan().Contains('a'));
 
             // Use a long-enough string to incur vectorization code
-            for (var length = 1; length < 250; length++)
+            const int max = 250;
+
+            for (var length = 1; length < max; length++)
             {
                 char[] ca = new char[length];
                 for (int i = 0; i < length; i++)
@@ -264,22 +269,109 @@ namespace System.Tests
                     ca[i] = (char)(i + 1);
                 }
 
-                var str = new string(ca);
                 var span = new Span<char>(ca);
                 var ros = new ReadOnlySpan<char>(ca);
+                var str = new string(ca);
 
                 for (var targetIndex = 0; targetIndex < length; targetIndex++)
                 {
                     char target = ca[targetIndex];
-                    var found = str.Contains(target);
-                    Assert.True(found);
 
-                    found = span.Contains(target);
+                    // Span
+                    bool found = span.Contains(target);
                     Assert.True(found);
 
+                    // ReadOnlySpan
                     found = ros.Contains(target);
                     Assert.True(found);
+
+                    // String
+                    found = str.Contains(target);
+                    Assert.True(found);
+                }
+            }
+        }
+
+        [Fact]
+        public static void Contains_ZeroLength_Char()
+        {
+            // Span
+            var span = new Span<char>(Array.Empty<char>());
+            bool found = span.Contains((char)0);
+            Assert.False(found);
+
+            span = Span<char>.Empty;
+            found = span.Contains((char)0);
+            Assert.False(found);
+
+            // ReadOnlySpan
+            var ros = new ReadOnlySpan<char>(Array.Empty<char>());
+            found = ros.Contains((char)0);
+            Assert.False(found);
+
+            ros = ReadOnlySpan<char>.Empty;
+            found = ros.Contains((char)0);
+            Assert.False(found);
+
+            // String
+            found = string.Empty.Contains((char)0);
+            Assert.False(found);
+        }
+
+        [Fact]
+        public static void Contains_MultipleMatches_Char()
+        {
+            for (int length = 2; length < 32; length++)
+            {
+                var ca = new char[length];
+                for (int i = 0; i < length; i++)
+                {
+                    ca[i] = (char)(i + 1);
                 }
+
+                ca[length - 1] = (char)200;
+                ca[length - 2] = (char)200;
+
+                // Span
+                var span = new Span<char>(ca);
+                bool found = span.Contains((char)200);
+                Assert.True(found);
+
+                // ReadOnlySpan
+                var ros = new ReadOnlySpan<char>(ca);
+                found = ros.Contains((char)200);
+                Assert.True(found);
+
+                // String
+                var str = new string(ca);
+                found = str.Contains((char)200);
+                Assert.True(found);
+            }
+        }
+
+        [Fact]
+        public static void Contains_EnsureNoChecksGoOutOfRange_Char()
+        {
+            for (int length = 0; length < 100; length++)
+            {
+                var ca = new char[length + 2];
+                ca[0] = '9';
+                ca[length + 1] = '9';
+
+                // Span
+                var span = new Span<char>(ca, 1, length);
+                bool found = span.Contains('9');
+                Assert.False(found);
+
+                // ReadOnlySpan
+                var ros = new ReadOnlySpan<char>(ca, 1, length);
+                found = ros.Contains('9');
+                Assert.False(found);
+
+                // String
+                var str = new string(ca, 1, length);
+                found = str.Contains('9');
+                Assert.False(found);
             }
         }