diff --git a/src/EntityFramework/Core/Query/InternalTrees/VarMap.cs b/src/EntityFramework/Core/Query/InternalTrees/VarMap.cs index 23d53ba1c6..193c172209 100644 --- a/src/EntityFramework/Core/Query/InternalTrees/VarMap.cs +++ b/src/EntityFramework/Core/Query/InternalTrees/VarMap.cs @@ -2,6 +2,7 @@ namespace System.Data.Entity.Core.Query.InternalTrees { + using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text; @@ -9,25 +10,21 @@ namespace System.Data.Entity.Core.Query.InternalTrees // // Helps map one variable to the next. // - internal class VarMap : Dictionary + internal class VarMap : IDictionary { #region public surfaces + private Dictionary map; + private Dictionary reverseMap; + internal VarMap GetReverseMap() { - var reverseMap = new VarMap(); - foreach (var kv in this) - { - Var x; - // On the odd chance that a var is in the varMap more than once, the first one - // is going to be the one we want to use, because it might be the discriminator - // var; - if (!reverseMap.TryGetValue(kv.Value, out x)) - { - reverseMap[kv.Value] = kv.Key; - } - } - return reverseMap; + return new VarMap(reverseMap, map); + } + + public bool ContainsValue(Var value) + { + return reverseMap.ContainsKey(value); } public override string ToString() @@ -35,7 +32,7 @@ public override string ToString() var sb = new StringBuilder(); var separator = string.Empty; - foreach (var v in Keys) + foreach (var v in map.Keys) { sb.AppendFormat(CultureInfo.InvariantCulture, "{0}({1},{2})", separator, v.Id, this[v].Id); separator = ","; @@ -45,8 +42,134 @@ public override string ToString() #endregion + #region IDictionary + + public Var this[Var key] + { + get + { + return map[key]; + } + set + { + map[key] = value; + } + } + + public ICollection Keys + { + get + { + return map.Keys; + } + } + + public ICollection Values + { + get + { + return map.Values; + } + } + + public int Count + { + get + { + return map.Count; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + public void Add(Var key, Var value) + { + if (!reverseMap.ContainsKey(value)) + { + reverseMap.Add(value, key); + } + map.Add(key, value); + } + + public void Add(KeyValuePair item) + { + if (!reverseMap.ContainsKey(item.Value)) + { + ((IDictionary)reverseMap).Add(new KeyValuePair(item.Value, item.Key)); + } + ((IDictionary)map).Add(item); + } + + public void Clear() + { + map.Clear(); + reverseMap.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((IDictionary)map).Contains(item); + } + + public bool ContainsKey(Var key) + { + return map.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)map).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return map.GetEnumerator(); + } + + public bool Remove(Var key) + { + reverseMap.Remove(map[key]); + return map.Remove(key); + } + + public bool Remove(KeyValuePair item) + { + reverseMap.Remove(map[item.Value]); + return ((IDictionary)map).Remove(item); + } + + public bool TryGetValue(Var key, out Var value) + { + return ((IDictionary)map).TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return map.GetEnumerator(); + } + + #endregion + #region constructors + public VarMap() + { + map = new Dictionary(); + reverseMap = new Dictionary(); + } + + private VarMap(Dictionary map, Dictionary reverseMap) + { + this.map = map; + this.reverseMap = reverseMap; + } + #endregion } } diff --git a/src/EntityFramework/Core/Query/InternalTrees/VarVec.cs b/src/EntityFramework/Core/Query/InternalTrees/VarVec.cs index 427341c5d6..07a9d2c97a 100644 --- a/src/EntityFramework/Core/Query/InternalTrees/VarVec.cs +++ b/src/EntityFramework/Core/Query/InternalTrees/VarVec.cs @@ -3,6 +3,7 @@ namespace System.Data.Entity.Core.Query.InternalTrees { using System.Collections; + using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Text; @@ -28,7 +29,7 @@ internal class VarVecEnumerator : IEnumerator, IDisposable private int m_position; private Command m_command; - private BitArray m_bitArray; + private BitVec m_bitArray; #endregion @@ -77,19 +78,54 @@ object IEnumerator.Current get { return Current; } } + static readonly int[] MultiplyDeBruijnBitPosition = + { + 0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, + 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9 + }; + // // Move to the next position // public bool MoveNext() { + int[] values = m_bitArray.m_array; m_position++; - for (; m_position < m_bitArray.Length; m_position++) + int length = m_bitArray.Length; + int valuesLen = BitVec.GetArrayLength(length, 32); + int i = m_position / 32; + int v = 0, mask = 0; + + if (i < valuesLen) { - if (m_bitArray[m_position]) + + v = values[i]; + // zero lowest bits that are skipped + mask = (~0 << (m_position % 32)); + + v &= mask; + + if (v != 0) { + m_position = (i * 32) + MultiplyDeBruijnBitPosition[((uint)((v & -v) * 0x077CB531U)) >> 27]; + return true; + } + + i++; + for (; i < valuesLen; i++) + { + v = values[i]; + + if (v == 0) + { + continue; + } + + m_position = (i * 32) + MultiplyDeBruijnBitPosition[((uint)((v & -v) * 0x077CB531U)) >> 27]; return true; } } + m_position = length; return false; } @@ -174,10 +210,26 @@ internal bool Overlaps(VarVec other) // internal bool Subsumes(VarVec other) { - for (var i = 0; i < other.m_bitVector.Length; i++) + int[] values = m_bitVector.m_array; + int[] otherValues = other.m_bitVector.m_array; + + // if the other is longer, and it has a bit set past the current vector's length return false + if (otherValues.Length > values.Length) { - if (other.m_bitVector[i] - && ((i >= m_bitVector.Length) || !m_bitVector[i])) + for (var i = values.Length; i < otherValues.Length; i++) + { + if (otherValues[i] != 0) + { + return false; + } + } + } + + int length = Math.Min(otherValues.Length, values.Length); + + for (var i = 0; i < length; i++) + { + if (!((values[i] & otherValues[i]) == otherValues[i])) { return false; } @@ -287,7 +339,7 @@ internal Var First // // dictionary of renamed vars // a new VarVec - internal VarVec Remap(Dictionary varMap) + internal VarVec Remap(IDictionary varMap) { var newVec = m_command.CreateVarVec(); foreach (var v in this) @@ -308,7 +360,7 @@ internal VarVec Remap(Dictionary varMap) internal VarVec(Command command) { - m_bitVector = new BitArray(64); + m_bitVector = new BitVec(64); m_command = command; } @@ -361,7 +413,7 @@ public override string ToString() #region private state - private readonly BitArray m_bitVector; + private readonly BitVec m_bitVector; private readonly Command m_command; #endregion @@ -380,4 +432,465 @@ public VarVec Clone() #endregion } + + internal class BitVec + { + private BitVec() + { + } + + /*========================================================================= + ** Allocates space to hold length bit values. All of the values in the bit + ** array are set to false. + ** + ** Exceptions: ArgumentException if length < 0. + =========================================================================*/ + public BitVec(int length) + : this(length, false) + { + } + + /*========================================================================= + ** Allocates space to hold length bit values. All of the values in the bit + ** array are set to defaultValue. + ** + ** Exceptions: ArgumentOutOfRangeException if length < 0. + =========================================================================*/ + public BitVec(int length, bool defaultValue) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException("length", "ArgumentOutOfRange_NeedNonNegNum"); + } + + m_array = ArrayPool.Instance.GetArray(GetArrayLength(length, BitsPerInt32)); + m_length = length; + + int fillValue = defaultValue ? unchecked(((int)0xffffffff)) : 0; + for (int i = 0; i < m_array.Length; i++) + { + m_array[i] = fillValue; + } + + _version = 0; + } + + /*========================================================================= + ** Allocates space to hold the bit values in bytes. bytes[0] represents + ** bits 0 - 7, bytes[1] represents bits 8 - 15, etc. The LSB of each byte + ** represents the lowest index value; bytes[0] & 1 represents bit 0, + ** bytes[0] & 2 represents bit 1, bytes[0] & 4 represents bit 2, etc. + ** + ** Exceptions: ArgumentException if bytes == null. + =========================================================================*/ + public BitVec(byte[] bytes) + { + if (bytes == null) + { + throw new ArgumentNullException("bytes"); + } + + // this value is chosen to prevent overflow when computing m_length. + // m_length is of type int32 and is exposed as a property, so + // type of m_length can't be changed to accommodate. + if (bytes.Length > Int32.MaxValue / BitsPerByte) + { + throw new ArgumentException("Argument_ArrayTooLarge", "bytes"); + } + + m_array = ArrayPool.Instance.GetArray(GetArrayLength(bytes.Length, BytesPerInt32)); + m_length = bytes.Length * BitsPerByte; + + int i = 0; + int j = 0; + while (bytes.Length - j >= 4) + { + m_array[i++] = (bytes[j] & 0xff) | + ((bytes[j + 1] & 0xff) << 8) | + ((bytes[j + 2] & 0xff) << 16) | + ((bytes[j + 3] & 0xff) << 24); + j += 4; + } + + switch (bytes.Length - j) + { + case 3: + m_array[i] = ((bytes[j + 2] & 0xff) << 16); + goto case 2; + // fall through + case 2: + m_array[i] |= ((bytes[j + 1] & 0xff) << 8); + goto case 1; + // fall through + case 1: + m_array[i] |= (bytes[j] & 0xff); + break; + } + + _version = 0; + } + + public BitVec(bool[] values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + m_array = ArrayPool.Instance.GetArray(GetArrayLength(values.Length, BitsPerInt32)); + m_length = values.Length; + + for (int i = 0; i < values.Length; i++) + { + if (values[i]) + m_array[i / 32] |= (1 << (i % 32)); + } + + _version = 0; + + } + + /*========================================================================= + ** Allocates space to hold the bit values in values. values[0] represents + ** bits 0 - 31, values[1] represents bits 32 - 63, etc. The LSB of each + ** integer represents the lowest index value; values[0] & 1 represents bit + ** 0, values[0] & 2 represents bit 1, values[0] & 4 represents bit 2, etc. + ** + ** Exceptions: ArgumentException if values == null. + =========================================================================*/ + public BitVec(int[] values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + // this value is chosen to prevent overflow when computing m_length + if (values.Length > Int32.MaxValue / BitsPerInt32) + { + //throw new ArgumentException(Environment.GetResourceString("Argument_ArrayTooLarge", BitsPerInt32), "values"); + } + + m_array = ArrayPool.Instance.GetArray(values.Length); + m_length = values.Length * BitsPerInt32; + + Array.Copy(values, m_array, values.Length); + + _version = 0; + } + + /*========================================================================= + ** Allocates a new BitVec with the same length and bit values as bits. + ** + ** Exceptions: ArgumentException if bits == null. + =========================================================================*/ + public BitVec(BitVec bits) + { + if (bits == null) + { + throw new ArgumentNullException("bits"); + } + + int arrayLength = GetArrayLength(bits.m_length, BitsPerInt32); + m_array = ArrayPool.Instance.GetArray(arrayLength); + m_length = bits.m_length; + + Array.Copy(bits.m_array, m_array, arrayLength); + + _version = bits._version; + } + + public bool this[int index] + { + get + { + return Get(index); + } + set + { + Set(index, value); + } + } + + /*========================================================================= + ** Returns the bit value at position index. + ** + ** Exceptions: ArgumentOutOfRangeException if index < 0 or + ** index >= GetLength(). + =========================================================================*/ + public bool Get(int index) + { + if (index < 0 || index >= Length) + { + throw new ArgumentOutOfRangeException("index", "ArgumentOutOfRange_Index"); + } + + return (m_array[index / 32] & (1 << (index % 32))) != 0; + } + + /*========================================================================= + ** Sets the bit value at position index to value. + ** + ** Exceptions: ArgumentOutOfRangeException if index < 0 or + ** index >= GetLength(). + =========================================================================*/ + public void Set(int index, bool value) + { + if (index < 0 || index >= Length) + { + throw new ArgumentOutOfRangeException("index", "ArgumentOutOfRange_Index"); + } + + if (value) + { + m_array[index / 32] |= (1 << (index % 32)); + } + else + { + m_array[index / 32] &= ~(1 << (index % 32)); + } + + _version++; + } + + /*========================================================================= + ** Sets all the bit values to value. + =========================================================================*/ + public void SetAll(bool value) + { + int fillValue = value ? unchecked(((int)0xffffffff)) : 0; + int ints = GetArrayLength(m_length, BitsPerInt32); + for (int i = 0; i < ints; i++) + { + m_array[i] = fillValue; + } + + _version++; + } + + /*========================================================================= + ** Returns a reference to the current instance ANDed with value. + ** + ** Exceptions: ArgumentException if value == null or + ** value.Length != this.Length. + =========================================================================*/ + public BitVec And(BitVec value) + { + if (value == null) + throw new ArgumentNullException("value"); + if (Length != value.Length) + throw new ArgumentException("Arg_ArrayLengthsDiffer"); + + int ints = GetArrayLength(m_length, BitsPerInt32); + for (int i = 0; i < ints; i++) + { + m_array[i] &= value.m_array[i]; + } + + _version++; + return this; + } + + /*========================================================================= + ** Returns a reference to the current instance ORed with value. + ** + ** Exceptions: ArgumentException if value == null or + ** value.Length != this.Length. + =========================================================================*/ + public BitVec Or(BitVec value) + { + if (value == null) + throw new ArgumentNullException("value"); + if (Length != value.Length) + throw new ArgumentException("Arg_ArrayLengthsDiffer"); + + int ints = GetArrayLength(m_length, BitsPerInt32); + for (int i = 0; i < ints; i++) + { + m_array[i] |= value.m_array[i]; + } + + _version++; + return this; + } + + /*========================================================================= + ** Returns a reference to the current instance XORed with value. + ** + ** Exceptions: ArgumentException if value == null or + ** value.Length != this.Length. + =========================================================================*/ + public BitVec Xor(BitVec value) + { + if (value == null) + throw new ArgumentNullException("value"); + if (Length != value.Length) + throw new ArgumentException("Arg_ArrayLengthsDiffer"); + + int ints = GetArrayLength(m_length, BitsPerInt32); + for (int i = 0; i < ints; i++) + { + m_array[i] ^= value.m_array[i]; + } + + _version++; + return this; + } + + /*========================================================================= + ** Inverts all the bit values. On/true bit values are converted to + ** off/false. Off/false bit values are turned on/true. The current instance + ** is updated and returned. + =========================================================================*/ + public BitVec Not() + { + int ints = GetArrayLength(m_length, BitsPerInt32); + for (int i = 0; i < ints; i++) + { + m_array[i] = ~m_array[i]; + } + + _version++; + return this; + } + + public int Length + { + get + { + return m_length; + } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("value", "ArgumentOutOfRange_NeedNonNegNum"); + } + + int newints = GetArraySize(value, BitsPerInt32); + if (newints > m_array.Length || newints + _ShrinkThreshold < m_array.Length) + { + // grow or shrink (if wasting more than _ShrinkThreshold ints) + int[] newarray = ArrayPool.Instance.GetArray(newints); //new int[newints]; + Array.Copy(m_array, newarray, newints > m_array.Length ? m_array.Length : newints); + ArrayPool.Instance.PutArray(m_array); + m_array = newarray; + } + + if (value > m_length) + { + // clear high bit values in the last int + int last = GetArrayLength(m_length, BitsPerInt32) - 1; + int bits = m_length % 32; + if (bits > 0) + { + m_array[last] &= (1 << bits) - 1; + } + + // clear remaining int values + Array.Clear(m_array, last + 1, newints - last - 1); + } + + m_length = value; + _version++; + } + } + + // XPerY=n means that n Xs can be stored in 1 Y. + private const int BitsPerInt32 = 32; + private const int BytesPerInt32 = 4; + private const int BitsPerByte = 8; + + /// + /// Used for conversion between different representations of bit array. + /// Returns (n+(div-1))/div, rearranged to avoid arithmetic overflow. + /// For example, in the bit to int case, the straightforward calc would + /// be (n+31)/32, but that would cause overflow. So instead it's + /// rearranged to ((n-1)/32) + 1, with special casing for 0. + /// + /// Usage: + /// GetArrayLength(77, BitsPerInt32): returns how many ints must be + /// allocated to store 77 bits. + /// + /// length of array + /// use a conversion constant, e.g. BytesPerInt32 to get + /// how many ints are required to store n bytes + /// length of the array + public static int GetArrayLength(int n, int div) + { + return n > 0 ? (((n - 1) / div) + 1) : 0; + } + + private static int GetArraySize(int n, int div) + { + // compute the next highest power of 2 of 32-bit v + uint v = Convert.ToUInt32(GetArrayLength(n, div)); + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + + return Convert.ToInt32(v); + } + + public int[] m_array; + private int m_length; + private int _version; + private const int _ShrinkThreshold = 1024; //256; + + private class ArrayPool + { + private Dictionary> dictionary; + + private ArrayPool() + { + dictionary = new Dictionary>(); + } + + private static readonly ArrayPool instance = new ArrayPool(); + + public static ArrayPool Instance + { + get + { + return instance; + } + } + + public int[] GetArray(int length) + { + ConcurrentBag arrays = GetBag(length); + + int[] arr; + if (arrays.TryTake(out arr)) return arr; + + return new int[length]; + } + + private ConcurrentBag GetBag(int length) + { + ConcurrentBag arrays; + if (!dictionary.ContainsKey(length)) + { + arrays = new ConcurrentBag(); + dictionary[length] = arrays; + } + else + { + arrays = dictionary[length]; + } + return arrays; + } + + public void PutArray(int[] arr) + { + ConcurrentBag arrays = GetBag(arr.Length); + Array.Clear(arr, 0, arr.Length); + arrays.Add(arr); + } + } + } } diff --git a/src/EntityFramework/Core/Query/PlanCompiler/ColumnMapTranslator.cs b/src/EntityFramework/Core/Query/PlanCompiler/ColumnMapTranslator.cs index e368f43b4c..17d80f1214 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/ColumnMapTranslator.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/ColumnMapTranslator.cs @@ -49,7 +49,7 @@ private ColumnMapTranslator() // replacement. Note that we will follow the chain of replacements, in // case the replacement was also replaced. // - private static Var GetReplacementVar(Var originalVar, Dictionary replacementVarMap) + private static Var GetReplacementVar(Var originalVar, IDictionary replacementVarMap) { // SQLBUDT #478509: Follow the chain of mapped vars, don't // just stop at the first one @@ -124,7 +124,7 @@ internal static ColumnMap Translate(ColumnMap columnMapToTranslate, Dictionary // Replace VarRefColumnMaps with new VarRefColumnMaps with the specified Var // - internal static ColumnMap Translate(ColumnMap columnMapToTranslate, Dictionary varToVarMap) + internal static ColumnMap Translate(ColumnMap columnMapToTranslate, IDictionary varToVarMap) { var result = Translate( columnMapToTranslate, diff --git a/src/EntityFramework/Core/Query/PlanCompiler/ITreeGenerator.cs b/src/EntityFramework/Core/Query/PlanCompiler/ITreeGenerator.cs index fc7b9b92b4..541114bb5a 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/ITreeGenerator.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/ITreeGenerator.cs @@ -163,7 +163,7 @@ internal override bool IsPredicate(string name) [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "OpCopier")] [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Data.Entity.Core.Query.PlanCompiler.PlanCompiler.Assert(System.Boolean,System.String)")] - private void MapCopiedNodeVars(IList sources, IList copies, Dictionary varMappings) + private void MapCopiedNodeVars(IList sources, IList copies, IDictionary varMappings) { PlanCompiler.Assert(sources.Count == copies.Count, "Source/Copy Node count mismatch"); diff --git a/src/EntityFramework/Core/Query/PlanCompiler/NestPullup.cs b/src/EntityFramework/Core/Query/PlanCompiler/NestPullup.cs index 9a18404bdf..9c3c864080 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/NestPullup.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/NestPullup.cs @@ -2533,7 +2533,7 @@ private Node BuildUnionAllSubqueryForNestOp( private static VarList GetUnionOutputs(UnionAllOp unionOp, VarList leftVars) { var varMap = unionOp.VarMap[0]; - Dictionary reverseVarMap = varMap.GetReverseMap(); + IDictionary reverseVarMap = varMap.GetReverseMap(); var unionAllVars = Command.CreateVarList(); foreach (var v in leftVars) diff --git a/src/EntityFramework/Core/Query/PlanCompiler/PlanCompiler.cs b/src/EntityFramework/Core/Query/PlanCompiler/PlanCompiler.cs index 62d4c677e9..1da99a5fa2 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/PlanCompiler.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/PlanCompiler.cs @@ -57,7 +57,7 @@ internal class PlanCompiler // Determines the maximum size of the query in terms of Iqt nodes for which we attempt to do transformation rules. // This number is ignored if applyTransformationsRegardlessOfSize is enabled. // - private const int MaxNodeCountForTransformations = 100000; + private const int MaxNodeCountForTransformations = 10000; // // The CTree we're compiling a plan for. diff --git a/src/EntityFramework/Core/Query/PlanCompiler/VarRemapper.cs b/src/EntityFramework/Core/Query/PlanCompiler/VarRemapper.cs index 3c0f6fa56a..151939c78e 100644 --- a/src/EntityFramework/Core/Query/PlanCompiler/VarRemapper.cs +++ b/src/EntityFramework/Core/Query/PlanCompiler/VarRemapper.cs @@ -13,7 +13,7 @@ internal class VarRemapper : BasicOpVisitor { #region Private state - private readonly Dictionary m_varMap; + private readonly IDictionary m_varMap; protected readonly Command m_command; #endregion @@ -34,7 +34,7 @@ internal VarRemapper(Command command) // // Current iqt command // Var map to be used - internal VarRemapper(Command command, Dictionary varMap) + internal VarRemapper(Command command, IDictionary varMap) { m_command = command; m_varMap = varMap; @@ -102,7 +102,7 @@ internal VarList RemapVarList(VarList varList) // // Remap the given varList using the given varMap // - internal static VarList RemapVarList(Command command, Dictionary varMap, VarList varList) + internal static VarList RemapVarList(Command command, IDictionary varMap, VarList varList) { var varRemapper = new VarRemapper(command, varMap); return varRemapper.RemapVarList(varList); diff --git a/test/EntityFramework/UnitTests/Core/Query/InternalTrees/VarVecTests.cs b/test/EntityFramework/UnitTests/Core/Query/InternalTrees/VarVecTests.cs index b27a7fd757..a80a5a1690 100644 --- a/test/EntityFramework/UnitTests/Core/Query/InternalTrees/VarVecTests.cs +++ b/test/EntityFramework/UnitTests/Core/Query/InternalTrees/VarVecTests.cs @@ -11,7 +11,8 @@ public class VarVecTests [Fact] public void MoveNext_returns_true_for_true_bits_and_false_when_end_is_reached() { - var enumerator = new VarVec.VarVecEnumerator(CreateVarVec(1)); + var enumerator = new VarVec.VarVecEnumerator(CreateVarVec(1, 0, 1)); + Assert.True(enumerator.MoveNext()); Assert.True(enumerator.MoveNext()); Assert.False(enumerator.MoveNext()); }