Редактировать: Прошу прощения у всех. Я использовал термин «зубчатый массив», когда на самом деле имел в виду «многомерный массив» (как можно увидеть в моем примере ниже). Прошу прощения за использование неправильного имени. Я действительно обнаружил, что зубчатые массивы быстрее, чем многомерные! Я добавил свои измерения для массивов с зазубринами.
Сегодня я пытался использовать многомерный массив зазубренный, когда заметил, что его производительность не такая, как я ожидал. Использование одномерного массива и ручное вычисление индексов было намного быстрее (почти в два раза), чем использование 2D-массива. Я написал тест с использованием массивов 1024*1024 (инициализированных случайными значениями) на 1000 итераций и получил на своей машине следующие результаты:
sum(double[], int): 2738 ms (100%)
sum(double[,]): 5019 ms (183%)
sum(double[][]): 2540 ms ( 93%)
Это мой тестовый код:
public static double sum(double[] d, int l1) {
// assuming the array is rectangular
double sum = 0;
int l2 = d.Length / l1;
for (int i = 0; i < l1; ++i)
for (int j = 0; j < l2; ++j)
sum += d[i * l2 + j];
return sum;
}
public static double sum(double[,] d) {
double sum = 0;
int l1 = d.GetLength(0);
int l2 = d.GetLength(1);
for (int i = 0; i < l1; ++i)
for (int j = 0; j < l2; ++j)
sum += d[i, j];
return sum;
}
public static double sum(double[][] d) {
double sum = 0;
for (int i = 0; i < d.Length; ++i)
for (int j = 0; j < d[i].Length; ++j)
sum += d[i][j];
return sum;
}
public static void Main() {
Random random = new Random();
const int l1 = 1024, l2 = 1024;
double[ ] d1 = new double[l1 * l2];
double[,] d2 = new double[l1 , l2];
double[][] d3 = new double[l1][];
for (int i = 0; i < l1; ++i) {
d3[i] = new double[l2];
for (int j = 0; j < l2; ++j)
d3[i][j] = d2[i, j] = d1[i * l2 + j] = random.NextDouble();
}
//
const int iterations = 1000;
TestTime(sum, d1, l1, iterations);
TestTime(sum, d2, iterations);
TestTime(sum, d3, iterations);
}
Дальнейшие исследования показали, что ИЖ для второго метода на 23% больше, чем для первого метода. (Размер кода 68 против 52). В основном это связано с вызовами System.Array::GetLength(int). Компилятор также отправляет вызовы Array::Get для многомерного массива зазубренный, тогда как он просто вызывает ldelem для простого массива.
Поэтому мне интересно, почему доступ через многомерные массивы медленнее, чем через обычные массивы? Я бы предположил, что компилятор (или JIT) сделает что-то похожее на то, что я сделал в моем первом методе, но на самом деле это было не так.
Не могли бы вы помочь мне понять, почему это происходит именно так?
Обновлять: Следуя предложению Хенка Холтермана, вот реализация TestTime:
public static void TestTime<T, TR>(Func<T, TR> action, T obj,
int iterations)
{
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < iterations; ++i)
action(obj);
Console.WriteLine(action.Method.Name + " took " + stopwatch.Elapsed);
}
public static void TestTime<T1, T2, TR>(Func<T1, T2, TR> action, T1 obj1,
T2 obj2, int iterations)
{
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < iterations; ++i)
action(obj1, obj2);
Console.WriteLine(action.Method.Name + " took " + stopwatch.Elapsed);
}
Это был блог B # .NET: community.bartdesmet.net/blogs/bart/archive/2007/02/27/…community.bartdesmet.net/blogs/bart/archive/2007/03/13/…
Это встроено в режим выпуска с оптимизацией?
Да, @Justice, это так, и запускается из командной строки с приоритетом процесса, установленным в реальном времени, и приоритетом потока, установленным на самый высокий.
Вы уверены, что этот код действительно оптимизируется JIT-компилятором?
@John, я запускаю его в режиме Release из консоли, так что его надо оптимизировать.
@Henk, я исправил; Спасибо.
не уверен, изменился ли он с течением времени и версия .NET, но я запустил код в настоящее время, и 2D-массив работает быстрее всего в зависимости от скомпилированного режима -Debug compiled: -[] sum took 00:00:04.9336682 -[,] sum took 00:00:06.6317560 -[][] sum took 00:00:07.3573180, и он изменяется при компиляции выпуска: sum took 00:00:01.4856096 sum took 00:00:02.2553230 sum took 00:00:01.4376627
К вашему сведению, эта проблема теперь регистрируется в проекте / репозитории github coreclr: github.com/dotnet/coreclr/issues/4059#issuecomment-208491798





