Управление памятью в Windows как оно есть
Цель данной статьи — не полное описание работы менеджера памяти (не хватит ни места ни опыта), а попытка пролить хоть немного света на темное царство мифов и суеверий, окружающих вопросы управления памятью в Windows.
Disclaimer
Сам я не претендую на то, чтобы знать все и никогда не ошибаться, поэтому с радостью приму любые сообщения о неточностях и ошибках.
Введение
С чего начать не знаю, поэтому начну с определений.

Commit Size — количество памяти, которое приложение запросило под собственные нужды.
Working Set (на картинке выше он так и называется Working Set) — это набор страниц физической памяти, которые в данный момент «впечатаны» в адресное пространство процесса. Рабочий набор процесса System принято выделять в отдельный «Системный рабочий набор», хотя механизмы работы с ним практически не отличаются от механизмов работы с рабочими наборами остальных процессов.
И уже здесь зачастую начинается непонимание. Если присмотреться, можно увидеть, что Commit у многих процессов меньше Working Set-а. То есть если понимать буквально, «запрошено» меньше памяти, чем реально используется. Так что уточню, Commit — это виртуальная память, «подкрепленная» (backed) только физической памятью или pagefile-ом, в то время как Working Set содержит еще и страницы из memory mapped файлов. Зачем это делается? Когда делается NtAllocateVirtualMemory (или любые обертки над heap manager-ом, например malloc или new) — память как бы резервируется (чтоб еще больше запутать, это не имеет никакого отношения к MEM_RESERVE, который резервирует адресное пространство, в данном же случае речь идет о резервировании именно физических страниц, которые система действительно может выделить), но физические страницы впечатываются только при фактическом обращении по выделенному адресу виртуальной памяти. Если позволить приложениям выделить больше памяти, чем система реально может предоставить — рано или поздно может случиться так, что все они попросят реальную страницу, а системе неоткуда будет ее взять (вернее некуда будет сохранить данные). Это не касается memory mapped файлов, так как в любой момент система может перечитать/записать нужную страницу прямо с/на диск(а).
В общем, суммарный Commit Charge в любой момент времени не должен превышать системный Commit Limit (грубо, суммарный объем физической памяти и всех pagefile-ов) и с этим связана одна из неверно понимаемых цифр на Task Manager-ах до Висты включительно.
Commit Limit не является неизменным — он может увеличиваться с ростом pagefile-ов. Вообще говоря, можно считать, что pagefile — это такой очень специальный memory mapped файл: привязка физической страницы в виртуальной памяти к конкретному месту в pagefile-е происходит в самый последний момент перед сбросом, в остальном же механизмы memory mapping-а и swapping-а очень схожи.
Working Set процесса делится на Shareable и Private. Shareable — это memory mapped файлы (в том числе и pagefile backed), вернее те части, которые в данный момент действительно представлены в адресном пространстве процесса физической страницей (это же Working Set в конце концов), а Private — это куча, стеки, внутренние структуры данных типа PEB/TEB и т.д. (опять таки, повторюсь на всякий случай: речь идет только той части кучи и прочих структур, которые физически находятся в адресном пространстве процесса). Это тот минимум информации, с которой уже можно что то делать. Для сильных духом есть Process Explorer, который показывает еще больше подробностей (в частности какая часть вот той Shareable действительно Shared).
И, самое главное, ни один из этих параметров по отдельности не позволяет сделать более менее полноценных выводов о происходящем в программе/системе.
Task Manager
Столбец «Memory» в списке процессов и практически вся вкладка «Performance» настолько часто понимаются неправильно, что у меня есть желание, чтоб Task Manager вообще удалили из системы: те, кому надо смогут воспользоваться Process Explorer-ом или хотя бы Resource Monitor-ом, всем остальным Task Manager только вредит. Для начала, собственно о чем речь



