Потокові сховища і організація пам'яті

Зміст


  1. Введення
  2. Організація віртуальної пам'яті в Windows
  3. Купи і менеджери куп
  4. Групи функцій роботи з пам'яттю
  5. Потокові сховища
  6. Бібліотека потокових сховищ
  7. Приклад використання бібліотеки потокових сховищ

1. Введення

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


У статті наводяться тільки основні принципи роботи з пам'яттю в системі Windows 32.
Для докладного вивчення всіх тонкощів цього складного процесу читач може
звернутися до спеціальної літератури. Особливо хочеться відзначити книгу:
Дж.Ріхтер, "Windows для професіоналів".

2. Організація віртуальної пам'яті в Windows

Як відомо, Windows 32 – трідцатідвуразрядная операційна система (число 32 як раз це
і позначає). Перш за все, з цього випливає, що запущена програма може адресувати
лінійне адресний простір розміром 2 ^ 32 байт = 4 ГБ, при цьому адресація здійснюється за допомогою
трідцатідвуразрядних регістрів-покажчиків. Кожен запущений в системі процес має
своїм власним адресним простором, кожне з яких не перетинається з адресними
просторами інших процесів. Розподіл системних областей в адресному просторі систем Windows 95/98 і
Windows NT-різному.


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


Механізм виділення пам'яті в Windows складається з двох фаз. Перша фаза виділення пам'яті
полягає в резервуванні (захоплення) ділянки необхідного розміру в адресному просторі
процесу. При цьому не виділяється ні байта реальної пам'яті (не рахуючи системних структур
ядра). Ви можете спокійно зарезервувати ділянку адресного простору розміром
100 мегабайт, і це нічого не коштуватиме системі. Ви можете вказати які адреси ви хотіли б
зайняти, а можете надати вибір ділянки необхідного розміру самій системі. Варто відзначити,
що адресний простір резервується з гранулярність 64 кБ. Це означає, що незважаючи на
вказані вами адреси, базовий адресу реально зарезервованого ділянки пам'яті буде кратний
64 кБ. При резервування ділянки в адресному просторі можна вказати бажаний атрибут,
який регулює доступ до цієї пам'яті: запис даних, читання даних, виконання коду або
комбінацію цих ознак. Порушення правил доступу до пам'яті приводить до генерації системою
винятки.


Друга фаза виділення пам'яті в Windows – це виділення реальної, фізичної пам'яті в
зарезервований ділянку віртуального адресного простору. На цьому етапі системою
виділяється реальна пам'ять, з усіма витікаючими з цього наслідками. Виділення
реальної пам'яті також гранулярному. Мінімальний блок реальної пам'яті, яким оперує
система, і який можна виділити, називається сторінкою пам'яті. Розмір сторінки
залежить від типу операційної системи і становить для Windows 95/98 – 4 кБ, а для
Windows NT – 8 кб.
Гранулярність при резервуванні ділянки пам'яті і при виділенні реальної пам'яті покликана
полегшити навантаження на ядро системи.


Виділення реальної пам'яті відбувається посторінково, при цьому існує можливість виділення
довільної кількості сторінок у довільні (кратні розміром сторінки) адреси заздалегідь
зарезервованого ділянки адресного простору процесу. Кожній сторінці може бути
призначений свій власний атрибут доступу.
Бажано вказувати той же самий атрибут, що має зарезервований
ділянка адресного простору в якому відбувається виділення сторінки реальної пам'яті.


Важливим моментом у механізмі виділення пам'яті є механізм динамічної вивантаження і
завантаження сторінок пам'яті. Справді, сучасний комп'ютер має оперативну пам'ять
об'ємом 16-256 МБ, а для спільної роботи декількох програм необхідно значно більше.
Windows, як і більшість сучасних операційних систем, вивантажує сторінки пам'яті,
до яких давно не було звернень, на жорсткий диск в так званий своп-файл. При цьому
розмір реальної пам'яті, доступної для програм, ставати рівним сумарним обсягом
оперативної пам'яті та своп-файлу. По можливості, система намагається тримати всі сторінки в
оперативної пам'яті, проте коли сумарний розмір виділених всіма процесами сторінок перевищує
її розмір, система вивантажує сторінки з давнім доступом на диск, а на їх місці виділяє
нові сторінки. Якщо ж вивантажена на диск сторінка затребуется володіє нею процесом,
система звільнить для неї місце в оперативній пам'яті шляхом вивантаження рідко використовуваної
сторінки, завантажить затребувану сторінку на її місце і поверне управління процесу.

