Какой самый быстрый способ инициализации многомерного массива в значения не по умолчанию в .NET? - PullRequest
5 голосов
/ 04 мая 2010

Как мне инициализировать многомерный массив примитивного типа как можно быстрее?

Я застрял с использованием многомерных массивов. Моя проблема в производительности. Следующая процедура инициализирует массив 100x100 в прибл. 500 тиков. Удаление инициализации int.MaxValue приводит к ок. 180 тиков только за циклы. Приблизительно 100 тиков для создания массива без циклов и без инициализации int.MaxValue.

  • Рутины, похожие на это, называются от нескольких сотен тысяч до нескольких миллионов раз во время «бега».
  • Размер массива не изменится во время выполнения, и массивы создаются по одному, используются, затем отбрасываются и создается новый массив.
  • «Бег», который может длиться от одной минуты (с использованием массивов 10x10) до сорока пяти минут (100x100).
  • Приложение создает массивы int, bool и struct.
  • Может быть несколько «запусков», выполняемых одновременно, но не потому, что производительность ужасно падает.
  • Я использую 100x100 в качестве базовой линии.

Я открыт для предложений о том, как оптимизировать эту нестандартную инициализацию массива. У меня была идея использовать примитивный тип меньшего размера, если он доступен. Например, использование байта вместо int сохраняет 100 тиков. Я был бы счастлив с этим, но я надеюсь, что мне не нужно менять примитивный тип данных.

    public int[,] CreateArray(Size size) {
        int[,] array = new int[size.Width, size.Height];
        for (int x = 0; x < size.Width; x++) {
            for (int y = 0; y < size.Height; y++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

До 450 тиков со следующим:

    public int[,] CreateArray1(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        for (int x = 0; x < iX; x++) {
            for (int y = 0; y < iY; y++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

CreateArray5; Принятая реализация: ограничение: невозможно изменить размер, можно изменить

Уменьшено примерно до 165 тиков после одноразовой инициализации 2800 тиков. (См. Мой ответ ниже.) Если я смогу заставить stackalloc работать с многомерными массивами, я смогу получить ту же производительность без необходимости инициализировать массив private static.

    private static bool _arrayInitialized5;
    private static int[,] _array5;

    public static int[,] CreateArray5(Size size) {
        if (!_arrayInitialized5) {
            int iX = size.Width;
            int iY = size.Height;
            _array5 = new int[iX, iY];
            for (int x = 0; x < iX; x++) {
                for (int y = 0; y < iY; y++) {
                    _array5[x, y] = int.MaxValue;
                }
            }
            _arrayInitialized5 = true;
        }
        return (int[,])_array5.Clone();
    }

CreateArray8; Принятая реализация; Ограничение: Требуется полное доверие

До примерно 165 тиков без использования "техники клонирования" выше. (См. Мой ответ ниже.) Я уверен, что смогу снизить тики, если смогу просто вычислить возврат CreateArray9.

    public unsafe static int[,] CreateArray8(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        fixed (int* pfixed = array) {
            int count = array.Length;
            for (int* p = pfixed; count-- > 0; p++)
                *p = int.MaxValue;
        }
        return array;
    }

Примечания

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

Массивы, выделенные в куче больших объектов (LOH), не являются частью этого обсуждения. Отмеченные улучшения производительности отмечаются только для массивов, выделенных в куче.

Stackalloc

Моя идея использовать stackalloc для устранения инициализации массива значениями по умолчанию не сработала, потому что выделенная память стека должна быть скопирована из метода. Это значит, что мне нужно создать еще один массив для хранения результатов. Этот массив будет инициализирован, уничтожая всю цель использования stackalloc.

CreateArray8; небезопасный / фиксированный метод

CLR выполнит код unsafe, только если он находится в полностью доверенной сборке.

CreateArray5; метод клонирования

Требуются переменные для определения инициализации массива и для хранения инициализированного массива. Производительность такая же, как у небезопасного / фиксированного метода после инициализации. См. Ответ Дана Тао для возможного решения.

300% увеличение производительности?

Я сосу в процентах, но я рассчитал 300% (от 500 до 165 тиков).


Окончательная реализация для приложения

Для этого приложения я остановился на методе «клон». Ниже приведена «бережливая» универсальная реализация, используемая в приложении с примерами производительности.

Инициализация:

  • Grid<int>; родовой класс клонов инициализируется: 4348, 4336, 4339, 4654
  • Grid<bool>; родовой класс клонов: 2692, 2684, 3916, 2680
  • Grid<Color>; родовой класс клонов: 3747, 4630, 2702, 2708

Использование:

  • Grid<int>; общий класс клонов: 185, 159, 152, 290
  • Grid<bool>;типовой класс клонов: 39, 36, 44, 46
  • Grid<Color>;общий класс клонов: 2229, 2431, 2460, 2496

    public class Grid<T> {
        private T[,] _array;
        private T _value;
        private bool _initialized;
        private int _x;
        private int _y;
        public Grid(Size size, T value, bool initialize) {
            _x = size.Width;
            _y = size.Height;
            _value = value;
            if (initialize) {
                InitializeArray();
            }
        }
        private void InitializeArray() {
            int iX = _x;
            int iY = _y;
            _array = new T[iX, iY];
            for (int y = 0; y < iY; y++) {
                for (int x = 0; x < iX; x++) {
                    _array[x, y] = _value;
                }
            }
            _initialized = true;
        }
        public T[,] CreateArray() {
            if (!_initialized) {
                InitializeArray();
            }
            return (T[,])_array.Clone();
        }
    }
    

Ответы [ 7 ]

4 голосов
/ 04 мая 2010

Замечание о вашем Clone подходе: я сомневаюсь, что вы сможете превзойти его с точки зрения производительности. Однако это может быть критическое изменение, учитывая, что после первой инициализации, он игнорирует параметр Size и просто возвращает массив одинакового размера при каждом вызове . В зависимости от того, имеет ли это значение в вашем сценарии, вы можете:

  1. Придерживайтесь этого, потому что это не имеет значения.
  2. Создать Dictionary<Size, int[,]>считаю, что Size будет вести себя как ключ - не проверял) для предварительной инициализации массива каждый раз, когда запрашивается уникальный Size. Накладные расходы в этом я не уверен.
  3. Откажитесь от идеи Clone.

В случае, если вам в конечном итоге придется перейти с 3 выше, вот несколько пограничных нелепых предложений:

1. Кэшируйте свои свойства Width и Height локально, а не обращайтесь к ним из структуры Size на каждой итерации .

static int[,] CreateArray(Size size) {
    int w = size.Width;
    int h = size.Height;

    int[,] array = new int[w, h];
    for (int x = 0; x < w; x++) {
        for (int y = 0; y < h; y++) {
            array[x, y] = int.MaxValue;
        }
    }

    return array;
}

Для создания массива 1000x1000 на моем компьютере среднее время выполнения составило около 120000 тиков против 140000 тиков.

2. Используйте несколько ядер, если они у вас есть, и параллельно инициализируйте массив.

static int[,] CreateArray(Size size) {
    int w = size.Width;
    int h = size.Height;

    int[,] array = new int[w, h];
    Action<int[,], int, int> fillFirstHalf = FillArray;
    Action<int[,], int, int> fillSecondHalf = FillArray;

    var firstResult = fillFirstHalf.BeginInvoke(array, 0, h / 2, null, null);
    var secondResult = fillSecondHalf.BeginInvoke(array, h / 2, h, null, null);

    fillFirstHalf.EndInvoke(firstResult);
    fillSecondHalf.EndInvoke(secondResult);

    return array;
}

static void FillArray(int[,] array, int ystart, int yend) {
    int w = array.GetLength(0);

    for (int x = 0; x < w; ++x) {
        for (int y = ystart; y < yend; ++y) {
            array[x, y] = int.MaxValue;
        }
    }
}

Это, вероятно, не очень реалистичное предложение в вашем сценарии, поскольку кажется, что вы создаете только массивы 100x100, и в этом случае накладные расходы на распараллеливание превышают выигрыш в производительности. Однако, для создания массива 1000x1000, я обнаружил, что этот подход уменьшил мое время выполнения в среднем до 70 тыс. Тиков (по сравнению с ~ 120 тыс. Тиков, которые я получил при первой предложенной оптимизации).

Кроме того, если вы создаете много массивов таким образом, я настоятельно рекомендую распараллелить , что (то есть, если вам нужно создать тысячу массивов, создайте 500 каждый из двух потоков ), при условии, что у вас есть несколько процессоров, чтобы сделать работу за вас. Без нескольких процессоров, забудьте об этом; добавление тем только ухудшит вашу производительность.

3. Увеличьте производительность, используя указатель unsafe.

Теперь вот интересное открытие: кажется, что двумерный массив в .NET выделяется предсказуемым образом *: в основном, как одномерный блок памяти, где каждая «строка» смещение от начальной точки на величину, эквивалентную длине всех предыдущих строк. Другими словами, к массиву 10x2 можно получить доступ, используя указатель, как массив 20x1; к массиву 10x10 можно получить доступ, как к массиву 100x1 и т. д.

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

* Вполне возможно, что большинство других разработчиков .NET уже знали об этом, и я просто констатирую очевидное, и в этом случае я отменяю свой комментарий об этом "интересе".

В любом случае знание этого позволяет использовать ключевое слово fixed в контексте unsafe для значительного увеличения производительности:

static int[,] CreateArray(Size size) {
    int w = size.Width;
    int h = size.Height;

    int[,] array = new int[w, h];
    unsafe {
        fixed (int* ptr = array) {
            for (int i = 0; i < w * h; ++i)
                ptr[i] = int.MaxValue;
        }
    }

    return array;
}

Для инициализации массивов значительного размера, я бы даже рекомендовал объединить вышеупомянутый подход (распараллеливание) с этим - так, оставьте тот же CreateArray из предложения # 2, а затем переписать FillArray как:

static void FillArray(int[,] array, int ystart, int yend) {
    int w = array.GetLength(0);

    unsafe {
        fixed (int* p = array) {
            for (int i = w * ystart; i < w * yend; ++i)
                p[i] = int.MaxValue;
        }
    } 
}

На самом деле кажется, что вы уже разобрались с этой последней частью до того, как я ее опубликовал, но я решил, что все равно включу ее, главным образом, для того, чтобы объединить unsafe с распараллеливанием.


Примечание по stackalloc: Я думаю, что вы можете гоняться за гномом в конце радуги с этим. Согласно документации stackalloc:

Блок памяти достаточного размера содержать expr элементы типа type размещается в стеке, а не куча; адрес блока хранится в указателе ptr. Эта память не подлежит сборке мусора и поэтому не должен быть закреплен(через fixed). Время жизни блок памяти ограничен время жизни метода, в котором это определено. (выделено мое)

Это заставляет меня поверить, что вы не можете вернуть объект, данные которого хранятся в памяти, выделенной stackalloc из функции, потому что эта память выделяется только на время жизни функции.

3 голосов
/ 04 мая 2010

Неуправляемый массив для гранулярного контроля

Создайте массив в неуправляемом режиме C # ( unsafe ), например, , показанном здесь [код проекта] , и инициализируйте его вручную.

В управляемом режиме C # массив сначала инициализирует все элементы значением по умолчанию, прежде чем ваш цикл начнет его заполнять. Скорее всего, вы можете сократить удвоение в неуправляемом режиме. Это сэкономит много времени.

2 голосов
/ 04 мая 2010

Добавление static и unsafe обеспечивает некоторое снижение тиков. Ниже приведены некоторые образцы.

  • CreateArray; нестатический небезопасный: 521, 464, 453, 474
  • CreateArray; статический: 430, 423, 418, 454
  • CreateArray; небезопасно: 485, 464, 435, 414
  • CreateArray; небезопасные статические: 476, 450, 433, 405

Я пытался использовать stackalloc. Моя идея состояла в том, чтобы выделить массив, который не будет инициализирован, потому что это код unsafe. Затем я бы сжал массив, инициализируя до int.MaxValue, а затем Clone массив для возвращаемого результата. Но я был озадачен многомерной декларацией.

Тогда я вспомнил использование Clone для массивов в другом проекте. Каждый Array.Clone экономил несколько секунд. Основываясь на этой идее, я создал следующую версию подпрограммы CreateArray, получив отличные результаты.

Теперь все, что мне нужно сделать, это заставить stackalloc работать с многомерными массивами.

  • CreateArray5; предварительная инициализация: 2663, 3036
  • CreateArray5; клон: 157, 172

    private static bool _arrayInitialized5;
    private static int[,] _array5;
    
    public static int[,] CreateArray5(Size size) {
        if (!_arrayInitialized5) {
            int iX = size.Width;
            int iY = size.Height;
            _array5 = new int[iX, iY];
            for (int x = 0; x < iX; x++) {
                for (int y = 0; y < iY; y++) {
                    _array5[x, y] = int.MaxValue;
                }
            }
            _arrayInitialized5 = true;
        }
        return (int[,])_array5.Clone();
    }
    

    int[,] actual;

    int iHi = 10000 * 10 * 2; 
    //'absolute minimum times array will be created   (200,000)
    //'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)

    Stopwatch o;

    //'pre-initialize
    o = Stopwatch.StartNew();
    actual = CreateArray5(new Size(100, 100));
    o.Stop();
    Trace.WriteLine(o.ElapsedTicks, "CreateArray5; pre-initialize");
    o = Stopwatch.StartNew();
    for (int i = 0; i < iHi; i++) { actual = CreateArray5(new Size(100, 100)); }
    o.Stop();
    Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray5; static unsafe clone");
1 голос
/ 04 мая 2010

Это не проверено в этом сценарии, но работало в подобных.Иногда замена порядка обхода массива ускоряет работу из-за локальности памяти.

Другими словами, вместо выполнения for(x) ... for(y) вместо for(y) ... for(x).

0 голосов
/ 06 мая 2010

Класс и Обобщение с использованием метода «Клон».

  • MDArray; Инициализация класса клонов: 2444, 2587, 2421, 2406
  • MDArray; класс клонов: 440, 362, 198, 139
  • Grid<int>; родовой класс клонов: 5344, 5334, 5693, 5272
  • Grid<int>; общий класс клонов: 187, 204, 199, 288
  • Grid<bool>; родовой класс клонов инициализируется: 3585, 3537, 3552, 3569
  • Grid<bool>; общий класс клонов: 37, 44, 36, 43
  • Grid<Color>; родовой класс клонов инициализируется: 4139, 3536, 3503, 3533
  • Grid<Color>; общий класс клонов: 2737, 3137, 2414, 2171

[TestClass]
public class CreateArrayTest {

    public class MDArray {
        private bool _initialized;
        private int[,] _array;
        private int _x;
        private int _y;
        private int _value;
        public MDArray(Size size, int value, bool initialize) {
            _x = size.Width;
            _y = size.Height;
            _value = value;
            if (initialize) {
                InitializeArray();
            }
        }
        private void InitializeArray() {
            int iX = _x;
            int iY = _y;
            _array = new int[iX, iY];
            for (int y = 0; y < iY; y++) {
                for (int x = 0; x < iX; x++) {
                    _array[x, y] = _value;
                }
            }
            _initialized = true;
        }
        public int[,] CreateArray() {
            if (!_initialized) {
                InitializeArray();
            }
            return (int[,])_array.Clone();
        }
    }

    public class Grid<T> {
        private T[,] _array;
        private T _value;
        private bool _initialized;
        private int _x;
        private int _y;
        public Grid(Size size, T value, bool initialize) {
            _x = size.Width;
            _y = size.Height;
            _value = value;
            if (initialize) {
                InitializeArray();
            }
        }
        private void InitializeArray() {
            int iX = _x;
            int iY = _y;
            _array = new T[iX, iY];
            for (int y = 0; y < iY; y++) {
                for (int x = 0; x < iX; x++) {
                    _array[x, y] = _value;
                }
            }
            _initialized = true;
        }
        public T[,] CreateArray() {
            if (!_initialized) {
                InitializeArray();
            }
            return (T[,])_array.Clone();
        }
    }

    [TestMethod()]
    public void CreateArray_Test() {

        int[,] actual;

        int iHi = 10000 * 10 * 2; //'absolute minimum times array will be created   (200,000)
        //                          'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)

        Stopwatch o;

        o = Stopwatch.StartNew();
        MDArray oMDArray = new MDArray(new Size(100, 100), int.MaxValue, true);
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks, "     MDArray; clone class initalize");
        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = oMDArray.CreateArray(); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "     MDArray; clone class");

        o = Stopwatch.StartNew();
        Grid<int> oIntMap = new Grid<int>(new Size(100, 100), int.MaxValue, true);
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks, "   Grid<int>; generic clone class initalize");
        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = oIntMap.CreateArray(); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "   Grid<int>; generic clone class");

        bool[,] fActual;
        o = Stopwatch.StartNew();
        Grid<bool> oBolMap = new Grid<bool>(new Size(100, 100), true, true);
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks, "  Grid<bool>; generic clone class initalize");
        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { fActual = oBolMap.CreateArray(); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "  Grid<bool>; generic clone class");

        Color[,] oActual;
        o = Stopwatch.StartNew();
        Grid<Color> oColMap = new Grid<Color>(new Size(100, 100), Color.AliceBlue, true);
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks, " Grid<Color>; generic clone class initalize");
        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { oActual = oColMap.CreateArray(); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, " Grid<Color>; generic clone class");
    }
}
0 голосов
/ 04 мая 2010