Начну с того, о чем я уже упоминал: Page File usage. XP показывает текущее использование pagefile-а и историю (самое забавное, что в статус баре те же цифры названы правильно), Виста — показывает Page File (в виде дроби Current/Limit), и только Win7 называет его так, чем оно на самом деле является: Commit Charge/Commit Limit.
Эксперимент. Открываем таск менеджер на вкладке с «использованием пейджфайла», открываем PowerShell и копируем в него следующее (для систем, у которых Commit Limit ближе, чем на 3 Гб от Commit Charge можно в последней строчке уменьшить 3Gb, а лучше увеличить pagefile):
add-type -Namespace Win32 -Name Mapping -MemberDefinition @" [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr CreateFileMapping( IntPtr hFile, IntPtr lpFileMappingAttributes, uint flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, [MarshalAs(UnmanagedType.LPTStr)] string lpName); [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr MapViewOfFile( IntPtr hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, uint dwNumberOfBytesToMap); "@ $mapping = [Win32.Mapping]::CreateFileMapping(-1, 0, 2, 1, 0, $null) [Win32.Mapping]::MapViewOfFile($mapping, 4, 0, 0, 3Gb)
Это приводит к мгновенному повышению «использования свопфайла» на 3 гигабайта. Повторная вставка «использует» еще 3 Гб. Закрытие процесса мгновенно освобождает весь «занятый свопфайл». Самое интересное, что, как я уже говорил memory mapped файлы (в том числе и pagefile backed) являются shareable и не относятся к какому либо конкретному процессу, поэтому не учитываются в Commit Size никакого из процессов, с другой стороны pagefile backed секции используют (charged against) commit, потому что именно физическая память или пейджфайл, а не какой нибудь посторонний файл, будут использоваться для того, чтобы хранить данные, которые приложение захочет разместить в этой секции. С третьей стороны, после меппинга секции себе в адресное пространство, процесс не трогает ее — следовательно, физические страницы по этим адресам не впечатываются и никаких изменений в Working Set процесса не происходит.
Строго говоря, пейджфайл действительно «используется» — в нем резервируется место (не конкретное положение, а именно место, как размер), но при этом реальная страница, для которой это место было зарезервировано может находиться в физической памяти, на диске или И ТАМ И ТАМ одновременно. Вот такая вот циферка, признайтесь честно, сколько раз глядя на «Page File usage» в Task Manager-е Вы действительно понимали, что она означает.
Что же до Processes таба — там все еще по дефолту показывается Memory (Private Working Set) и несмотря на то, что он называется совершенно правильно и не должен вызывать недоразумений у знающих людей — проблема в том, что подавляющее большинство людей, которые смотрят на эти цифры совершенно не понимают, что они означают. Простой эксперимент: запускаем утилилиту RamMap (советую скачать весь комплект), запускаете Task Manager со списком процессов. В RamMap выбираете в меню Empty->Empty Working Sets и смотрите на то, что происходит с памятью процессов.
Если кого-то все еще раздражают циферки в Task Manager-е, можете поместить следующий код в профайл павершелла:
add-type -Namespace Win32 -Name Psapi -MemberDefinition @" [DllImport("psapi", SetLastError=true)] public static extern bool EmptyWorkingSet(IntPtr hProcess); "@ filter Reset-WorkingSet { [Win32.Psapi]::EmptyWorkingSet($_.Handle) } sal trim Reset-WorkingSet
После чего станет возможно «оптимизировать» использование памяти одной командой, например для «оптимизации» памяти, занятой хромом: ps chrome | trim
Или вот «оптимизация» памяти всех процессов хрома, использующих больше 100 Мб физической памяти: ps chrome |? {$_.WS -gt 100Mb} | trim
Если хотя бы половина прочитавших отметет саму идею о подобной «оптимизации», как очевиднейший абсурд — можно будет сказать, что я не зря старался.
Кеш
В первую очередь отмечу, что кеш в Windows не блочный, а файловый. Это дает довольно много преимуществ, начиная от более простого поддержания когерентности кеша например при онлайн дефрагментации и простого механизма очистки кеша при удалении файла и заканчивая более консистентными механизмами его реализации (кеш контроллер реализован на основе механизма memory mapping-а), возможностью более интеллектуальных решений на основе более высокоуровневой информации о читаемых данных (к примеру интеллектуальный read-ahead для файлов открытых на последовательный доступ или возможность назначать приоритеты отдельным файловым хендлам).
В принципе из недостатков я могу назвать только значительно более сложную жизнь разработчиков файловых систем: слышали о том, что написание драйверов — это для психов? Так вот, написание драйверов файловых систем — для тех, кого даже психи считают психами.
Если же описывать работу кеша, то все предельно просто: когда файловая система запрашивает у кеш-менеджера какую нибудь часть файла, последний просто меппит часть этого файла в специальный «слот», описываемый структурой VACB (посмотреть все смепленные файлы можно из отладчика ядра с помощью расширения !filecache) после чего просто выполняет операцию копирования памяти (RtlCopyMemory). Происходт Page Fault, так как сразу после отображения файла в память все страницы невалидны и дальше система может либо найти необходимую страницу в одном из «свободных» списков либо выполнить операцию чтения.
Для того, чтобы понять рекурсию нужно понять рекурсию. Каким же образом выполняется операция чтения файла, необходимая для завершения операции чтения этого самого файла? Здесь опять все достаточно просто: пакет запроса на ввод/вывод (IRP) создается с флагом IRP_PAGING_IO и при получении такого пакета файловая система уже не обращается к кешу, а идет непосредственно к нижележащему дисковому устройству за данными. Все эти смепленные слоты идут в System Working Set и составляют ЧАСТЬ кеша.
Страница из лекции какого то токийского университета (эх, мне бы так):

