Microsoft Visual С# 2008 Сокращение количества загруженных DLL

Как уменьшить количество загружаемых dll при отладке в Visual С# 2008 Express Edition?

При запуске визуального проекта С# в отладчике я получаю исключение OutOfMemoryException из-за фрагментации виртуального адресного пространства 2 ГБ, и мы предполагаем, что причиной для фрагментации могут быть загруженные DLL.

Брайан Расмуссен, ты сделал мой день!:)

Его предложение "отключить процесс хостинга визуальной студии" решило проблему.


(дополнительную информацию см. в истории вопроса-разработки ниже)








Привет, Мне нужно, чтобы две большие int-массивы загружались в память с ~ 120 миллионами элементов (~ 470 МБ) каждый и оба в одном проекте Visual С#.

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

У меня достаточно полной свободной памяти, и после того, как я сделал веб-поиск, я подумал, что моя проблема в том, что в моей системе не хватает достаточно смежных блоков свободной памяти. НО! - когда я создаю экземпляр только одного из массивов в одном экземпляре Visual С#, а затем открываю другой экземпляр Visual С#, второй экземпляр может создать массив из 470 МБ. (Изменить для уточнения: в приведенном выше параграфе я имел в виду, что он запускал его в отладчике Visual С#)

И диспетчер задач показывает соответствующее увеличение использования памяти так же, как и следовало ожидать. Поэтому недостаточно непрерывных блоков памяти для всей системы не проблема. Затем я попытался запустить скомпилированный исполняемый файл, который создает экземпляры обоих массивов, которые также работают (использование памяти 1 ГБ)

Резюме:

OutOfMemoryException в Visual С# с использованием двух больших массивов int, но запуск скомпилированных exe-работ (использование памяти 1GB) и два отдельных экземпляра Visual С# могут найти два достаточно больших смежных блока памяти для моих больших массивов, но мне нужен один Visual С#, чтобы обеспечить память.


Обновление:

Прежде всего, особая благодарность nobugz и Брайану Расмуссену, я думаю, что они на месте с их предсказанием, что проблема "Фрагментация виртуального адресного пространства 2GB процесса".

Следуя их предложениям, я использовал VMMap и listdll для моего короткого любительского анализа, и я получаю:
* 21 DLL, перечисленных для "автономного" -exe. (тот, который работает и использует 1 ГБ памяти.)
* 58 dll, перечисленных для версии vshost.exe. (версия, которая запускается при отладке и которая генерирует исключение и использует только 500 МБ)

VMMap показал мне самые большие свободные блоки памяти для версии отладчика: 262,175,167,155,108MBs.
Таким образом, VMMap говорит, что нет непрерывного блока 500 МБ, и, согласно информации о свободных блоках, я добавил ~ 9 меньших встроенных массивов, которые добавили более 1,2 ГБ памяти и действительно работали. Поэтому из этого я бы сказал, что мы можем назвать "фрагментацию виртуального адресного пространства 2 ГБ" виновным.

Из вывода listdll я создал небольшую электронную таблицу с шестнадцатеричными числами, преобразованными в десятичные числа, чтобы проверять свободные области между dll, и я нашел большое свободное пространство для автономной версии inbetween (21) dll, но не для vshost-debugger- версия (58 DLL). Я не утверждаю, что между ними не может быть ничего другого, и я не уверен, что то, что я делаю там, имеет смысл, но похоже, что это согласуется с анализом VMMaps, и кажется, что только DLL уже фрагментирует память для отладчик-версия.

Итак, возможно, решение было бы, если бы я смог уменьшить количество DLL, используемых отладчиком.
1. Возможно ли это? 2. Если да, как бы я это сделал?

Ответ 1

3-е обновление. Вы можете значительно уменьшить количество загружаемых DLL, отключив хостинг Visual Studio (свойства проекта, отладка). Это все равно позволит вам отлаживать приложение, но оно избавится от много библиотек DLL и ряда вспомогательных потоков.

