Уважаемые читатели, в этой статье я хочу рассказать о небольших тестах получения обратного массива и представить свои выводы. Тесты сделаны на .net 7.
Тестировал с использованием BenchmarkDotNet, так что каждый может проверить результаты и сделать свои выводы.
Сразу отмечу, что для тестов используется атрибут [GlobalSetup], что позволяет не переживать о стартовых данных, так как они будут «Executed once per each N value» и это нам и надо.
Для полной картины происходящего основные тесты идут на массивах с количеством элементов 1000, 10_000, 100_000, 1_000_000, 100_000_000, а самый последний с добавлением до 1_000_000_000 чисел.
Первые тесты связаны с обработкой исходного массива без получения нового, что не всегда верный выбор, но памяти почти всегда не требует (тесты покажут странность на крупной выборке).
Для старта используется обычный While, представленный в более удобной записи, чем аналоги на просторах сети:
[Benchmark] public int[] While() { if (array.Length == 0) { return Array.Empty<int>(); } int i = -1; int j = array.Length; while (i++ < j--) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } return array; }
Самый часто встречаемый ответ на данный вопрос:
[Benchmark] public int[] Array_Reverse() { Array.Reverse(array); return array; }
Не скажу, что тут все плохо, ведь под капотом там много работы с unsafe и разной «магии» для ускорения:
public unsafe static void Reverse<[Nullable(2)] T>(T[] array) { if (array == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); } if (array.Length > 1) { SpanHelpers.Reverse(ref MemoryMarshal.GetArrayDataReference(array), (UIntPtr)(void*)array.Length); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Reverse<T>(ref T elements, UIntPtr length) { if (!RuntimeHelpers.IsReferenceOrContainsReferences<T>()) { if (Unsafe.SizeOf<T>() == 1) { Reverse(ref Unsafe.As<T, byte>(ref elements), length); return; } if (Unsafe.SizeOf<T>() == 2) { Reverse(ref Unsafe.As<T, char>(ref elements), length); return; } if (Unsafe.SizeOf<T>() == 4) { Reverse(ref Unsafe.As<T, int>(ref elements), length); return; } if (Unsafe.SizeOf<T>() == 8) { Reverse(ref Unsafe.As<T, long>(ref elements), length); return; } } ReverseInner(ref elements, length); } public unsafe static void Reverse(ref long buf, UIntPtr length) { if (Avx2.IsSupported && (ulong)(UIntPtr)(void*)((long)Vector256<long>.Count * 2L) <= (ulong)length) { UIntPtr uIntPtr = (UIntPtr)(void*)Vector256<long>.Count; UIntPtr uIntPtr2 = (UIntPtr)(void*)((ulong)(UIntPtr)(void*)((ulong)length / (ulong)uIntPtr) / 2uL); for (UIntPtr uIntPtr3 = (UIntPtr)(void*)null; (ulong)uIntPtr3 < (ulong)uIntPtr2; uIntPtr3 = (UIntPtr)(void*)((ulong)(long)(ulong)uIntPtr3 + 1uL)) { UIntPtr elementOffset = (UIntPtr)(void*)((ulong)(long)(ulong)uIntPtr3 * (ulong)(long)(ulong)uIntPtr); UIntPtr elementOffset2 = (UIntPtr)(void*)((ulong)(long)(ulong)length - (ulong)(long)(IntPtr)(void*)((long)(IntPtr)(void*)(1L + (long)(ulong)uIntPtr3) * (long)(ulong)uIntPtr)); Vector256<long> value = Vector256.LoadUnsafe(ref buf, elementOffset); Vector256<long> value2 = Vector256.LoadUnsafe(ref buf, elementOffset2); value = Avx2.Permute4x64(value, 27); value2 = Avx2.Permute4x64(value2, 27); value2.StoreUnsafe(ref buf, elementOffset); value.StoreUnsafe(ref buf, elementOffset2); } buf = ref Unsafe.Add(ref buf, (UIntPtr)(void*)((ulong)(long)(ulong)uIntPtr2 * (ulong)(long)(ulong)uIntPtr)); length = (UIntPtr)(void*)((ulong)(long)(ulong)length - (ulong)(long)(IntPtr)(void*)((long)(IntPtr)(void*)((long)(ulong)uIntPtr2 * (long)(ulong)uIntPtr) * 2L)); } else if (Vector128.IsHardwareAccelerated && (ulong)(UIntPtr)(void*)((long)Vector128<long>.Count * 2L) <= (ulong)length) { UIntPtr uIntPtr4 = (UIntPtr)(void*)Vector128<long>.Count; UIntPtr uIntPtr5 = (UIntPtr)(void*)((ulong)(UIntPtr)(void*)((ulong)length / (ulong)uIntPtr4) / 2uL); for (UIntPtr uIntPtr6 = (UIntPtr)(void*)null; (ulong)uIntPtr6 < (ulong)uIntPtr5; uIntPtr6 = (UIntPtr)(void*)((ulong)(long)(ulong)uIntPtr6 + 1uL)) { UIntPtr elementOffset3 = (UIntPtr)(void*)((ulong)(long)(ulong)uIntPtr6 * (ulong)(long)(ulong)uIntPtr4); UIntPtr elementOffset4 = (UIntPtr)(void*)((ulong)(long)(ulong)length - (ulong)(long)(IntPtr)(void*)((long)(IntPtr)(void*)(1L + (long)(ulong)uIntPtr6) * (long)(ulong)uIntPtr4)); Vector128<long> vector = Vector128.LoadUnsafe(ref buf, elementOffset3); Vector128<long> vector2 = Vector128.LoadUnsafe(ref buf, elementOffset4); vector = Vector128.Shuffle(vector, Vector128.Create(1L, 0L)); vector2 = Vector128.Shuffle(vector2, Vector128.Create(1L, 0L)); vector2.StoreUnsafe(ref buf, elementOffset3); vector.StoreUnsafe(ref buf, elementOffset4); } buf = ref Unsafe.Add(ref buf, (UIntPtr)(void*)((ulong)(long)(ulong)uIntPtr5 * (ulong)(long)(ulong)uIntPtr4)); length = (UIntPtr)(void*)((ulong)(long)(ulong)length - (ulong)(long)(IntPtr)(void*)((long)(IntPtr)(void*)((long)(ulong)uIntPtr5 * (long)Vector128<long>.Count) * 2L)); } ReverseInner(ref buf, length); }
Для разнообразия проверим сортировку:
[Benchmark] public int[] Array_Sort() { Array.Sort(array, 0, array.Length, _comparer); return array; }
public static void Sort<[Nullable(2)] T>(T[] array, int index, int length, [Nullable(new byte[] { 2, 1 })] IComparer<T> comparer) { if (array == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array); } if (index < 0) { ThrowHelper.ThrowIndexArgumentOutOfRange_NeedNonNegNumException(); } if (length < 0) { ThrowHelper.ThrowLengthArgumentOutOfRange_ArgumentOutOfRange_NeedNonNegNum(); } if (array.Length - index < length) { ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen); } if (length > 1) { Span<T> keys = new Span<T>(ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(array), index), length); ArraySortHelper<T>.Default.Sort(keys, comparer); } }
Далее следует несколько вариантов цикла for, каждый из которых не совсем привычный:
[Benchmark] public int[] For() { for (int i = 0, length = array.Length - 1; i < length / 2; i++) { int item = array[i]; array[i] = array[length - i]; array[length - i] = item; } return array; } [Benchmark] public int[] For_Size() { for (int i = 0, length = array.Length - 1, size = length / 2; i < size; i++) { int item = array[i]; array[i] = array[length - i]; array[length - i] = item; } return array; }
Для примера приведу 2 варианта, но далее в полном коде можно посмотреть все доступные решения. Отмечу, что столь простые с виду улучшения помогают чуть ускориться, но тут каждый сам выбирает как удобней делать.
Сайт https://sharplab.io показывает следующее на For():
using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] public class C { private static int[] array; public void M() { int i = 0; for (int num = array.Length - 1; i < num / 2; i++) { int num2 = array[i]; array[i] = array[num - i]; array[num - i] = num2; } } static C() { int[] obj = new int[15]; RuntimeHelpers.InitializeArray(obj, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); array = obj; } } [CompilerGenerated] internal sealed class <PrivateImplementationDetails> { [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 60)] private struct __StaticArrayInitTypeSize=60 { } internal static readonly __StaticArrayInitTypeSize=60 5A8B4F50AC386D79A3A5947122333429A35EBC81F89E807236ED66A4BB8FE498/* Not supported: data(01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00 0B 00 00 00 0C 00 00 00 0D 00 00 00 0E 00 00 00 0F 00 00 00) */; }
В то же время для For_Size():
using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] public class C { private static int[] array; public void M() { int i = 0; int num = array.Length - 1; for (int num2 = num / 2; i < num2; i++) { int num3 = array[i]; array[i] = array[num - i]; array[num - i] = num3; } } static C() { int[] obj = new int[15]; RuntimeHelpers.InitializeArray(obj, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); array = obj; } } [CompilerGenerated] internal sealed class <PrivateImplementationDetails> { [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 60)] private struct __StaticArrayInitTypeSize=60 { } internal static readonly __StaticArrayInitTypeSize=60 5A8B4F50AC386D79A3A5947122333429A35EBC81F89E807236ED66A4BB8FE498/* Not supported: data(01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00 0B 00 00 00 0C 00 00 00 0D 00 00 00 0E 00 00 00 0F 00 00 00) */; }
И для ознакомления For_Unsafe_SameArray:
using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] public class C { private static int[] array; public void M() { For_Unsafe_SameArray(); } public unsafe int[] For_Unsafe_SameArray() { fixed (int* ptr = array) { int i = 0; for (int num = array.Length - 1; i < num / 2; i++) { int num2 = ptr[i]; ptr[i] = ptr[num - i]; ptr[num - i] = num2; } } return array; } static C() { int[] obj = new int[15]; RuntimeHelpers.InitializeArray(obj, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); array = obj; } } [CompilerGenerated] internal sealed class <PrivateImplementationDetails> { [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 60)] private struct __StaticArrayInitTypeSize=60 { } internal static readonly __StaticArrayInitTypeSize=60 5A8B4F50AC386D79A3A5947122333429A35EBC81F89E807236ED66A4BB8FE498/* Not supported: data(01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 09 00 00 00 0A 00 00 00 0B 00 00 00 0C 00 00 00 0D 00 00 00 0E 00 00 00 0F 00 00 00) */; }
Дополнительно в итоговом листинге представлена работа с unsafe и range в разных вариациях.
Hidden text
using BenchmarkDotNet.Attributes; namespace Benchmarks.Benchmarks.Arrays; [MemoryDiagnoser] public class ArrayReverseSameArray { private static readonly Comparer<int> _comparer = Comparer<int>.Create((x, y) => y.CompareTo(x)); [Params(1000, 10_000, 100_000, 1_000_000, 100_000_000)] public int NumberCount; private int[] array; [GlobalSetup] public void GlobalSetup() { array = Enumerable.Range(1, NumberCount).ToArray(); } [Benchmark] public int[] While() { if (array.Length == 0) { return Array.Empty<int>(); } int i = -1; int j = array.Length; while (i++ < j--) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } return array; } [Benchmark] public int[] Array_Reverse() { Array.Reverse(array); return array; } [Benchmark] public int[] Array_Sort() { Array.Sort(array, 0, array.Length, _comparer); return array; } [Benchmark] public int[] For() { for (int i = 0, length = array.Length - 1; i < length / 2; i++) { int item = array[i]; array[i] = array[length - i]; array[length - i] = item; } return array; } [Benchmark] public int[] For_Size() { for (int i = 0, length = array.Length - 1, size = length / 2; i < size; i++) { int item = array[i]; array[i] = array[length - i]; array[length - i] = item; } return array; } [Benchmark] public unsafe int[] For_Unsafe() { fixed (int* arrayItem = array) { for (int i = 0, length = array.Length - 1; i < length / 2; i++) { int item = arrayItem[i]; arrayItem[i] = arrayItem[length - i]; arrayItem[length - i] = item; } } return array; } [Benchmark] public unsafe int[] For_Unsafe_Size() { fixed (int* arrayItem = array) { for (int i = 0, length = array.Length - 1, size = length / 2; i < size; i++) { int item = arrayItem[i]; arrayItem[i] = arrayItem[length - i]; arrayItem[length - i] = item; } } return array; } [Benchmark] public int[] For_Range() { for (int i = 0, length = array.Length - 1; i < length / 2; i++) { (array[^(i + 1)], array[i]) = (array[i], array[^(i + 1)]); } return array; } [Benchmark] public int[] For_Range_Size() { for (int i = 0, length = array.Length - 1, size = length / 2; i < size; i++) { (array[^(i + 1)], array[i]) = (array[i], array[^(i + 1)]); } return array; } [Benchmark] public int[] For_Range_v2() { for (int i = 0, length = array.Length - 1; i < length / 2; i++) { (array[i], array[length - i]) = (array[length - i], array[i]); } return array; } [Benchmark] public int[] For_Range_v2_Size() { for (int i = 0, length = array.Length - 1, size = length / 2; i < size; i++) { (array[i], array[length - i]) = (array[length - i], array[i]); } return array; } [Benchmark] public unsafe int[] For_Range_v2_Unsafe() { fixed (int* arrayItem = array) { for (int i = 0, length = array.Length - 1; i < length / 2; i++) { (arrayItem[i], arrayItem[length - i]) = (arrayItem[length - i], arrayItem[i]); } } return array; } [Benchmark] public unsafe int[] For_Range_v2_Unsafe_Size() { fixed (int* arrayItem = array) { for (int i = 0, length = array.Length - 1, size = length / 2; i < size; i++) { (arrayItem[i], arrayItem[length - i]) = (arrayItem[length - i], arrayItem[i]); } } return array; } }