Потому что многомерный массив - это просто синтаксический сахар, поскольку на самом деле это просто плоский массив с некоторой магией вычисления индекса. С другой стороны, зубчатый массив подобен массиву массивов. В случае двумерного массива для доступа к элементу требуется чтение памяти только один раз, в то время как для двухуровневого массива с зазубринами вам нужно дважды прочитать память.
Обновлено: Очевидно, в оригинальном плакате «зубчатые массивы» смешивались с «многомерными массивами», поэтому мои рассуждения не совсем верны. Чтобы узнать настоящую причину, проверьте ответ Джона Скита о тяжелой артиллерии выше.
Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!
@DrJokepu: Использование многомерного массива должно быть быстрее, чем зубчатого массива, но на самом деле все наоборот.
Это «волшебство расчета индекса» и составляет суть моего вопроса. Разве это не должно быть (по крайней мере) таким же быстрым, как мой первый метод?
Я думаю, что здесь есть чем заняться, потому что зубчатые массивы на самом деле являются массивами массивов, следовательно, есть два уровня косвенного обращения к фактическим данным.
Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!
@Henk: И что еще более удивительно, так это то, что (IMO) проверка границ для многомерных массивов в цикле for с фиксированным числом итераций может быть оптимизирована из-за регулярной (= прямоугольной) природы массива! Я предполагаю, что эта оптимизация не проводится по какой-то непонятной причине.
Проверка границ. Ваша переменная «j» может превышать l2 при условии, что «i» меньше l1. Во втором примере это было бы недопустимо.
Я не голосовал против, но не применяется ли проверка границ в обоих случаях?
Проверка привязки правильная (или, по крайней мере, соответствующий аспект), но указанная причина неверна (хотя я не отрицал ее), просто существует больше проверок границ с зазубренным массивом. GetLength (int) проверяет параметр (> 0, <размеры массива) перед возвратом размера соответствующего массива.
Я хотел сказать, что в опубликованный код, где многомерный массив моделировался с использованием одномерного, использовался недопустимый код индекса, при условии, что арифметика достигла окончательного значения индекса в пределах
Неровные массивы - это массивы ссылок на классы (другие массивы) вплоть до листового массива, который может быть массивом примитивного типа. Следовательно, память, выделенная для каждого из других массивов, может быть повсюду.
В то время как многомерный массив имеет свою память, распределенную в виде одного непрерывного блока.
Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!
Интересно, что если вы размещаете неровный массив последовательно, он также будет последовательно размещаться в памяти, даже если это куча ссылок (управляемая куча .NET не ищет свободного места, в отличие от malloc и т. д.). Это означает, что если вы не делаете что-то очень неправильно, вы по-прежнему хорошо используете кеш.
Проверка границ массива?
Одномерный массив имеет член длины, к которому вы обращаетесь напрямую - при компиляции это просто чтение из памяти.
Для многомерного массива требуется вызов метода GetLength (int Dimension), который обрабатывает аргумент, чтобы получить соответствующую длину для этого измерения. Это не компилируется до чтения из памяти, поэтому вы получаете вызов метода и т. д.
Кроме того, GetLength (int Dimension) будет проверять границы параметра.
Хм, хорошо подумайте, вы как-нибудь это проверили (отладили код, использовали отражатель и т. д.)?
Я знаю в Java, что вызов метода для получателя или установщика фактически оптимизирует вызов метода и напрямую обращается к значению. Я не понимаю, почему .NET будет другим. Также будут проверки границ аргумента GetLength (int index).
Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!
К счастью, я на самом деле говорил о многомерных массивах!
Я здесь со всеми
У меня была программа с трехмерным массивом, позвольте мне сказать вам, что когда я переместил массив в двухмерный, я увидел огромный прирост, а затем я перешел к одномерному массиву.
В конце концов, я думаю, что я увидел прирост производительности более чем на 500% во время выполнения.
Единственным недостатком была сложность определения того, где что находится в одномерном массиве, а не в трех.
Одномерные массивы с нижней границей 0 являются типом, отличным от многомерных или ненулевых массивов нижней границы в IL (vector против array IIRC). С vector проще работать - чтобы добраться до элемента x, достаточно выполнить pointer + size * x. Для array вы должны выполнить pointer + size * (x-lower bound) для одномерного массива и еще больше арифметических операций для каждого добавляемого вами измерения.
В основном среда CLR оптимизирована для гораздо более распространенных случаев.
Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!
Честно говоря, я все равно считал многомерным :) Неровный массив обычно представляет собой «вектор векторов» в терминах CLR, поэтому я не удивлен, что он быстрее, чем многомерный массив.
Меня это сбивает с толку, многомерный массив должен быть быстрее, чем зубчатый. Во всяком случае, это ошибка CLR.
Хороший компилятор должен иметь возможность перемещать все проверки привязки перед циклом и генерировать в основном тот же код, что и d1 для d2. Это просто доказывает, что компилятор MS не очень хорош (для массивов).
@ILoveFortran: JIT-компилятор (где проверки фактически выдаются или пропускаются) сильно оптимизирован для скорости выполнения - цель состоит в том, чтобы JIT-компиляция была быстрее, чем типичная ошибка страницы. Даже тогда компилятор x64 JIT выполняет именно ту оптимизацию, о которой вы говорите, а новый компилятор (еще не производственная версия) RyuJIT получает еще больше оптимизаций. Кроме того, факт в том, что даже компилятор x86 удаляет Связанная проверка вообще, если вы используете for (i = 0; i < ar.Length; i++), потому что это гарантирует проверку границ в самом цикле for.
Одномерные массивы с нижней границей 0 также частично реализуют IList <T> и, следовательно, ICollection <T> и IEnumerable <T>, что несколько примечательно для меня.
Я думаю, что многомерный процесс медленнее, среда выполнения должна проверять две или более границы (трехмерные и выше).
Интересно, что я запустил следующий код сверху используя VS2008 NET3.5SP1 Win32 на коробке Vista, а в выпуске / оптимизации разница была едва заметной, в то время как debug / noopt многотомные массивы были намного медленнее. (Я провел три теста дважды, чтобы уменьшить влияние JIT во втором наборе.)
Here are my numbers:
sum took 00:00:04.3356535
sum took 00:00:04.1957663
sum took 00:00:04.5523050
sum took 00:00:04.0183060
sum took 00:00:04.1785843
sum took 00:00:04.4933085
Посмотрите на второй набор из трех чисел. Мне не хватает разницы, чтобы кодировать все в одномерных массивах.
Хотя я их не публиковал, в Debug / unoptimized multidimension vs. одиночный / зубчатый действительно имеет огромное значение.
Полная программа:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
namespace single_dimension_vs_multidimension
{
class Program
{
public static double sum(double[] d, int l1) { // assuming the array is rectangular
double sum = 0;
int l2 = d.Length / l1;
for (int i = 0; i < l1; ++i)
for (int j = 0; j < l2; ++j)
sum += d[i * l2 + j];
return sum;
}
public static double sum(double[,] d)
{
double sum = 0;
int l1 = d.GetLength(0);
int l2 = d.GetLength(1);
for (int i = 0; i < l1; ++i)
for (int j = 0; j < l2; ++j)
sum += d[i, j];
return sum;
}
public static double sum(double[][] d)
{
double sum = 0;
for (int i = 0; i < d.Length; ++i)
for (int j = 0; j < d[i].Length; ++j)
sum += d[i][j];
return sum;
}
public static void TestTime<T, TR>(Func<T, TR> action, T obj, int iterations)
{
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < iterations; ++i)
action(obj);
Console.WriteLine(action.Method.Name + " took " + stopwatch.Elapsed);
}
public static void TestTime<T1, T2, TR>(Func<T1, T2, TR> action, T1 obj1, T2 obj2, int iterations)
{
Stopwatch stopwatch = Stopwatch.StartNew();
for (int i = 0; i < iterations; ++i)
action(obj1, obj2);
Console.WriteLine(action.Method.Name + " took " + stopwatch.Elapsed);
}
public static void Main() {
Random random = new Random();
const int l1 = 1024, l2 = 1024;
double[ ] d1 = new double[l1 * l2];
double[,] d2 = new double[l1 , l2];
double[][] d3 = new double[l1][];
for (int i = 0; i < l1; ++i)
{
d3[i] = new double[l2];
for (int j = 0; j < l2; ++j)
d3[i][j] = d2[i, j] = d1[i * l2 + j] = random.NextDouble();
}
const int iterations = 1000;
TestTime<double[], int, double>(sum, d1, l1, iterations);
TestTime<double[,], double>(sum, d2, iterations);
TestTime<double[][], double>(sum, d3, iterations);
TestTime<double[], int, double>(sum, d1, l1, iterations);
TestTime<double[,], double>(sum, d2, iterations);
TestTime<double[][], double>(sum, d3, iterations);
}
}
}
Я расширил вашу тестовую программу до трехмерных массивов (256 * 256 * 256) и получил следующие результаты (без отладчика, сборка выпуска, .Net 4.5, Intel Core 2 Duo @ 2,20 ГГц, 64-разрядная Win7): pastebin.com/SUtMSXkk Вот программа на случай, если вам интересно: pastebin.com/Uzh9jrAM Это говорит о том, что различиями можно пренебречь.
Да, здесь так же. Я подозреваю, что OP выбрал конфигурацию «Release», а затем запустил программу с помощью F5, а не из командной строки. Или, возможно, это потому, что мы используем 64-разрядную версию, или, может быть, потому, что JIT теперь оптимизирован для этого случая ...
Собственно, посмотрите, что произойдет, если вы измените числа немного выше, например, если это будет ~ 30 секунд. Получил довольно значимые результаты. Например, многомерный массив был в 2 раза медленнее, чем зубчатый. (зазубренный массив запускается 30 секунд, многомерный запуск 1:24 секунды)
Что быстрее всего зависит от размеров ваших массивов.
// * Summary *
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.997 (1909/November2018Update/19H2)
Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.302
[Host] : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT
.NET Core 3.1 : .NET Core 3.1.6 (CoreCLR 4.700.20.26901, CoreFX 4.700.20.31603), X64 RyuJIT
Job=.NET Core 3.1 Runtime=.NET Core 3.1
| Method | D | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------- |----- |----------------:|--------------:|--------------:|-----------:|----------:|----------:|-----------:|
| 'double[D1][D2]' | 10 | 376.2 ns | 7.57 ns | 12.00 ns | 0.3643 | - | - | 1144 B |
| 'double[D1, D2]' | 10 | 325.5 ns | 3.71 ns | 3.47 ns | 0.2675 | - | - | 840 B |
| 'double[D1][D2]' | 50 | 4,821.4 ns | 44.71 ns | 37.34 ns | 6.8893 | - | - | 21624 B |
| 'double[D1, D2]' | 50 | 5,834.1 ns | 64.35 ns | 60.20 ns | 6.3629 | - | - | 20040 B |
| 'double[D1][D2]' | 100 | 19,124.4 ns | 230.39 ns | 454.77 ns | 26.2756 | 0.7019 | - | 83224 B |
| 'double[D1, D2]' | 100 | 23,561.4 ns | 299.18 ns | 279.85 ns | 24.9939 | - | - | 80040 B |
| 'double[D1][D2]' | 500 | 1,248,458.7 ns | 11,241.19 ns | 10,515.01 ns | 322.2656 | 160.1563 | - | 2016025 B |
| 'double[D1, D2]' | 500 | 966,940.8 ns | 5,694.46 ns | 5,326.60 ns | 303.7109 | 303.7109 | 303.7109 | 2000034 B |
| 'double[D1][D2]' | 1000 | 8,987,202.8 ns | 97,133.16 ns | 90,858.41 ns | 1421.8750 | 578.1250 | 265.6250 | 8032582 B |
| 'double[D1, D2]' | 1000 | 3,628,421.3 ns | 72,240.02 ns | 177,206.01 ns | 179.6875 | 179.6875 | 179.6875 | 8000036 B |
| 'double[D1][D2]' | 1500 | 26,496,994.4 ns | 380,625.25 ns | 356,037.09 ns | 3406.2500 | 1500.0000 | 531.2500 | 18048064 B |
| 'double[D1, D2]' | 1500 | 12,417,733.7 ns | 243,802.76 ns | 260,866.22 ns | 156.2500 | 156.2500 | 156.2500 | 18000038 B |
| 'double[D1][D2]' | 3000 | 86,943,097.4 ns | 485,339.32 ns | 405,280.31 ns | 12833.3333 | 7000.0000 | 1333.3333 | 72096325 B |
| 'double[D1, D2]' | 3000 | 57,969,405.9 ns | 393,463.61 ns | 368,046.11 ns | 222.2222 | 222.2222 | 222.2222 | 72000100 B |
// * Hints *
Outliers
MultidimensionalArrayBenchmark.'double[D1][D2]': .NET Core 3.1 -> 1 outlier was removed (449.71 ns)
MultidimensionalArrayBenchmark.'double[D1][D2]': .NET Core 3.1 -> 2 outliers were removed, 3 outliers were detected (4.75 us, 5.10 us, 5.28 us)
MultidimensionalArrayBenchmark.'double[D1][D2]': .NET Core 3.1 -> 13 outliers were removed (21.27 us..30.62 us)
MultidimensionalArrayBenchmark.'double[D1, D2]': .NET Core 3.1 -> 1 outlier was removed (4.19 ms)
MultidimensionalArrayBenchmark.'double[D1, D2]': .NET Core 3.1 -> 3 outliers were removed, 4 outliers were detected (11.41 ms, 12.94 ms..13.61 ms)
MultidimensionalArrayBenchmark.'double[D1][D2]': .NET Core 3.1 -> 2 outliers were removed (88.68 ms, 89.27 ms)
// * Legends *
D : Value of the 'D' parameter
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Gen 0 : GC Generation 0 collects per 1000 operations
Gen 1 : GC Generation 1 collects per 1000 operations
Gen 2 : GC Generation 2 collects per 1000 operations
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
1 ns : 1 Nanosecond (0.000000001 sec)
[SimpleJob(BenchmarkDotNet.Jobs.RuntimeMoniker.NetCoreApp31)]
[MemoryDiagnoser]
public class MultidimensionalArrayBenchmark {
[Params(10, 50, 100, 500, 1000, 1500, 3000)]
public int D { get; set; }
[Benchmark(Description = "double[D1][D2]")]
public double[][] JaggedArray() {
var array = new double[D][];
for (int i = 0; i < array.Length; i++) {
var subArray = new double[D];
array[i] = subArray;
for (int j = 0; j < subArray.Length; j++) {
subArray[j] = j + i * 10;
}
}
return array;
}
[Benchmark(Description = "double[D1, D2]")]
public double[,] MultidimensionalArray() {
var array = new double[D, D];
for (int i = 0; i < D; i++) {
for (int j = 0; j < D; j++) {
array[i, j] = j + i * 10;
}
}
return array;
}
}
Я провел свои собственные быстрые тесты (другой код), и Multi-Dim в целом показался быстрее. Было бы логично, что multi-dim работает быстрее, и в 2002 году, когда узнал, что Jagged был быстрее, когда узнал .net. Гораздо более эффективная математика может быть выполнена на многомерном изображении, хотя для этого потребуется большой кусок непрерывной памяти. Что кажется еще быстрее в ядре .net, так это самодельная версия с использованием единственного массива «int outValue = MyArray [x * DIM1 * DIM2 + y * DIM2 + z]».
Некоторое время назад я нашел сообщение в блоге, в котором нужно было оптимизировать инверсию матрицы (или что-то в этом роде). Результаты показали, что зубчатые массивы намного быстрее, чем многомерные массивы. Я не могу вспомнить, какой это был блог.