Стеки і купи

Описані вище засоби керування пам'яттю, засновані на виділенні регіонів, являють собою могутній і красивий інструмент для роботи з великими масивами пам'яті. Однак у практиці програмування частіше зустрічаються прозові завдання, пов'язані з використанням невеликих ділянок пам'яті: виклик функцій з передачею їм параметрів і виділенням локальних змінних, створення та звільнення змінних в динамічній пам'яті і т.п. Але зате ці дрібні операції можуть виконуватися дуже багато разів. Використовувати виділення окремого регіону заради того, щоб отримати 10 - 20 байт пам'яті, це приблизно те ж саме, що застосовувати ракетну зброю в боротьбі з тарганами. Нагадаємо, що мінімальний розмір регіону дорівнює 4 Кб.

Програмісти звикли, що для розміщення параметрів і локальних змінних функцій використовується стек, а виділення ділянок динамічної пам'яті відбувається з «купи» - спеціально призначеній для цього області пам'яті, керованої виконуючою системою мови програмування. Обидва цих механізму отримують підтримку з боку Windows.

При створенні нової нитки вона отримує свій власний стек, розмір якого, якщо він не вказаний явно, приймається рівним 1 Мб. Ця величина може здатися явно надлишкової для більшості програм. Чи варто системі так кидатися пам'яттю?

Насправді, виділяється 1 Мб резервованої пам'яті. Реального витрачання ресурсів пам'яті при цьому не відбувається, хіба що від 2 Гб адресного простору процесу відщипують порівняно невеликий шматочок. Розмір «справжньої» пам'яті, закріпленої за стеком в сторінковому файлі, дорівнює спочатку двом сторінкам, розміщеним в самому кінці зарезервованого регіону, як показано на рис. 5-6.

 

Рис. 1‑25

 

Стек за своїм звичаєм зростає в напрямку убування адрес, тому, починаючи зростання з самого старшого адреси, він поступово заповнює одну сторінку, а потім переходить до другої, з меншими адресами. Ця сторінка виділяється з атрибутом PAGE_GUARD, який, нагадаємо, означає, що при першому зверненні до сторінки генерується переривання, що оповіщає про це систему. Для Windows це сигнал, що пора передати стеку ще одну сторінку пам'яті про запас, причому ця сторінка знову отримує атрибут PAGE_GUARD, щоб потім оповістити систему, що стек знову виріс. Так може тривати до тих пір, поки пам'ять не буде передана всім сторінкам регіону стека, крім самої молодшої. Ця сторінка завжди залишається зарезервованої і спроба запису на неї розглядається як переповнення стека. Таким чином, молодша сторінка регіону стека служить бар'єром, що не дозволяє стеку вийти за початок регіону.

А чому необхідний такий бар'єр?

Для розміщення динамічних змінних зручно використовувати об'єкт «купа» (heap). Windows надає кожному процесу власну купу, хендл якої можна отримати викликом функції GetProcessHeap. Після цього нитки процесу можуть запитувати блоки пам'яті з купи, викликаючи функцію HeapAlloc. Параметри цієї функції включають в себе хендл купи, розмір запитуваного блоку і деякі прапори. Після закінчення потреби у виділеному блоці він може бути повернений в купу викликом функції HeapFree.

Невелика проблема виникає в зв'язку з тим, що до однієї і тієї ж купі можуть звертатися різні нитки одного процесу. Не виключено їх одночасне звернення до функцій, що працюють з купою. Виникає проблема взаємного виключення, і Windows вирішує її, використовуючи вбудований мьютекс. Це називається сериализацией доступу до купи, тобто послідовним виконанням запитів (від «serial» - послідовний). Для програми користувача цей мьютекс не видно і можна не звертати на нього уваги. Проте в тому випадку, якщо програміст впевнений, що нитки не можуть перешкодити один одному, він може відключити сериализацию, вказавши відповідний прапор або при відкритті купи, або при запиті блоку. Це декілька підвищує продуктивність.

У деяких випадках виявляється вигідно використовувати не одну, а кілька куп для одного і того ж процесу. Можна назвати, принаймні, дві подібних ситуації.

· Якщо з купою працюють дві або більше ниток процесу, то виділення окремої купи для кожної нитки дозволяє обійтися без серіалізациі, підвищивши таким чином продуктивність.

· Якщо програма запитує з купи блоки різного розміру, то неминуче таке знайоме нам явище, як фрагментація пам'яті. В даному випадку вона призведе до зайвого зростанню купи і до уповільнення роботи. Іноді вдається уникнути фрагментації, виділивши окрему купу для кожного використовуваного розміру блоків. Наприклад, з однієї купи будуть запитуватися блоки тільки розміром 105 байт, а з іншого - розміром 72 байта. При виділенні блоків одного розміру фрагментації не виникає.

Windows дозволяє процесу створити будь-яку кількість додаткових куп. Для цього потрібно викликати функцію HeapCreate, передавши їй два числа: початковий розмір фізичної пам'яті, переданої купі при створенні, і максимальний розмір купи, задаючий розмір регіону зарезервованої пам'яті для купи. Якщо максимальний розмір заданий рівним 0, то купа може рости необмежено.

Коли додаткова купа перестає бути потрібна, можна звільнити займану пам'ять, передавши хендл купи функції HeapDestroy.