Примечательно, что на предельных значениях везде будет, пусть и незначительный, но расход памяти.
Пришло время попробовать варианты, которые более практичны. Речь про возврат нового массива без изменения входного.
Hidden text
using BenchmarkDotNet.Attributes; namespace Benchmarks.Benchmarks.Arrays; [MemoryDiagnoser] public class ArrayReverseNewArray { private static readonly Comparer<int> _comparer = Comparer<int>.Create((x, y) => y.CompareTo(x)); [Params(1000, 10_000, 100_000, 1_000_000, 100_000_000)] public int NumberCount; private int[] array; [GlobalSetup] public void GlobalSetup() { array = Enumerable.Range(1, NumberCount).ToArray(); } [Benchmark] public int[] Array_Reverse_ToArray() { int[] items = array.ToArray(); Array.Reverse(items); return items; } [Benchmark] public int[] Array_Reverse_CopyTo() { int[] results = new int[array.Length]; array.CopyTo(results, 0); Array.Reverse(results); return results; } [Benchmark] public int[] For() { int[] items = new int[array.Length]; int j = 0; for (int i = array.Length - 1; i > -1; i--) { items[j++] = array[i]; } return items; } [Benchmark] public int[] Enumerable_ToArray() { return array.Reverse().ToArray(); } [Benchmark] public int[] Stack_ToArray() { Stack<int> stack = new(array); return stack.ToArray(); } [Benchmark] public int[] Stack_CopyTo() { Stack<int> stack = new(array); int[] results = new int[stack.Count]; stack.CopyTo(results, 0); return results; } [Benchmark] public int[] OrderByDescending_ToArray() { return array.OrderByDescending(x => x).ToArray(); } [Benchmark] public int[] QueryOperators_OrderByDescending_ToArray() { return (from x in array orderby x descending select x).ToArray(); } [Benchmark] public int[] List_ToArray() { List<int> items = new(array); items.Reverse(); return items.ToArray(); } [Benchmark] public int[] List_CopyTo() { List<int> items = new(array); items.Reverse(); int[] results = new int[items.Count]; items.CopyTo(results, 0); return results; } [Benchmark] public int[] List_Sort() { List<int> items = new(array); items.Sort(_comparer); return items.ToArray(); } [Benchmark] public int[] List_Sort_CopyTo() { List<int> items = new(array); items.Sort(_comparer); int[] results = new int[items.Count]; items.CopyTo(results, 0); return results; } [Benchmark] public int[] Span_ToArray() { Span<int> items = new(array); items.Reverse(); return items.ToArray(); } [Benchmark] public int[] Span_Sort_ToArray() { Span<int> items = new(array); items.Sort(_comparer); return items.ToArray(); } [Benchmark] public int[] SortedSet_ToArray() { SortedSet<int> items = new(array, _comparer); return items.ToArray(); } [Benchmark] public int[] SortedSet_CopyTo() { SortedSet<int> items = new(array, _comparer); int[] results = new int[items.Count]; items.CopyTo(results, 0); return results; } }
Итог 80-ти тестов следующий:

Для общего итога, хоть выводы уже очевидны, предлагаю протестировать несколько лучших вариантов из разных подходов:
Hidden text
using BenchmarkDotNet.Attributes; namespace Benchmarks.Benchmarks.Arrays; [MemoryDiagnoser] public class ArrayReverseFaster { [Params(1000, 10_000, 100_000, 1_000_000, 100_000_000, 1_000_000_000)] public int NumberCount; private int[] array; [GlobalSetup] public void GlobalSetup() { array = Enumerable.Range(1, NumberCount).ToArray(); } [Benchmark] public int[] Array_Reverse_SameArray() { Array.Reverse(array); return array; } [Benchmark] public unsafe int[] For_Unsafe_SameArray() { fixed (int* arrayItem = array) { for (int i = 0, length = array.Length - 1; i < length / 2; i++) { int item = arrayItem[i]; arrayItem[i] = arrayItem[length - i]; arrayItem[length - i] = item; } } return array; } [Benchmark] public unsafe int[] For_Unsafe_SameArray_Length() { int length = array.Length - 1; fixed (int* arrayItem = array) { for (int i = 0; i < length / 2; i++) { int item = arrayItem[i]; arrayItem[i] = arrayItem[length - i]; arrayItem[length - i] = item; } } return array; } [Benchmark] public unsafe int[] For_Unsafe_SameArray_Size() { int length = array.Length - 1; int size = length / 2; fixed (int* arrayItem = array) { for (int i = 0; i < size; i++) { int item = arrayItem[i]; arrayItem[i] = arrayItem[length - i]; arrayItem[length - i] = item; } } return array; } [Benchmark] public int[] Array_Reverse_ToArray() { int[] items = array.ToArray(); Array.Reverse(items); return items; } [Benchmark] public int[] Array_Reverse_CopyTo() { int[] results = new int[array.Length]; array.CopyTo(results, 0); Array.Reverse(results); return results; } [Benchmark] public int[] Enumerable_ToArray() { return array.Reverse().ToArray(); } [Benchmark] public int[] Span_ToArray() { Span<int> items = new(array); items.Reverse(); return items.ToArray(); } }
Итог говорит сам за себя:

И на закуску немного кода с https://referencesource.microsoft.com, который иногда радует и удивляет одновременно.
Stack.ToArray() (namespace System.Collections):
// Copies the Stack to an array, in the same order Pop would return the items. public virtual Object[] ToArray() { Contract.Ensures(Contract.Result<Object[]>() != null); Object[] objArray = new Object[_size]; int i = 0; while (i < _size) { objArray[i] = _array[_size - i - 1]; i++; } return objArray; }
List.ToArray() (namespace System.Collections.Generic):
// ToArray returns a new Object array containing the contents of the List. // This requires copying the List, which is an O(n) operation. public T[] ToArray() { Contract.Ensures(Contract.Result<T[]>() != null); Contract.Ensures(Contract.Result<T[]>().Length == Count); T[] array = new T[_size]; Array.Copy(_items, 0, array, 0, _size); return array; }
Итого победители: Array и Span Reverse.
Всем удачи и до новых встреч!
P.S.: почти все тесты крайне длительные (20 - 90+ минут на 1 тест) и если повторять, лучше уменьшить выборку.