Весь механізм динамічного завантаження і вивантаження сторінок абсолюдно прозорий для процесів,
а реалізація цього механізму повністю забезпечується операційною системою. Процес може
абсолюдно нічого не знати про сущесвованіі такого механізму – він звертається до своїх осередків
пам'яті, а система автоматично вивантажує рідко вживані і завантажує необхідні
сторінки; процес може лише регулювати деякі нюанси роботи цього механізму.


На підставі згаданого вище можна зробити наступні висновки:

3. Купи і менеджери куп

Алгоритми сучасних програм використовують механізми виділення і звільнення пам'яті
дуже інтенсивно. Рядки, динамічні масиви, об'єкти, структури, буфера – виділення
і звільнення цих елементів відбувається дуже часто, при цьому виявляється, що всі ці
елементи мають невеликий розмір.


Виділення у системи великої кількості об'єктів невеликого розміру виявляється
неефективним з наступних причин.

Вирішення цієї проблеми організується таким чином: у операційної системи виділяється
досить велику ділянку пам'яті, а вже з нього для прикладної програми "нарізаються"
невеликі ділянки. Така організація називається купою, а механізм, який стежить за
виділенням і звільненням ділянок пам'яті називається менеджером купи. Купа дозволяє
вирішити як проблему втрати продуктивності – менеджер купи може функціонувати
на рівні прикладної програми, так і проблему гранулярності – менеджер запитує
у системи одну велику ділянку пам'яті.


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


Інженери компанії Borland по видимому не довіряють інженерам компанії Microsoft, тому
кожна програма на Delphi має свою власну реалізацію менеджера купи, яка визначена
в системних модулях. Таке рішення аргументується інженерами Borland тим, що стандартний
менеджер купи Windows не забезпечує достатньо ефективну роботу з пам'яттю. Звичайно, як
і будь-яка Windows-програма, програма на Delphi має стандартну купу за замовчуванням, яку
створює для неї система, проте функції New, Release, GetMem, FreeMem і деякі інші
оперують з власною реалізацією менеджера куп. Менеджер купи Delphi резервує блоки
адресного простору розміром 1 Мб, а виділяє блоки реальної пам'яті розміром 16 Кб.
Також ви можете написати і встановити свою реалізацію менеджера куп, якщо не довіряєте ні
інженерам Borland, ні інженерам Microsoft – для цього є всі необхідні функції.


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

4. Групи функцій роботи з пам'яттю

У розділі описуються групи базових функцій роботи з пам'яттю, які доступні для програміста
на Delphi. Включені опису як API-фукнції, так і Delphi-функцій.

Delphi-функції

New(), Dispose()
Фунції працюють з менеджером купи Delphi. Забезпечують типізовані виділення і звільнення
пам'яті. Використовуються для динамічної роботи зі структурами.

GetMem(), FreeMem()
Фунції працюють з менеджером купи Delphi. Забезпечують нетипізовані виділення і звільнення
пам'яті. Використовуються для динамічної роботи з невеликими бінарними блоками пам'яті
(Буфера, блоки).

API-функції

HeapCreate(), HeapDestroy(), …
Функції роботи із стандартним менеджером купи Windows. Використовуються для створення і знищення
куп, виділення і звільнення великої кількості нетипізовані блоків пам'яті малого розміру.
Функції дозволяють працювати зі стандартною купою за замовчуванням, яку створює операційна
система для кожного процесу.

LocalAlloc (), LocalFree (), … , GlobalAlloc (), GlobalFree (), …
Так як в Windows 32 немає поділу на глобальні та локальні купи, ці дві групи функцій
ідентичні. Функції працюють зі стандартною купою за замовчуванням, яку створює операційна
система для кожного процесу. Функції морально застаріли і Microsoft не рекомендує їх використовувати
без крайньої необхідності. Однак ці функції можуть стати в нагоді, наприклад, при роботі з буфером
обміну.

VirtualAlloc(), VirtualFree(), …
"Основоположні" функції виділення пам'яті в Windows. Використовуються як для резервування
адресного простору, так і для виділення сторінок реальної пам'яті в заздалегідь зарезервований
ділянка адресного простору. Дозволяють виконати обидві фази за один виклик функції. Використовуються
для резервування і виділення великих ділянок пам'яті.

5. Потокові сховища

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


