Уменьшение использования памяти приложениями .NET?

108

Какие советы по сокращению использования памяти приложениями .NET? Рассмотрим следующую простую программу на C #.

class Program
{
    static void Main(string[] args)
    {
        Console.ReadLine();
    }
}

Скомпилированный в режиме выпуска для x64 и работающий вне Visual Studio, диспетчер задач сообщает следующее:

Working Set:          9364k
Private Working Set:  2500k
Commit Size:         17480k

Немного лучше, если он скомпилирован только для x86 :

Working Set:          5888k
Private Working Set:  1280k
Commit Size:          7012k

Затем я попробовал следующую программу, которая делает то же самое, но пытается сократить размер процесса после инициализации среды выполнения:

class Program
{
    static void Main(string[] args)
    {
        minimizeMemory();
        Console.ReadLine();
    }

    private static void minimizeMemory()
    {
        GC.Collect(GC.MaxGeneration);
        GC.WaitForPendingFinalizers();
        SetProcessWorkingSetSize(Process.GetCurrentProcess().Handle,
            (UIntPtr) 0xFFFFFFFF, (UIntPtr)0xFFFFFFFF);
    }

    [DllImport("kernel32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetProcessWorkingSetSize(IntPtr process,
        UIntPtr minimumWorkingSetSize, UIntPtr maximumWorkingSetSize);
}

Результаты в выпуске x86 вне Visual Studio:

Working Set:          2300k
Private Working Set:   964k
Commit Size:          8408k

Что немного лучше, но все же кажется чрезмерным для такой простой программы. Есть ли какие-нибудь уловки, чтобы сделать процесс C # немного проще? Я пишу программу, которая большую часть времени предназначена для работы в фоновом режиме. Я уже занимаюсь любым пользовательским интерфейсом в отдельном домене приложения, что означает, что пользовательский интерфейс можно безопасно выгружать, но занимать 10 МБ, когда он просто находится в фоновом режиме, кажется чрезмерным.

PS Насчет того, почему меня это волнует --- (Опытные) пользователи обычно беспокоятся об этих вещах. Даже если это почти не влияет на производительность, полутехнические пользователи (моя целевая аудитория) имеют тенденцию впадать в истерику по поводу использования памяти фоновыми приложениями. Даже я схожу с ума, когда вижу, что Adobe Updater занимает 11 МБ памяти, и чувствую себя успокаивающим от успокаивающего прикосновения Foobar2000, который может занимать менее 6 МБ даже во время игры. Я знаю, что в современных операционных системах эти вещи действительно не имеют большого значения технически, но это не значит, что они не влияют на восприятие.

Роберт Фрейзер
источник
14
Зачем тебе это нужно? Частный рабочий набор довольно низкий. Современные ОС будут выгружать страницу на диск, если память не нужна. На дворе 2009 год. Если вы не создаете что-то на встроенных системах, вам не нужно беспокоиться о 10 МБ.
Mehrdad Afshari
8
Прекратите использовать .NET, и вы сможете создавать небольшие программы. Чтобы загрузить платформу .NET, в память необходимо загрузить множество больших DLL.
Адам Силлс,
2
Цены на память падают экспоненциально (да, сейчас вы можете заказать домашний компьютер Dell с 24 ГБ ОЗУ). Если ваше приложение не использует> 500 МБ, оптимизация не требуется.
Alex
28
@LeakyCode Мне очень неприятно, что современные программисты думают так, вы должны заботиться об использовании памяти вашим приложением. Я могу сказать, что большинство современных приложений, написанных в основном на java или C #, довольно неэффективны, когда дело доходит до управления ресурсами, и благодаря этому в 2014 году мы можем запускать столько приложений, сколько могли в 1998 году, на win95 и 64 МБ оперативной памяти. ... всего 1 экземпляр браузера теперь ест 2 ГБ оперативной памяти, а простая IDE около 1 ГБ. Баран дешевый, но это не значит, что вы должны его тратить.
Петр
6
@Petr Вам стоит позаботиться об управлении ресурсами. Время программиста - тоже ресурс. Пожалуйста, не преувеличивайте трату от 10 МБ до 2 ГБ.
Mehrdad Afshari

Ответы:

33
  1. Возможно, вы захотите проверить вопрос о переполнении стека. Объем памяти .NET EXE .
  2. Сообщение в блоге MSDN « Рабочий набор! = Фактический объем памяти» - это демистификация рабочего набора, памяти процесса и того, как выполнять точные вычисления общего потребления оперативной памяти.

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

Если вы пишете стандартные клиентские приложения Windows Forms и WPF, которые предназначены для работы на индивидуальном ПК и, вероятно, будут основным приложением, в котором работает пользователь, вам может сойти с рук более скудное отношение к распределению памяти. (Пока все это освобождается.)

Тем не менее, чтобы обратиться к некоторым людям, которые говорят, что не беспокойтесь об этом: если вы пишете приложение Windows Forms, которое будет работать в среде служб терминалов на общем сервере, который, возможно, будет использоваться 10, 20 или более пользователями, тогда да , вы обязательно должны учитывать использование памяти. И вам нужно будет быть бдительным. Лучший способ решить эту проблему - разработать хорошую структуру данных и следовать лучшим практикам в отношении того, когда и что вы выделяете.

Джон Руди
источник
45

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

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

Если вы хотите, чтобы занимаемая площадь оставалась небольшой, вам придется подумать об использовании памяти. Вот пара идей:

  • Уменьшите количество объектов и убедитесь, что вы не держите их дольше, чем требуется.
  • Имейте в виду List<T>и подобные типы, которые удваивают производительность, когда это необходимо, поскольку они могут привести к 50% отходов.
  • Вы можете рассмотреть возможность использования типов значений вместо ссылочных типов, чтобы увеличить объем памяти в стеке, но имейте в виду, что пространство стека по умолчанию составляет всего 1 МБ.
  • Избегайте объектов размером более 85000 байт, так как они попадут в LOH, который не сжимается и, следовательно, может легко фрагментироваться.

Это, вероятно, ни в коем случае не исчерпывающий список, а всего лишь пара идей.

Брайан Расмуссен
источник
IOW, в .NET работают те же методы, которые работают для уменьшения размера нативного кода?
Роберт Фрейзер,
Я предполагаю, что есть некоторое совпадение, но с собственным кодом у вас есть больше вариантов использования памяти.
Брайан Расмуссен,
17

В этом случае вам нужно учитывать одну вещь - это стоимость памяти для среды CLR. CLR загружается для каждого процесса .Net и, следовательно, учитывает требования к памяти. Для такой простой / небольшой программы стоимость CLR будет преобладать над объемом памяти.

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

JaredPar
источник
7

Как таковых конкретных предложений нет, но вы можете взглянуть на Средство профилирования среды CLR (бесплатно загружаемое с сайта Microsoft).
После того, как вы установили его, взгляните на эту страницу с инструкциями .

Из инструкции:

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

Пончик
источник
7

Возможно, вы захотите посмотреть на использование памяти «настоящим» приложением.

Как и в случае с Java, существует фиксированная сумма накладных расходов для среды выполнения независимо от размера программы, но после этого потребление памяти будет гораздо более разумным.

Эрик Петрелье
источник
4

Есть еще способы уменьшить частный рабочий набор этой простой программы:

  1. NGEN ваше приложение. Это устраняет затраты на JIT-компиляцию из вашего процесса.

  2. Обучите свое приложение, используя MPGO, уменьшив использование памяти, а затем NGEN.

Фэн Юань
источник
2

Есть много способов уменьшить ваш след.

Одна вещь, с которой вам всегда придется жить в .NET, - это то, что размер собственного образа вашего IL-кода огромен.

И этот код не может быть полностью разделен между экземплярами приложения. Даже сборки, созданные на основе NGEN , не являются полностью статичными, в них все еще есть небольшие детали, требующие JIT-обработки.

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

Часто встречающийся пример: использование Datareader, загрузка содержимого в DataTable только для записи в XML-файл. Вы можете легко столкнуться с OutOfMemoryException. OTOH, вы можете использовать XmlTextWriter и прокручивать Datareader, генерируя XmlNodes при прокрутке курсора базы данных. Таким образом, у вас будет в памяти только текущая запись базы данных и ее вывод XML. Который никогда (или вряд ли) получит более высокое поколение сборки мусора и, следовательно, может быть повторно использован.

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

В C # есть отличная функция, называемая итераторами. Они позволяют передавать объекты в потоковом режиме путем прокрутки ввода и сохранять только текущий экземпляр, пока вы не получите следующий. Даже при использовании LINQ вам все равно не нужно хранить все это просто потому, что вы хотите, чтобы это было отфильтровано.

Роберт Гизеке
источник
1

Отвечая на общий вопрос в заголовке, а не на конкретный вопрос:

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

Это то, что я сделал в своем основном приложении, и оно значительно улучшило потребление памяти.

Питер Мортенсен
источник
0

Я обнаружил, что использование API-интерфейсов SetProcessWorkingSetSize или EmptyWorkingSet для периодической принудительной загрузки страниц памяти на диск в длительном процессе может привести к тому, что вся доступная физическая память на машине будет эффективно исчезать до тех пор, пока машина не будет перезагружена. У нас была .NET DLL, загруженная в собственный процесс, который будет использовать EmptyWorkingSet API (альтернатива использованию SetProcessWorkingSetSize) для уменьшения рабочего набора после выполнения задачи с интенсивным использованием памяти. Я обнаружил, что где-то от 1 дня до недели машина будет показывать 99% использования физической памяти в диспетчере задач, в то время как никакие процессы не используют сколько-нибудь значительного использования памяти. Вскоре после этого машина перестала отвечать, что требовало полной перезагрузки. Указанные машины представляли собой более двух десятков серверов Windows Server 2008 R2 и 2012 R2, работающих как на физическом, так и на виртуальном оборудовании.

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

Стюарт Велч
источник