Мне удалось снизить тики примерно до 165. См. CreateArray8 ниже.

Я получил идею из раздела «Устранение проверки диапазона» статьи CodeProject на http://www.codeproject.com/KB/dotnet/arrays.aspx, предоставленной jdk. (@jdk, большое спасибо.) Идея состояла в том, чтобы исключить проверку диапазона, используя указатель и инициализируя каждый элемент в одном цикле. Я смог снизить тики примерно до 165. Так же хорошо, как клонирование без задержки предварительной инициализации и поддерживающих статических переменных. (См. Мой другой ответ.)

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

  • CreateArray3; небезопасные статические: 501, 462, 464, 462
  • CreateArray7; статически небезопасно для (у, х): 452, 451, 444, 429
  • CreateArray8; статический небезопасный указатель single_for: 187, 173, 156, 145

[TestClass]
public class CreateArrayTest {

    public static unsafe int[,] CreateArray3(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        for (int x = 0; x < iX; x++) {
            for (int y = 0; y < iY; y++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

    public unsafe static int[,] CreateArray7(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        for (int y = 0; y < iY; y++) {
            for (int x = 0; x < iX; x++) {
                array[x, y] = int.MaxValue;
            }
        }
        return array;
    }

    public unsafe static int[,] CreateArray8(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        int[,] array = new int[iX, iY];
        fixed (int* pfixed = array) {
            int count = array.Length;
            for (int* p = pfixed; count-- > 0; p++)
                *p = int.MaxValue;
        }
        return array;
    }

    public unsafe static int[,] CreateArray9(Size size) {
        int iX = size.Width;
        int iY = size.Height;
        void* array = stackalloc int[iX * iY];
        int count = iX * iY;
        for (int* p = (int*)array; count-- > 0; p++)
            *p = int.MaxValue;

        //return (int[,])array; //how to return?
        return new int[1, 1];
    }

    [TestMethod()]
    public void CreateArray_Test() {

        int[,] actual;

        int iHi = 10000 * 10 * 2;
        //'absolute minimum times array will be created   (200,000)
        //'could be as high as 10000 * 10 * 20? * 50? (100,000,000?)

        Stopwatch o;

        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = CreateArray3(new Size(100, 100)); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray3; static unsafe");

        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = CreateArray7(new Size(100, 100)); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray7; static unsafe for(y,x)");

        o = Stopwatch.StartNew();
        for (int i = 0; i < iHi; i++) { actual = CreateArray8(new Size(100, 100)); }
        o.Stop();
        Trace.WriteLine(o.ElapsedTicks / iHi, "CreateArray8; static unsafe pointer single_for");

    }

}
0 голосов
/ 04 мая 2010

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

Тем не менее, я думаю, что для этого есть ограничение. В очереди могут находиться только 64 операции, но в этом случае вы можете поставить в очередь от 0 до 63, от 64 до 127 и т. Д. В Parallel.For ..

public int[,] CreateArray(Size size) { 
    int[,] array = new int[size.Width, size.Height]; 
    System.Threading.Paralle.For (0,size.Width, 
      x=>{ 
        for (int y = 0; y < size.Height; y++) { 
            array[x, y] = int.MaxValue; 
        } 
      }
    ); 
    return array; 
} 

Это включено в .NEt 4, однако для .NET 3.51 вы можете получить ту же библиотеку из codeplex.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...