Почему многомерные массивы в .NET медленнее, чем обычные массивы?

Редактировать: Прошу прощения у всех. Я использовал термин «зубчатый массив», когда на самом деле имел в виду «многомерный массив» (как можно увидеть в моем примере ниже). Прошу прощения за использование неправильного имени. Я действительно обнаружил, что зубчатые массивы быстрее, чем многомерные! Я добавил свои измерения для массивов с зазубринами.

Сегодня я пытался использовать многомерный массив зазубренный, когда заметил, что его производительность не такая, как я ожидал. Использование одномерного массива и ручное вычисление индексов было намного быстрее (почти в два раза), чем использование 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);
}

Некоторое время назад я нашел сообщение в блоге, в котором нужно было оптимизировать инверсию матрицы (или что-то в этом роде). Результаты показали, что зубчатые массивы намного быстрее, чем многомерные массивы. Я не могу вспомнить, какой это был блог.

dalle 22.01.2009 15:17

Это встроено в режим выпуска с оптимизацией?

yfeldblum 22.01.2009 16:07

Да, @Justice, это так, и запускается из командной строки с приоритетом процесса, установленным в реальном времени, и приоритетом потока, установленным на самый высокий.

Hosam Aly 22.01.2009 16:22

Вы уверены, что этот код действительно оптимизируется JIT-компилятором?

John Leidegren 28.02.2009 17:22

@John, я запускаю его в режиме Release из консоли, так что его надо оптимизировать.

Hosam Aly 28.02.2009 17:41

@Henk, я исправил; Спасибо.

Hosam Aly 28.02.2009 17:50

не уверен, изменился ли он с течением времени и версия .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

Falco Alexander 22.10.2015 12:46

К вашему сведению, эта проблема теперь регистрируется в проекте / репозитории github coreclr: github.com/dotnet/coreclr/issues/4059#issuecomment-208491798

redcalx 11.04.2016 22:30
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
55
9
25 774
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

Потому что многомерный массив - это просто синтаксический сахар, поскольку на самом деле это просто плоский массив с некоторой магией вычисления индекса. С другой стороны, зубчатый массив подобен массиву массивов. В случае двумерного массива для доступа к элементу требуется чтение памяти только один раз, в то время как для двухуровневого массива с зазубринами вам нужно дважды прочитать память.

Обновлено: Очевидно, в оригинальном плакате «зубчатые массивы» смешивались с «многомерными массивами», поэтому мои рассуждения не совсем верны. Чтобы узнать настоящую причину, проверьте ответ Джона Скита о тяжелой артиллерии выше.

Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!

Hosam Aly 22.01.2009 15:07

@DrJokepu: Использование многомерного массива должно быть быстрее, чем зубчатого массива, но на самом деле все наоборот.

dalle 22.01.2009 15:16

Это «волшебство расчета индекса» и составляет суть моего вопроса. Разве это не должно быть (по крайней мере) таким же быстрым, как мой первый метод?

Hosam Aly 22.01.2009 15:16

Я думаю, что здесь есть чем заняться, потому что зубчатые массивы на самом деле являются массивами массивов, следовательно, есть два уровня косвенного обращения к фактическим данным.

Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!

Hosam Aly 22.01.2009 15:12

@Henk: И что еще более удивительно, так это то, что (IMO) проверка границ для многомерных массивов в цикле for с фиксированным числом итераций может быть оптимизирована из-за регулярной (= прямоугольной) природы массива! Я предполагаю, что эта оптимизация не проводится по какой-то непонятной причине.

Autodidact 28.02.2009 19:33

Проверка границ. Ваша переменная «j» может превышать l2 при условии, что «i» меньше l1. Во втором примере это было бы недопустимо.

Я не голосовал против, но не применяется ли проверка границ в обоих случаях?

Dirk Vollmar 22.01.2009 14:59

Проверка привязки правильная (или, по крайней мере, соответствующий аспект), но указанная причина неверна (хотя я не отрицал ее), просто существует больше проверок границ с зазубренным массивом. GetLength (int) проверяет параметр (> 0, <размеры массива) перед возвратом размера соответствующего массива.

JeeBee 22.01.2009 15:02

Я хотел сказать, что в опубликованный код, где многомерный массив моделировался с использованием одномерного, использовался недопустимый код индекса, при условии, что арифметика достигла окончательного значения индекса в пределах

Damien_The_Unbeliever 22.01.2009 18:42

Неровные массивы - это массивы ссылок на классы (другие массивы) вплоть до листового массива, который может быть массивом примитивного типа. Следовательно, память, выделенная для каждого из других массивов, может быть повсюду.

В то время как многомерный массив имеет свою память, распределенную в виде одного непрерывного блока.

Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!

Hosam Aly 22.01.2009 15:10

Интересно, что если вы размещаете неровный массив последовательно, он также будет последовательно размещаться в памяти, даже если это куча ссылок (управляемая куча .NET не ищет свободного места, в отличие от malloc и т. д.). Это означает, что если вы не делаете что-то очень неправильно, вы по-прежнему хорошо используете кеш.