На этом работа собственно кеш-менеджера заканчивается и начинается работа менедера памяти. Когда выше мы делали EmptyWorkingSet это не приводило ни к какой дисковой активности, но тем не менее, физическая память используемая процессом сокращалась (и все физические страницы действительно уходили из адресного пространства процесса делая его почти полностью невалидным). Так куда же она уходит после того, как отбирается у процесса? А уходит она, в зависимости от того, соответствует ли ее содержимое тому, что было прочитано с диска, в один из двух списков: Standby (начиная с Висты это не один список, а 8, о чем позже) или Modified:

Standby список таким образом — это свободная память, содержащая какие то данные с диска (в том числе возможно и pagefile-а).
Если Page Fault происходит по адресу, который спроецирован на часть файла, которая все еще есть в одном из этих списков — она просто возвращается обратно в рабочий набор процесса и впечатывается по искомому адресу (этот процесс называется softfault). Если нет — то, как и в случае со слотами кеш менеджера, выполняется PAGING_IO запрос (называется hardfault).
Modified список может содержать «грязные» страницы достаточно долго, но либо когда размер этого списка чрезмерно вырастает, либо по когда система видит недостаток свободной памяти, либо по таймеру, просыпается modified page writer thread и начинает частями сбрасывать этот список на диск, и перемещая страницы из modified списка в standby (ведь эти страницы опять содержат неизмененную копию данных с диска).
Немного о том, как страницы попадают из рабочих наборов в эти самые списки.
Попадают разными способами. Один уже рассмотрен: кто-то явно вызвал EmptyWorkingSet для процесса. Это происходит нечасто. Обычно же это случается при помощи так называемого тримминга (trimming).
Когда система приближается к одному из установленных лимитов на свободную память, она начинает эту память освобождать. Во первых система находит процессы, максимально превысившие свой лимит на размер рабочего набора. Для этих процессов запускается процесс «старения» страниц (aging), для определения какие из страниц меньше всего используются. После этого, самые старые страницы «подрезаются» в standby или modified список.
Из той же лекции:

На диаграмме видно еще Delete page, которая происходит когда приложение просто освобождает выделенную память — с содержимым этих страниц попросту уже ничего нельзя делать, потому как по всем правилом оно не валидно и даже если новая память будет выделена по тому же адресу — это должна быть новая память.
Pagefile
Наконец-то я добрался до всеми любимого своп-файла. Драконы здесь водятся просто табунами. Ну что ж, попробуем их вывести.
Миф: Для повышения производительности нужно уменьшать количество обращений к пейджфайлу.
На самом деле: Для повышения производительности нужно уменьшать количество обращений К ДИСКУ. Пейджфайл является почти таким же файлом как и остальные.
Миф: Винда использует пейджфайл, даже если свободной памяти еще завались.
На самом деле: В пейджфайл страница может попасть только из modified списка. В modified список — при подрезке редко используемых страниц у разросшихся приложений. После того, как страница сброшена, она остается в standby списке и перечитываться не будет. Память никогда не берется из standby списка, если еще есть free или zeroed (то есть кешированные данные никогда не выбрасываются, если еще есть страницы вообще без данных). Standby список имеет 8 уровней приоритета (которыми до некоторой степени может управлять как само приложение, так и Superfetch, осуществляющий динамическое управление приоритетами страниц на основе анализа реального использования файлов/страниц), если не остается вообще никакого выбора — винда первым делом выбрасывает кеш самого низкого приоритета.
Вот пример получасовой работы в обычном режиме:

Прошу отметить, что файлы отсортированы по названию и между kernel.etl и «Program Files» должен был бы быть pagefile.sys.
Миф: Но винда сама признается, что использует пейджфайл.
На самом деле: Рассмотрено выше. Чаще всего речь идет о Commit Charge, который можно назвать «использованием», но совершенно не в том, смысле в котором это обычно принято понимать. В большинстве случаев при отсутсвии необходимости, приватные страницы (подлежащие сбросу в пейджфайл) будут сидеть в modified списке (даже если попадут туда) практически неограниченно долго.
Миф: Отключение пейджфайла улучшает производительность системы.
На самом деле: В редких случаях, приложение, которое злоупотребляет памятью и не понижает приоритет страниц для своих потоков может привести к понижению отзывчивости, однако в подавляющем большинстве случаев производительность только повышается за счет выгрузки неиспользуемого «хлама» в чулан и использовании освободившегося места для хранения более актуальных данных (тем самым снижая общее количество обращений к диску).
Приоритеты памяти и ввода/вывода
Чем ниже I/O приоритет потока, выполняющего запись/чтение, тем меньше он мешает нормальной работе (в целом, как и случае с приоритетами потоков используется round robin алгоритм для операций, ожидающих с наивысшим приоритетом, при этом небольшая часть пропускной способности отдается операциям с низким приоритетом в целях борьбы со starvation — на русский переводится не совсем адекватно). Запустите встроенный дефрагментатор. Resource Monitor покажет 100% загрузку диска, однако система при этом если и теряет в отзывчивости, то на глаз я этого заметить не могу.
Про приоритеты памяти я уже упоминал. Независимо от i/o приоритета, существует page приоритет (Page Priority). Каждая физическая страница в системе (а если точнее, то данные, которые она содержит в каждый момент времени) имеет приоритет. Когда страница перемещается в Standby list она попадает в один из восьми отдельных списков, соответствующий ее приоритету. Данные с более высоким приоритетом не могут быть замещены данными с более низким.
Prefetch и Superfetch
Prefetch появился в XP и является способом ускорить запуск приложений. Вот утилита, позволяющая просмотреть содержимое pf файлов. Собственно говоря там находятся имена и смещения файлов, к которым соответствующее приложение обращается в первые несколько (кажется 10) секунд после запуска. Так как запуск зачастую сопровождается бешенным чтением данных с диска, то упорядочивание этих чтений уменьшает количество head seek-ов и тем самым ускоряет запуск. Работает из службы SysMain. Помимо префетча и суперфетча этот сервис отвечает еще за ReadyBoot (который помогал ускорять загрузку в одном из предыдущих постов), ReadyBoost (штука довольно бесполезная на системах с большим количеством ОЗУ, но очень полезная на low-end системах) и ReadyDrive (это такой ReadyBoost, но с гарантией, что «флешка» не будет выниматься между перезагрузками — позволяет сильно ускорить основные операции не прибегая к полной замене HDD на SSD — используя так называемые гибридные или H-HDD: скорость SSD по цене HDD).
С суперфетчем все несколько сложнее. Мало кто понимает его назначение, но каждый норовит обвинить этот сервис в использовании памяти во время Idle и в чтении «чего то» с диска. Для начала чтение:

Осуществляется с background приоритетом и практически незаметно при использовании. Если раздражает мигание светодиода — его можно заклеить изолентой. Более того, это чтение загружает кеш нужными данными (можно посмотреть на увеличение standby после загрузки/просыпания даже при том, что пользователь не делает никаких активных операций с диском). Эти данные загруженные сейчас с низким приоритетом и с хорошим упорядочением, потом сохранят несколько секунд и бешенное перемещение головок при старте какого нибудь приложения. Речь идет чаще всего о загрузке данных на 6-7 уровни приоритетов (да, I/O операции с низким приоритетом используются для загрузки страниц в Standby списки высоких уровней), которые могут быть вытеснены только в самом крайнем случае, когда памяти не хватает уже ни на что.
Память этот сервис использует для хранения истории обращений к диску/страницам памяти. Периодически (в Idle) он выделяет больше для того, чтобы произвести анализ этих данных и построить долгосрочный план действий (а также бутплан на следующую загрузку). Кроме того, он производит динамическое перераспределение приоритетов страниц. Вот например, что происходит на моей домашней машине, на которой я частенько пользуюсь дотнетом (в виде powershell):

Там в списке предствлены страницы с приоритетами от 1 до 6 (7 — это специальный приоритет, который никогда не меняется). А вот свежеустановленная виртуальная машина, на которой powershell был запущен раз в жизни за несколько секунд до этого:

Суперфетч еще не знает, что делать с этим файлом, поэтому все страницы попали на уровнь, соответствующий приоритету потока, который загружал данный файл. А вот так выглядит кеш того же файла на той же виртуалке после нескольких часов бездействия (просто свернутое в таскбаре окно):

Видно, что все страницы понизили свой приоритет.
Копирование больших файлов
Здесь все просто. Запускаем копирование исошника. Видим, что копирование производится с нормальным I/O приоритетом:

Page priority потока я не сохранил, но могу с большой долей уверенности сказать, что там тоже «норма» — то есть 5. Значит ли это, что копирование файлов рано или поздно повыкидывает из кеша всех остальных? Проверим:

Все закешировалось с приоритетом 2 даже несмотря на то, что чтение осуществлялось с приоритетом 5. В чем же дело? А вот в чем:

Для операций копирования используется флаг FILE_FLAG_SEQUENTIAL_SCAN, который приводит к понижению приоритета кеша по сравнению с базовым, к использованию только одного VACB для кеширования и к более агрессивному read-ahead-у.
μTorrent
Это, пожалуй, самая забавная часть. При очевидно высоких навыках программирования, авторы либо не читали, либо не поняли вот этот документ. Вот что происходит с дефолтными настройками:

Поток 4600 открывает для раздачи файл с отключенным системным кешем. Причем все выполняется без снижения приоритета:

Я не пользуюсь торрентами и файлов для раздачи у меня не оказалось, да и каналы на отдачу в местных широтах довольно узкие, так что мне пришлось написать «эмулятор» мюторрента. Для начала «старая версия», не выключавшая кеширование (написал на сишарпе, потому что не у всех может оказаться установленная Visual Studio):
using System; using System.IO; using System.Threading; namespace abuseio { class Program { static void Main(string[] args) { for (var i = 0; i < 10; i++) { (new Thread(() => { var file = File.OpenRead(args[0]); var chunkSize = 16384; var chunks = (int)(file.Length / chunkSize); var rnd = new Random(); var buffer = new byte[chunkSize]; while (true) { long index = rnd.Next(chunks); file.Seek(index * chunkSize, SeekOrigin.Begin); file.Read(buffer, 0, chunkSize); } })).Start(); } } } }
В несколько потоков читаем случайные места предоставленного файла (лучше использовать что нибудь большое). Несколько минут после старта:

Видим довольно много кешированных файлов. Несколько часов после старта:

В памяти остался только хром (в основном потому, что я им активно пользовался) и ругие Active страницы файлов, означающие недавнее использование. В Standby остались в основном те самые priority 7 страницы:
Вот, к примеру, небольшая часть shell32.dll:

Ну и «общий вид»:

Почти все закешировалось на пятый уровень, вытеснив всех, кто находился там до этого. Более того, из-за частых обращений к «раздаваемым» файлам, суперфетч динамически повысил приоритет частей этих файлов и вытеснил часть кеша даже оттуда. Короче, все плохо.
Вот «эмулятор» нового мюторрента:
using System; using System.Runtime.InteropServices; using System.IO; using System.Threading; using Microsoft.Win32.SafeHandles; namespace abuseio { class Program { [Flags] enum FileAttributes : uint { Readonly = 0x00000001, Hidden = 0x00000002, System = 0x00000004, Directory = 0x00000010, Archive = 0x00000020, Device = 0x00000040, Normal = 0x00000080, Temporary = 0x00000100, SparseFile = 0x00000200, ReparsePoint = 0x00000400, Compressed = 0x00000800, Offline = 0x00001000, NotContentIndexed = 0x00002000, Encrypted = 0x00004000, Write_Through = 0x80000000, Overlapped = 0x40000000, NoBuffering = 0x20000000, RandomAccess = 0x10000000, SequentialScan = 0x08000000, DeleteOnClose = 0x04000000, BackupSemantics = 0x02000000, PosixSemantics = 0x01000000, OpenReparsePoint = 0x00200000, OpenNoRecall = 0x00100000, FirstPipeInstance = 0x00080000 } [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern SafeFileHandle CreateFile( string fileName, [MarshalAs(UnmanagedType.U4)] FileAccess fileAccess, [MarshalAs(UnmanagedType.U4)] FileShare fileShare, IntPtr securityAttributes, [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, [MarshalAs(UnmanagedType.U4)] FileAttributes flags, IntPtr template); static void Main(string[] args) { for (var i = 0; i < 10; i++) { (new Thread(() => { var fileHandle = CreateFile(args[0], FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.NoBuffering, IntPtr.Zero); var file = new FileStream(fileHandle, FileAccess.Read); var chunkSize = 16384; var chunks = (int)(file.Length / chunkSize); var rnd = new Random(); var buffer = new byte[chunkSize]; while (true) { long index = rnd.Next(chunks); file.Seek(index * chunkSize, SeekOrigin.Begin); file.Read(buffer, 0, chunkSize); } })).Start(); } } } }
Отключаем буферизацию и делаем все «кеширование» сами (кавычки, потому что сложно назвать нормальным решение управлять физической памятью из пользовательского режима):
1. За счет все еще неоправданно высокого приоритета ввода-вывода это мешает нормальной работе с компьютером
2. Отсутствует глобальный кеш. Если кому то еще надо обратиться к файлу — кешированная информация продублируется еще и в системном кеше
3. Существуют проблемы с размером доступного кеша
4. Last but not least, это не особо помогает. Так как если мюторрент будет выделять гигабайты памяти, он окажется первым кандидатом на тримминг и весь его «кеш» уйдет в пейджфайл.
Самые любознательные к этому моменту уже прочитали документ, на который я ссылался выше и знают правильное решение. Вот его реализация на шарпе:
using System; using System.Runtime.InteropServices; using System.IO; using System.Threading; using Microsoft.Win32.SafeHandles; namespace abuseio { class Program { [DllImport("kernel32.dll")] static extern IntPtr GetCurrentThread(); enum ThreadPriority { THREAD_MODE_BACKGROUND_BEGIN = 0x00010000, THREAD_MODE_BACKGROUND_END = 0x00020000, THREAD_PRIORITY_ABOVE_NORMAL = 1, THREAD_PRIORITY_BELOW_NORMAL = -1, THREAD_PRIORITY_HIGHEST = 2, THREAD_PRIORITY_IDLE = -15, THREAD_PRIORITY_LOWEST = -2, THREAD_PRIORITY_NORMAL = 0, THREAD_PRIORITY_TIME_CRITICAL = 15 } [DllImport("kernel32.dll")] static extern bool SetThreadPriority(IntPtr hThread, ThreadPriority nPriority); static void Main(string[] args) { for (var i = 0; i < 10; i++) { (new Thread(() => { SetThreadPriority(GetCurrentThread(), ThreadPriority.THREAD_MODE_BACKGROUND_BEGIN); var file = File.OpenRead(args[0]); var chunkSize = 16384; var chunks = (int)(file.Length / chunkSize); var rnd = new Random(); var buffer = new byte[chunkSize]; while (true) { long index = rnd.Next(chunks); file.Seek(index * chunkSize, SeekOrigin.Begin); file.Read(buffer, 0, chunkSize); } })).Start(); } } } }
Понижаются все, известные человечеству, приоритеты потока:

Несколько часов активной «раздачи» не приводят к вытеснению обычного кеша, но и от самого кеширования мы при этом не отказываемся:

Все красиво сложено на самом низком уровне приоритета


Теперь о том, как с жить с тем мюторрентом, который есть. Все проще, чем кажется и делается на 1-2-3.
1. Выключаем дурацкий кеш самого мюторрента и включаем кеш винды:

2. Импортируем в реестр следующий файл:
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\utorrent.exe] [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\utorrent.exe\PerfOptions] "IoPriority"=dword:00000000 "PagePriority"=dword:00000001
3. Перезапускаем мюторрент



Можно оставлять на ночь.
Пара слов о swappiness
Не знаю, я бы не назвал это «правильной работой с памятью». Вместо принятия решений на основе реального использования (aging/trimming, приоритеты в том числе динамические и пр) — какой то совершенно оторванный от жизни параметр. В общем то не только мне swappiness кажется не сильно хорошей идеей, сам автор low latency патча, реализовавшего swappiness говорит следующее:
Decrease /proc/sys/vm/swappiness? Swapout is good. It frees up unused memory. I run my desktop machines at swappiness=100.
И здесь я с ним совершенно согласен. Если приложение последний раз использовало страницу неделю назад, то можно достаточно безопасно выгрузить содержимое на диск, а освободившееся место использовать для кеширования чего нибудь полезного. Другое дело, что надо бы более интеллектуально подходить к поиску страниц, которые стоит выгрузить.
Разбирать весь флейм по ссылке ни желания ни сил у меня уже нет — читайте сами.