Microsoft Visual C # 2008 Сокращение количества загружаемых библиотек - PullRequest
16 голосов
/ 27 февраля 2010

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

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

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

Его предложение "отключить хостинг Visual Studio" решило проблему.


(подробнее см. Историю развития вопроса ниже)








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

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

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

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

Резюме:

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


Обновление:

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

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

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

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

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

Ответы [ 7 ]

9 голосов
/ 27 февраля 2010

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

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

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

7 голосов
/ 27 февраля 2010

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, что может помочь вам точно определить, почему память не может быть выделена.

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

3 голосов
/ 05 марта 2010

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

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

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

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

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

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

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

Класс выделяет группу из 8192-элементных массивов (13-битных индексов), которые на 64-битных для ссылочных типов упадут ниже предела больших объектов. Если вы собираетесь использовать это только для 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;
            }
        }
    }
}
1 голос
/ 27 февраля 2010

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

0 голосов
/ 05 марта 2010

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

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

0 голосов
/ 05 марта 2010

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

0 голосов
/ 05 марта 2010

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

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

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

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

CLR Profiler для .NET Framework 2.0 в http://www.microsoft.com/downloads/details.aspx?familyid=A362781C-3870-43BE-8926-862B40AA0CD0&displaylang=en

...