Luaan 03.12.2013 17:28

Проверка границ массива?

Одномерный массив имеет член длины, к которому вы обращаетесь напрямую - при компиляции это просто чтение из памяти.

Для многомерного массива требуется вызов метода GetLength (int Dimension), который обрабатывает аргумент, чтобы получить соответствующую длину для этого измерения. Это не компилируется до чтения из памяти, поэтому вы получаете вызов метода и т. д.

Кроме того, GetLength (int Dimension) будет проверять границы параметра.

Хм, хорошо подумайте, вы как-нибудь это проверили (отладили код, использовали отражатель и т. д.)?

Autodidact 22.01.2009 15:01

Я знаю в Java, что вызов метода для получателя или установщика фактически оптимизирует вызов метода и напрямую обращается к значению. Я не понимаю, почему .NET будет другим. Также будут проверки границ аргумента GetLength (int index).

JeeBee 22.01.2009 15:04

Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!

Hosam Aly 22.01.2009 15:08

К счастью, я на самом деле говорил о многомерных массивах!

JeeBee 22.01.2009 15:09

Я здесь со всеми

У меня была программа с трехмерным массивом, позвольте мне сказать вам, что когда я переместил массив в двухмерный, я увидел огромный прирост, а затем я перешел к одномерному массиву.

В конце концов, я думаю, что я увидел прирост производительности более чем на 500% во время выполнения.

Единственным недостатком была сложность определения того, где что находится в одномерном массиве, а не в трех.

Ответ принят как подходящий

Одномерные массивы с нижней границей 0 являются типом, отличным от многомерных или ненулевых массивов нижней границы в IL (vector против array IIRC). С vector проще работать - чтобы добраться до элемента x, достаточно выполнить pointer + size * x. Для array вы должны выполнить pointer + size * (x-lower bound) для одномерного массива и еще больше арифметических операций для каждого добавляемого вами измерения.

В основном среда CLR оптимизирована для гораздо более распространенных случаев.

Я прошу прощения. На самом деле я использовал многомерный массив, но я использовал неправильный термин. Простите!

Hosam Aly 22.01.2009 15:09

Честно говоря, я все равно считал многомерным :) Неровный массив обычно представляет собой «вектор векторов» в терминах CLR, поэтому я не удивлен, что он быстрее, чем многомерный массив.

Jon Skeet 22.01.2009 15:28

Меня это сбивает с толку, многомерный массив должен быть быстрее, чем зубчатый. Во всяком случае, это ошибка CLR.

John Leidegren 28.02.2009 17:23

Хороший компилятор должен иметь возможность перемещать все проверки привязки перед циклом и генерировать в основном тот же код, что и d1 для d2. Это просто доказывает, что компилятор MS не очень хорош (для массивов).

ILoveFortran 28.02.2009 18:11

@ILoveFortran: JIT-компилятор (где проверки фактически выдаются или пропускаются) сильно оптимизирован для скорости выполнения - цель состоит в том, чтобы JIT-компиляция была быстрее, чем типичная ошибка страницы. Даже тогда компилятор x64 JIT выполняет именно ту оптимизацию, о которой вы говорите, а новый компилятор (еще не производственная версия) RyuJIT получает еще больше оптимизаций. Кроме того, факт в том, что даже компилятор x86 удаляет Связанная проверка вообще, если вы используете for (i = 0; i < ar.Length; i++), потому что это гарантирует проверку границ в самом цикле for.

Luaan 03.12.2013 17:24

Одномерные массивы с нижней границей 0 также частично реализуют IList <T> и, следовательно, ICollection <T> и IEnumerable <T>, что несколько примечательно для меня.

Kyle Baran 25.06.2014 16:12

Я думаю, что многомерный процесс медленнее, среда выполнения должна проверять две или более границы (трехмерные и выше).

Интересно, что я запустил следующий код сверху используя 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 Это говорит о том, что различиями можно пренебречь.

d3dave 22.11.2013 19:59

Да, здесь так же. Я подозреваю, что OP выбрал конфигурацию «Release», а затем запустил программу с помощью F5, а не из командной строки. Или, возможно, это потому, что мы используем 64-разрядную версию, или, может быть, потому, что JIT теперь оптимизирован для этого случая ...

Erti-Chris Eelmaa 24.10.2014 19:38

Собственно, посмотрите, что произойдет, если вы измените числа немного выше, например, если это будет ~ 30 секунд. Получил довольно значимые результаты. Например, многомерный массив был в 2 раза медленнее, чем зубчатый. (зазубренный массив запускается 30 секунд, многомерный запуск 1:24 секунды)

Erti-Chris Eelmaa 24.10.2014 22:55

Что быстрее всего зависит от размеров ваших массивов.

Изображение для удобства чтения:

Результат в консоли:

// * 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]».

Sunsetquest 13.09.2020 00:50

Другие вопросы по теме