У таких випадках використовують динамічні сховища. У Delphi такими сховищами є
динамічні масиви, об'єкт TMemoryStream, динамічний перерозподіл пам'яті. Всі ці
сховища працюють на одному і тому ж принципі: під зберігання даних виділяється блок пам'яті і
дані, що надходять послідовно записуються в цей блок, коли цей блок заповнюється
повністю, він перерозподіляється з деяким запасом (розмір його збільшується, а старі
дані залишаються). Після того, як кожен новий блок заповнюється повністю він знову
перерозподіляється у міру надходження нових даних.


Перерозподіл пам'яті займає багато ресурсів саме по собі, а так як воно виконується
ще й у купі, то можна вважати його подвійно неефективним, особливо якщо розміри
перерозподілених блоків стають дуже великими. Динамічні масиви і динамічне
перерозподіл пам'яті використовують менеджер купи Delphi, а об'єкт TMemoryStream використовує
стандартний менеджер купи Windows.


Для вирішення проблеми накопичення даних автором були розроблені два об'єкти накопичення
даних, засновані на двох різних принципах і мають різні характеристики.

TLinearStorage

Якщо розмір буфера великий, але максимально можливий розмір відомий (наприклад якщо є
обмеження на обсяг записуваних даних), то можна поступити наступним чином.
У адресному просторі процесу резервується блок пам'яті необхідного розміру, нагадаємо,
що така операція не займає ресурсів у системи. Потім у міру необхідності в цьому
зарезервованому ділянці пам'яті, у міру необхідності, послідовно виділяються
сторінки реальної пам'яті. Перевагою такого методу є лінійне розташування
осередків у пам'яті один за одним, тобто до такого сховища можна звертатися як до звичайного
лінійному масиву. Недоліком – необхідність вказівки максимального розміру.

TSectionStorage

