Microsoft Visual C# 2008 Сокращение количества загружаемых библиотек
Как я могу уменьшить количество загружаемых библиотек при отладке в Visual C# 2008 Express Edition?
При запуске визуального проекта C# в отладчике я получаю исключение OutOfMemoryException из-за фрагментации виртуального адресного пространства объемом 2 ГБ, и мы предполагаем, что причиной фрагментации могут быть загруженные библиотеки DLL.
Брайан Расмуссен, ты сделал мой день!:)
Его предложение "отключить хостинг визуальной студии" решило проблему.
(для получения дополнительной информации см. историю развития вопроса ниже)
Привет, мне нужно, чтобы два больших массива 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, и нашел большое свободное место для автономной версии inbetween (21) dll, но не для vshost-debugger-version (58 DLLs). Я не утверждаю, что между ними не может быть ничего другого, и я не совсем уверен, имеет ли смысл то, что я там делаю, но это согласуется с анализом VMMap s, и кажется, что одни dll уже фрагментируют память для версия отладчика.
Так что, возможно, решение будет, если я смогу уменьшить количество DLL, используемых отладчиком.
1. Это возможно?
2. Если да, как бы я это сделал?
7 ответов
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, поэтому я не уверен, что вы выиграете от перебазирования на этих платформах.
Вы боретесь с фрагментацией адресного пространства виртуальной памяти. Для процесса в 32-разрядной версии Windows доступно 2 гигабайта памяти. Эта память разделяется как кодом, так и данными. Кусками кода являются CLR и JIT-компилятор, а также сборки фреймворка ngen-ed. Куски данных - это различные кучи, используемые в.NET, включая кучу загрузчика (статические переменные) и кучи сборщика мусора. Эти куски расположены по разным адресам в карте памяти. Свободная память доступна для размещения ваших массивов.
Проблема в том, что большой массив требует непрерывного куска памяти. "Дырки" в адресном пространстве между кусками кода и данными недостаточно велики, чтобы позволить вам выделять такие большие массивы. Первая дыра обычно составляет от 450 до 550 мегабайт, поэтому первое выделение массива прошло успешно. Следующая доступная дыра намного меньше. Слишком маленький, чтобы вместить другой большой массив, вы получите OOM, даже если у вас останется легкий гигабайт свободной памяти.
Вы можете посмотреть на структуру виртуальной памяти вашего процесса с помощью утилиты SysInternals VMMap. Хорошо для диагностики, но это не решит твою проблему. Есть только одно реальное исправление - переход на 64-битную версию Windows. Возможно, лучше: переосмыслить свой алгоритм, чтобы он не требовал таких больших массивов.
Это не ответ сам по себе, но, возможно, альтернатива может сработать.
Если проблема действительно в том, что у вас есть фрагментированная память, то, возможно, одним из обходных путей было бы просто использовать эти дыры вместо того, чтобы пытаться найти дыру, достаточно большую для всего подряд.
Вот очень простой класс BigArray, который не добавляет слишком много накладных расходов (некоторые накладные расходы вводятся, особенно в конструкторе, для инициализации сегментов).
Статистика для массива:
- Основной выполняется за 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;
}
}
}
}
Однажды у меня была похожая проблема, и в итоге я использовал список вместо массива. При создании списков я устанавливал емкость на требуемые размеры и определял оба списка ДО того, как попытался добавить к ним значения. Я не уверен, что вы можете использовать списки вместо массивов, но это может быть что-то, чтобы рассмотреть. В конце концов мне пришлось запустить исполняемый файл на 64-битной ОС, потому что, когда я добавил элементы в список, общее использование памяти превысило 2 ГБ, но, по крайней мере, я смог запустить и отладить локально с уменьшенным набором данных.
Каждый 32-битный процесс имеет 2 ГБ адресного пространства (если только вы не попросите пользователя добавить /3 ГБ в параметрах загрузки), поэтому, если вы допустите некоторое снижение производительности, вы можете запустить новый процесс, чтобы получить еще 2 ГБ в адресном пространстве - ну, немного меньше, чем это. Новый процесс будет по-прежнему фрагментирован всеми DLL-библиотеками CLR плюс всеми DLL-библиотеками Win32, которые они используют, поэтому вы можете избавиться от всей фрагментации адресного пространства, вызванной DLL-библиотеками CLR, написав новый процесс на родном языке, например C++. Вы даже можете перенести часть своих расчетов в новый процесс, чтобы получить больше адресного пространства в основном приложении и меньше общаться с основным процессом.
Вы можете общаться между вашими процессами, используя любой из методов взаимодействия между процессами. Вы можете найти много примеров IPC в All-In-One Code Framework.
Вопрос: заняты ли все элементы вашего массива? Если многие из них содержат какое-то значение по умолчанию, возможно, вы могли бы уменьшить потребление памяти, используя реализацию разреженного массива, который выделяет память только для значений, отличных от значений по умолчанию. Просто мысль.
У меня есть опыт работы с двумя настольными приложениями и одним мобильным приложением, выходящим за пределы нехватки памяти. Я понимаю проблемы. Я не знаю ваших требований, но предлагаю перенести ваши поисковые массивы в SQL CE. Производительность хорошая, вы будете удивлены, а SQL CE находится в процессе. С последним настольным приложением мне удалось уменьшить объем используемой памяти с 2,1 ГБ до 720 МБ, что позволило ускорить работу приложения благодаря значительному уменьшению количества сбоев страниц. (Ваша проблема - фрагментация памяти AppDomain, которую вы не можете контролировать.)
Честно говоря, я не думаю, что вы будете удовлетворены производительностью после сжатия этих массивов в память. Не забывайте, что чрезмерные сбои страниц значительно влияют на производительность.
Если вы используете 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