В небольшом тестовом проекте количество загружаемых DLL файлов увеличилось с 69 до 34, когда я отключил процесс хостинга. Я также избавился от 10+ потоков. В целом значительное сокращение использования памяти, которое также должно помочь уменьшить фрагментацию кучи.

Дополнительная информация о хостинге: http://msdn.microsoft.com/en-us/library/ms242202.aspx


Причина, по которой вы можете загрузить второй массив в новом приложении, состоит в том, что каждый процесс получает полное виртуальное адресное пространство на 2 ГБ. То есть ОС будет обменивать страницы, чтобы каждый процесс мог обрабатывать общий объем памяти. Когда вы пытаетесь выделить оба массива в одном процессе, среда выполнения должна иметь возможность выделить два смежных фрагмента нужного размера. Что вы храните в массиве? Если вы храните объекты, вам нужно дополнительное пространство для каждого из объектов.

Помните, что приложение фактически не запрашивает физическую память. Вместо этого каждому приложению предоставляется адресное пространство, из которого они могут выделять виртуальную память. Затем ОС сопоставляет виртуальную память с физической памятью. Это довольно сложный процесс (Руссинович тратит более 100 страниц на то, как Windows обрабатывает память в своей внутренней книге Windows). Подробнее о том, как Windows это делает, см. http://blogs.technet.com/markrussinovich/archive/2008/11/17/3155406.aspx

Обновление: Я размышлял над этим вопросом некоторое время, и это звучит немного странно. При запуске приложения через Visual Studio вы можете видеть, что дополнительные модули загружаются в зависимости от вашей конфигурации. В моей настройке я получаю несколько разных DLL, загруженных во время отладки из-за профилировщиков и TypeMock (что по сути делает его магию через крючки профилировщика).

В зависимости от их размера и адреса загрузки они могут препятствовать распределению непрерывной памяти времени выполнения. Сказав это, я все еще немного удивлен тем, что вы получили OOM после выделения только двух из этих больших массивов, поскольку их объединенный размер составляет менее 1 ГБ.

Вы можете посмотреть загруженные библиотеки DLL с помощью инструментов listdlls из SysInternals. Он покажет вам адреса и размер загрузки. Кроме того, вы можете использовать WinDbg. Команда lm показывает загруженные модули. Если вам нужен размер, вам нужно указать опцию v для подробного вывода. WinDbg также позволит вам изучить кучи .NET, которые могут помочь вам определить, почему память не может быть выделена.

2nd Update. Если вы находитесь в Windows XP, вы можете попробовать rebase часть загруженного DLL, чтобы освободить более непрерывное пространство. Vista и Windows 7 используют ASLR, поэтому я не уверен, что вы выиграете от перезагрузки на этих платформах.

Ответ 2

Вы сражаетесь с фрагментацией адресного пространства виртуальной памяти. Процесс 32-разрядной версии Windows имеет 2 ГБ доступной памяти. Эта память совместно используется кодом, а также данными. Куски кода - это CLR и JIT-компилятор, а также сборки фреймов. Куски данных - это различные кучи, используемые .NET, включая кучу загрузчика (статические переменные) и сборку мусора. Эти куски расположены по разным адресам на карте памяти. Свободная память доступна для размещения ваших массивов.

Проблема заключается в том, что для большого массива требуется непрерывный кусок памяти. "Отверстия" в адресном пространстве между кусками кода и данных недостаточно велики, чтобы позволить вам выделять такие большие массивы. Первое отверстие обычно составляет от 450 до 550 мегабайт, поэтому ваше первое распределение массива преуспело. Следующая доступная дыра намного меньше. Слишком маленький, чтобы соответствовать другому большому массиву, вы получите OOM, даже если у вас остался простой гигабайт свободной памяти.

Вы можете посмотреть схему виртуальной памяти вашего процесса с помощью утилиты SysInternals VMMap. Хорошо для диагностики, но это не решит вашу проблему. Там только одно реальное исправление, переходящее к 64-битной версии Windows. Возможно, лучше: переосмыслите свой алгоритм, чтобы он не требовал таких больших массивов.