Якщо розмір буфера заздалегідь невідомий (обмежений лише розміром доступної реальної пам'яті),
то попереднє рішення не підходить. У цьому випадку можна запропонувати інше рішення.
У міру необхідності, у системи можна запитувати ділянки пам'яті однакового розміру і
записувати в них дані, що надходять. При цьому буфер не буде мати лінійного адресного
простору, а буде складатися з одноразмерних "клаптів" пам'яті – для обчислювального
алгоритму така організація буфера пожет стати серйозною перешкодою. Перевагою ж такого
рішення є відсутність необхідності спочатку вказувати який або розмір буфера.


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

Додатково, кожне сховище має функції запису і читання в стандартні потоки Delphi
з упаковкою. Упаковка проводиться за стандартними алгоритмами бібліотеки ZLIB.

6. Бібліотека потокових сховищ

TBaseStorage – базовий клас

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

Item[] – Отримання покажчика на зазначений елемент за його індексом.
ItemSize – Запит розміру зберігається елемента.
Count – Запит і установка числа збережених елементів.

Clear – Очищення сховища, встановлення його розміру в нуль.
AddItems, GetItems, SetItems – Додавання, запит і установка блоку елементів.
SaveStream, LoadStream – Запис та завантаження сховища в / з потоку. Параметр Compression
в цих процедурах означає наступне 0 – компресія не проводиться, і сховище записується
в лінійному натуральному вигляді; 1 – найменший ступінь компресії; 9 – найвища ступінь компресії.
Число між 1 .. 9 – довільна ступінь компресії.


////////////////////////////////////////////////// //////////////////////////////
// TBaseStorage
/ / _____________________________________________________________________________
/ / Базовий клас для сховищ
////////////////////////////////////////////////// //////////////////////////////
type
TBaseStorage = class (TObject)
public
property Item [Ind: Cardinal]: Pointer read GetItem; default;
property ItemSize: Cardinal read FItemSize;
property Count: Cardinal read FCount write SetCount;
public
procedure Clear; virtual; abstract;
procedure AddItems (Items: Pointer; Count: Cardinal); virtual; abstract;
procedure SetItems (Items: Pointer; Index, Count: Cardinal); virtual; abstract;
procedure GetItems (Items: Pointer; Index, Count: Cardinal); virtual; abstract;
procedure SaveStream (Stream: TStream; Compression: Integer); virtual; abstract;
procedure LoadStream (Stream: TStream; Compression: Integer; Count: Cardinal);
virtual; abstract;
end;

Лінійне сховище

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

Capacity – Запит і установка максимальної місткості сховища. При установці
ємності сховища, всі раніше зберігалися дані губляться.
Memory – Запит покажчика на лінійний ділянку пам'яті, в якому зберігаються
дані, може бути використаний в обчислювальних алгоритмах.

Create – Конструктор, в якому необхідно вказати розмір зберігається елемента.


////////////////////////////////////////////////// //////////////////////////////
// TLinearStorage
/ / _____________________________________________________________________________
/ / Лінійне сховище
////////////////////////////////////////////////// //////////////////////////////
type
TLinearStorage = class (TBaseStorage)
public
property Capacity: Cardinal read FCapacity write SetCapacity;
property Memory: Pointer read FMemory;
public
procedure Clear; override;
procedure AddItems (Items: Pointer; Count: Cardinal); override;
procedure SetItems (Items: Pointer; Index, Count: Cardinal); override;
procedure GetItems (Items: Pointer; Index, Count: Cardinal); override;
procedure SaveStream (Stream: TStream; Compression: Integer); override;
procedure LoadStream (Stream: TStream; Compression: Integer; Count: Cardinal);
override;
public
constructor Create(AItemSize: Cardinal);
destructor Destroy; override;
end;

Секційні сховище

Секційні сховище зберігає дані в кусочно-лінійному буфері складається з ділянок
однакового розміру. Сховище не вимагає вказівки максимальної місткості, але натомість не
дозволяє звертатися до елементів як до масиву даних.

Block – Список покажчиків на блоки, з яких складається сховище.
BlockSize – Розмір блоків, вимірюваний в числі збережених елементів.

Create – Конструктор, в якому необхідно вказати розмір зберігається елемента в байтах
і розмір блоку зберігання.


////////////////////////////////////////////////// //////////////////////////////
// TSectionStorage
/ / _____________________________________________________________________________
/ / Секційне сховище
////////////////////////////////////////////////// //////////////////////////////
type
TSectionStorage = class (TBaseStorage)
public
property Blocks: TList read FBlocks;
property BlockSize: Cardinal read FBlockSize;
public
procedure Clear; override;
procedure AddItems (Items: Pointer; Count: Cardinal); override;
procedure SetItems (Items: Pointer; Index, Count: Cardinal); override;
procedure GetItems (Items: Pointer; Index, Count: Cardinal); override;
procedure SaveStream (Stream: TStream; Compression: Integer); override;
procedure LoadStream (Stream: TStream; Compression: Integer; Count: Cardinal);
override;
public
constructor Create (AItemSize: Cardinal; ABlockSize: Cardinal);
destructor Destroy; override;
end;

7. Приклад використання бібліотеки потокових сховищ

Перший приклад демонструє ефективність менеджера купи Delphi перед
стандатним менеджером купи Windows. На комп'ютерах, які були мені доступні, тест
показував більш ніж чотирикратне перевагу менеджера купи Delphi над менеджером
купи Windows.


3 кБ

Порівняльний тест двох менеджерів куп – Delphi і Windows

Наступний приклад містить вихідні тексти бібліотеки потокових сховищ і тест, що порівнює
два потокових сховища, а також об'єкти TMemoryStream і TFileStream. Тест містить
один параметр, який ви можете регулювати – число додаються об'єктів.
Збільшуйте цей параметр удвічі при кожному запуску тесту і спостерігайте за поведінкою
всіх чотирьох об'єктів, особливо об'єкта TMemoryStream. Поки масив даних міститься
в оперативній пам'яті, результати цього об'єкта будуть прекрасними, однак після того як
масив перестане поміщатися в ОЗУ, об'єкт починає різко здавати свої позиції,
а незабаром перестає працювати зовсім. Коли ж він працює на межі можливостей, він створює
перешкоди при виділенні пам'яті – саме через це тест бажано перезапускати.

Взагалі з об'єктом TMemoryStream пов'язані дивні, незрозумілі історії. Як-то раз автор мав
нещастя використовувати цей об'єкт в одній зі своїх програм для накопичення потоку даних
з модему. Через деякий час після запуску програма зависала сама і, крім того, підвішували
Windows NT. Аналіз за допомогою диспетчера завдань показав, що в процесі життєдіяльності
програми, вона займає все нові і нові ділянки пам'яті.

Пошук помилок ні до чого не привів, проте врешті-решт довелося звернути увагу на
дивності в поведінці об'єкта TMemoryStream. Довелося створити свій потік THeapStream
шляхом формальної заміни функцій сімейства Global … на функції GetMem, FreeMem, ReallocMem –
тобто заміною стандартного менеджера купи Windows на менеджер купи Delphi. Після цього
всі дивності при роботі програми зникли.


31 кБ

Вихідні тексти бібліотеки потокових сховищ і приклад

© Микола Мазуркін, 1999-2000.
E-Mail: mazurkin@mailru.com,
mazurkin@chat.ru
WWW: http://mazurkin.virtualave.net

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*