Ответ 3

Это не ответ как таковой, но, возможно, альтернатива может работать.

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

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

Статистика для массива:

  • Main исполняется в 404 мс
  • статический конструктор программ не отображается

Статистика для класса:

  • Главная заняла 473 мс
  • статический конструктор программ занимает 837 мс (инициализация ковшей)

Класс выделяет кучу массивов из 8192 элементов (13-разрядные индексы), которые по 64-битным для ссылочных типов будут падать ниже предела LOB. Если вы только собираетесь использовать это для Int32, вы, вероятно, можете увеличить это до 14 и, возможно, даже сделать его ненастоящим, хотя я сомневаюсь, что он значительно улучшит производительность.

В другом направлении, если вы боитесь, что у вас будет много отверстий меньше, чем массивы 8192-элементов (64 КБ на 64-битных или 32 КБ на 32-разрядных), вы можете просто уменьшить бит -размер для индексов ковша по его константе. Это добавит дополнительные накладные расходы для конструктора и добавит больше служебных данных памяти, поскольку самый большой массив будет больше, но производительность не должна быть затронута.

Здесь код:

using System;
using NUnit.Framework;

namespace ConsoleApplication5
{
    class Program
    {
        // static int[] a = new int[100 * 1024 * 1024];
        static BigArray<int> a = new BigArray<int>(100 * 1024 * 1024);

        static void Main(string[] args)
        {
            int l = a.Length;
            for (int index = 0; index < l; index++)
                a[index] = index;
            for (int index = 0; index < l; index++)
                if (a[index] != index)
                    throw new InvalidOperationException();
        }
    }

    [TestFixture]
    public class BigArrayTests
    {
        [Test]
        public void Constructor_ZeroLength_ThrowsArgumentOutOfRangeException()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() =>
            {
                new BigArray<int>(0);
            });
        }

        [Test]
        public void Constructor_NegativeLength_ThrowsArgumentOutOfRangeException()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() =>
            {
                new BigArray<int>(-1);
            });
        }

        [Test]
        public void Indexer_SetsAndRetrievesCorrectValues()
        {
            BigArray<int> array = new BigArray<int>(10001);
            for (int index = 0; index < array.Length; index++)
                array[index] = index;
            for (int index = 0; index < array.Length; index++)
                Assert.That(array[index], Is.EqualTo(index));
        }

        private const int PRIME_ARRAY_SIZE = 10007;

        [Test]
        public void Indexer_RetrieveElementJustPastEnd_ThrowsIndexOutOfRangeException()
        {
            BigArray<int> array = new BigArray<int>(PRIME_ARRAY_SIZE);
            Assert.Throws<IndexOutOfRangeException>(() =>
            {
                array[PRIME_ARRAY_SIZE] = 0;
            });
        }

        [Test]
        public void Indexer_RetrieveElementJustBeforeStart_ThrowsIndexOutOfRangeException()
        {
            BigArray<int> array = new BigArray<int>(PRIME_ARRAY_SIZE);
            Assert.Throws<IndexOutOfRangeException>(() =>
            {
                array[-1] = 0;
            });
        }

        [Test]
        public void Constructor_BoundarySizes_ProducesCorrectlySizedArrays()
        {
            for (int index = 1; index < 16384; index++)
            {
                BigArray<int> arr = new BigArray<int>(index);
                Assert.That(arr.Length, Is.EqualTo(index));

                arr[index - 1] = 42;
                Assert.That(arr[index - 1], Is.EqualTo(42));
                Assert.Throws<IndexOutOfRangeException>(() =>
                {
                    arr[index] = 42;
                });
            }
        }
    }

    public class BigArray<T>
    {
        const int BUCKET_INDEX_BITS = 13;
        const int BUCKET_SIZE = 1 << BUCKET_INDEX_BITS;
        const int BUCKET_INDEX_MASK = BUCKET_SIZE - 1;

        private readonly T[][] _Buckets;
        private readonly int _Length;

        public BigArray(int length)
        {
            if (length < 1)
                throw new ArgumentOutOfRangeException("length");

            _Length = length;
            int bucketCount = length >> BUCKET_INDEX_BITS;
            bool lastBucketIsFull = true;
            if ((length & BUCKET_INDEX_MASK) != 0)
            {
                bucketCount++;
                lastBucketIsFull = false;
            }

            _Buckets = new T[bucketCount][];
            for (int index = 0; index < bucketCount; index++)
            {
                if (index < bucketCount - 1 || lastBucketIsFull)
                    _Buckets[index] = new T[BUCKET_SIZE];
                else
                    _Buckets[index] = new T[(length & BUCKET_INDEX_MASK)];
            }
        }

        public int Length
        {
            get
            {
                return _Length;
            }
        }

        public T this[int index]
        {
            get
            {
                return _Buckets[index >> BUCKET_INDEX_BITS][index & BUCKET_INDEX_MASK];
            }

            set
            {
                _Buckets[index >> BUCKET_INDEX_BITS][index & BUCKET_INDEX_MASK] = value;
            }
        }
    }
}

Ответ 4

У меня была аналогичная проблема один раз, и в результате я использовал список вместо массива. При создании списков я устанавливал емкость требуемых размеров, и я определил оба списка ПЕРЕД тем, как я попытался добавить к ним значения. Я не уверен, что вы можете использовать списки вместо массивов, но это может быть что-то рассмотреть. В итоге мне пришлось запустить исполняемый файл на 64-битной ОС, потому что когда я добавил элементы в список, общее использование памяти превысило 2 ГБ, но, по крайней мере, я мог запускать и отлаживать локально с уменьшенным набором данных.

Ответ 5

У меня есть опыт работы с двумя настольными приложениями и одним мобильным приложением, выходящим за пределы памяти. Я понимаю проблемы. Я не знаю ваших требований, но я предлагаю переместить ваши поисковые массивы в SQL CE. Производительность хорошая, вы будете удивлены, и SQL CE находится в процессе. С последним настольным приложением мне удалось уменьшить объем памяти с 2,1 ГБ до 720 МБ, что позволило ускорить работу приложения из-за значительного сокращения ошибок страниц. (Ваша проблема - фрагментация памяти AppDomain, которой вы не контролируете.)

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

Если вы заходите в SqlServerCe, убедитесь, что соединение открыто для повышения производительности. Кроме того, однострочные поисковые запросы (скалярные) могут быть медленнее, чем возврат набора результатов.

Если вы действительно хотите узнать, что происходит с памятью, используйте CLR Profiler. VMMap не поможет. ОС не выделяет память для вашего приложения. Framework делает, захватывая большие патроны OS-памяти для себя (кэшируя память), а затем выделяя при необходимости части этой памяти для приложений.

Профилировщик CLR для .NET Framework 2.0 на http://www.microsoft.com/downloads/details.aspx?familyid=A362781C-3870-43BE-8926-862B40AA0CD0&displaylang=en

Ответ 6

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

Ответ 7

Каждый 32-битный процесс имеет адресное пространство 2 ГБ (если вы не попросите пользователя добавить /3 ГБ в параметры загрузки), поэтому, если вы можете принять выпадение производительности, вы можете запустить новый процесс, чтобы получить еще 2 ГБ адресного пространства - ну, немного меньше. Новый процесс будет по-прежнему фрагментирован со всеми DLL CLR и всеми DLL-библиотеками Win32, которые они используют, поэтому вы можете избавиться от всей фрагментации пространственного пространства, вызванной DLL CLR, написав новый процесс на родном языке, например. С++. Вы даже можете перенести некоторые из ваших вычислений в новый процесс, чтобы получить больше адресного пространства в главном приложении и менее чаты с вашим основным процессом.

Вы можете связываться между вашими процессами с помощью любого из методов межпроцессного взаимодействия. Вы можете найти много образцов IPC в Все-в-одном Code Framework.