Розробка ресурсномістких додатків в середовищі Visual C + +, Різне, Програмування, статті

Анотація.


Стаття познайомить розробників прикладного програмного забезпечення з завданнями, які ставить перед ним масове впровадження багатоядерних 64-бітних обчислювальних систем, що знаменують революційне збільшення обчислювальної потужності, доступне пересічному користувачеві. Будуть розглянуті питання ефективного використання апаратних ресурсів для вирішення повсякденних прикладних задач в рамках операційної системи Windows x64.


Інформація читачеві.


За замовчуванням у статті під операційною системою буде розумітися Windows. Під 64-бітними системами слід розуміти архітектуру x86-64 (AMD64). Під середовищем розробки – Visual Studio 2005/2008. Завантажити демонстраційний приклад, про який йтиметься в статті можна за адресою: http://www.Viva64.com/articles/testspeedexp.zip.


Введення.


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


Можливість розпаралелювання і великий обсяг оперативної пам’яті з одного боку полегшує розробку ресурсоємних додатків, але з іншого вимагає від програміста більшої кваліфікації і знань в області паралельного програмування. На жаль, зараз цієї кваліфікацією і знаннями володіють далеко не всі розробники. Але не тому, що вони погані розробники, а лише в силу того, що їм не доводилося стикатися з подібними завданнями. Це не дивно, так як створенням паралельних систем обробки інформації до не недавнього часу займалися в основному в наукових інститутах, вирішуючи завдання моделювання і прогнозування. Паралельні обчислювальні комплекси з великим обсягом пам’яті застосовувалися і в прикладних цілях на підприємствах, в банках і так далі, але до недавнього часу вони були дуже дороги і не багато розробників мали можливість познайомитися з особливостями розробки програмного забезпечення для таких систем.


Авторам статті довелося брати участь у розробці ресурсномістких програмних продуктів пов’язаних з візуалізацією і моделюванням фізичних процесів і на собі відчути всю специфічність розробки, тестування і налагодження систем подібного типу. Під ресурсоємним програмним забезпеченням розуміється програмний код, ефективно використовує можливості багатопроцесорних систем і великий обсяг пам’яті (від гігабайти і більше). Тому, хочеться по можливості забезпечити розробників знаннями, які можуть стати в нагоді їм найближчим часом при освоєнні сучасних паралельних 64-бітових систем.


Справедливо зазначимо, що питання пов’язані, з паралельним програмуванням вже давно й докладно опрацьовано і знайшли своє відображення в багатьох книгах, статтях і навчальних курсах. У зв’язку з цим акцент у статті буде зміщений у бік організаційних і практичних питань розробки високопродуктивних додатків і використання 64-бітових технологій.


Говорячи про 64-бітних системах, ми будемо вважати, що вони використовують модель даних LLP64 (див. таблицю N1). Саме така модель даних використовується в 64-бітових версіях операційної системи Windows. Але наведена інформація може бути корисною і при роботі з системами з відмінною від LLP64 моделлю даних.


1. Сміливо використовуйте паралельність і 64-битность.


Усвідомлюючи всю консервативність в розробці великих програмних систем, тим не менше, хочеться порекомендувати використовувати ті можливості, які представляють багатоядерні 64-бітові процесори. Це може стати великою конкурентною перевагою перед аналогічними системами, а так же стати хорошим новинним приводом в рекламних компаніях.


Немає сенсу відкладати 64-битность і паралельність на потім, так як їх освоєння неминуче. Можна безболісно пропустити повсюдне захоплення новою мовою програмування або не оптимізувати програму під технологію MMX. Але не можна піти від зростання обсягу оброблюваних даних і уповільнення швидкості росту тактовою частоти. Давайте зупинимося на цьому твердженні більш докладно.


Паралелізм стає основою зростання продуктивності, що пов’язано з уповільненням темпів зростання тактової частоти сучасних мікропроцесорів. У той час як кількість транзисторів на кристалі неухильно зростає, з 2005 року намітився різкий спад темпів зростання тактової частоти (дивися малюнок 1). Цікавою роботою по цій темі є стаття “The Free Lunch Is Over. A Fundamental Turn Toward Concurrency in Software”.


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


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


Використання 64-бітових технологій, хоча й не виглядає настільки переконливо порівняно з паралельною, тим не менш, також відкриває багато нових можливостей. По-перше, це безкоштовний приріст продуктивності на 5-15%. По-друге, велика адресний простір вирішує проблему фрагментації оперативної пам’яті при роботі з великими об’єктами. Виконання цього завдання є головним болем для багатьох розробників, чиї програми аварійно завершуються через брак пам’яті після декількох годин роботи. По-третє, це можливість легко працювати з масивами даних в кілька гігабайт. Іноді це призводить до вражаючого приросту продуктивності, за рахунок виключення операцій доступу до жорсткого диску.


Якщо вищесказане не переконало Вас в перевагах 64-бітових систем, то подивіться уважніше, чим займаються Ваші колеги або Ви самі. Хтось оптимізує код, підвищуючи продуктивність функції на 10%, хоча ці 10% можна отримати простий перекомпіляцією програми під 64-бітну архітектуру? Хтось створює свій клас для роботи з масивами, підкачуємі з файлів, так як повністю цей масиви не поміщається в пам’ять? Ви робите свій менеджер розподілу пам’яті, щоб не фрагментувати пам’ять? Якщо Ви відповісте на одне з питань – “Так”, то слід зупинитися і подумати. Ймовірно, Ви ведете марне бій. І можливо буде вигідніше витратити час на перенесення вашої програми на 64-бітну систему, де всі ці питання зникнуть самі собою. Тим більше що рано чи пізно, Ви все одно витратите на це час.


Підсумуємо сказане. Немає сенсу витрачати час, щоб вичавити останні можливості з 32-бітної архітектури. Економте свій час. Використовуйте для підвищення продуктивності паралельність і 64-бітове адресний простір. Зробіть новий інформаційний привід і випередити своїх конкурентів при освоєнні ринку високопродуктивних додатків.


2. Озброїтеся хорошим апаратним забезпеченням.


Отже, Ви прийняли рішення використовувати паралельність і 64-бітові технології в ваших програмних розробках. Чудово. Тоді давайте спочатку розглянемо деякі організаційні питання.


Не дивлячись на те, що Вам доводиться стикатися з розробкою складних програм, що обробляють величезні обсяги даних, керівництво все одно часто не розуміє необхідність забезпечення своїх розробників найбільш потужними комп’ютерами. Хоча Ваші додатки можуть бути призначені для потужних робочих станцій, Вам, можливо, все одно доведеться їх тестувати і налагоджувати на своїй робочій машині. Звичайно, потреба у потужній обчислювальній техніці існує у всіх програмістів, нехай навіть їх програми не обробляють гігабайти даних. Усім доводиться їх компілювати, запускати допоміжні великовагові інструменти і так далі. Але тільки розробник ресурсоемкого програмного забезпечення може у всіх деталях відчути неприємності від недостатньої кількості оперативної пам’яті або повільності дискової підсистеми.


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


Це може прозвучати неоригінально, але швидкий процесор і швидка дискова підсистема можуть істотно прискорити процес компіляції програм! Здається між двома або однією хвилиною компіляції частини коду немає різниці? Вона величезна! Хвилини виливаються у години, дні, місяці. Попросіть своїх програмістів порахувати, скільки часу вони проводять в очікуванні компіляції коду. Поділіть цей час хоча б в 1.5 рази і потім зможете підрахувати, як швидко окупиться вкладення в нову техніку. Запевняю Вас – Ви будете приємно здивовані.


Пам’ятайте і про наступний ефект, який буде економити робочий час. Якщо якусь дію триває 5 хвилин, то людина почекає. Якщо 10 – то він піде готувати каву, читати форуми або грати в пінг-понг, що займе набагато більше 10 хвилин! І, не тому що він злодій, або дуже хоче кави – йому буде просто нудно. Не давайте перериватися дії: натиснув – отримав результат. Зробіть так, щоб ті процеси, які займали 10 хвилин – почали займати менше 5.


Ще раз хочу звернути увагу – мета не зайняти вільний час програміста корисною справою, а прискорити процеси в цілому. Установка другого комп’ютера (двухпроцессорной системи) з метою, щоб програміст перемикався в хвилини очікування на інші завдання – в корені хибна. Праця програміста, це не праця двірника, де в перекурах між колки льоду можна почистити лавку від снігу. Праця програміста вимагає концентрації над завданням і утримання в пам’яті багатьох її елементів. Не намагайтеся переключити програміста, намагайтеся зробити так, щоб він якомога швидше міг продовжити вирішувати задачу, над якою зараз працює. Ніякої користі від такої спроби не буде, Ви тільки ще більше втомило розробника, при меншій ефективності праці. Згідно зі статтею “Стреси многозадачной роботи: як з ними боротися” [2] Необхідний час на занурення в іншу або перервану завдання становить 25 хвилин. Якщо не забезпечити непріривності процесу, половина часу буде йти на це саме перемикання. Не важливо, що це – Гра в пінг-понг, або пошук помилки в іншій програмі.


Не шкодуйте купити кілька зайвих гігабайт пам’яті. Ця покупка окупиться після кількох етапів налагодження програми, що виділяє велику кількість пам’яті. Знайте, що нестача оперативної пам’яті призводить до вивантаженні даних на диск (swapping) і може уповільнити процес налагодження за хвилин до годин.


Не шкодуйте забезпечити машину RAID підсистемою. Не будемо теоретиками, ось приклад з особистої практики (таблиця N2).













Конфігурація (зверніть увагу на RAID) Час збірки середнього проекту, що використовує велику кількість зовнішніх бібліотек.
AMD Athlon(tm) 64 X2 Dual Core Processor 3800+, 2 GB of RAM,2 x 250Gb HDD SATA – RAID 0  95 хвилин
AMD Athlon(tm) 64 X2 Dual Core Processor 4000+, 4 GB of RAM,500 Gb HDD SATA (No RAID) 140 хвилин
Таблиця 2. Приклад вплив RAID на швидкість збірки додатку.

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


На цьому текст, присвячений керівникам, закінчений, і ми знову хочемо звернутися до творців програмних рішень. Вимагайте, вимагайте для себе тієї техніки, яку вважаєте себе необхідною. Не соромтеся, в кінці кінців Ваш начальник, швидше за все, може просто не розуміти, що це вигідно всім. Потрібно займатися просвітницькою роботою. Тим більше у випадку відставання в планах, винним будете здаватися Ви. Простіше вибити нову техніку, ніж намагатися пояснити, на що Ви витрачаєте час. Самі уявіть, як може звучати Ваше виправдання про виправлення однієї єдиної помилки протягом всього дня: “Так адже проект великий прислали. Я запустив під відладчиком, довго чекав. А пам’яті у мене тільки 1 гігабайт. А більше нічим паралельно займатися неможливо. Windows в своп пішов. Знайшов помилку виправив, але так адже знову знову запустити і перевірити треба …. “. Ваш начальник можливо промовчить, але буде вважати Вас просто ледарем. Не доводьте до цього.


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


Перш ніж перейти до наступної теми, хочеться поділитися ще деякими думками, які допоможуть зробити роботу більш комфортною.


Проблему повільної складання проекту можна спробувати вирішити шляхом використання спеціальних засобів паралельної збірки подібної, наприклад системі IncrediBuild by Xoreax Software (www.xoreax.com). Природно існують і інші подібні системи, які можна пошукати в мережі.


Проблему тестування додатків на величезних масивах даних (запуск пакетів з тестами), для яких робочі машини недостатньо продуктивні, можна вирішити використанням декількох спеціальних потужних машин з віддаленим доступом. Прикладом віддаленого доступу може служити Remote Desktop або X-Win. Звичайно одночасно тестові запуски здійснює тільки мала кількість розробників. І для колективу з 5-7 чоловік цілком може вистачити 2-х потужних виділених машин. Це буде не саме зручне рішення, але вельми економічне, порівняно з придбанням таких робочих станцій кожному розробнику.


3. Міняємо відладчик на систему логування.


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


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


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


3.1. Причини, що знижують привабливість відладчика.


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


Розглянемо причини, що вимагають використання альтернативних засобів замість класичного отладчика (наприклад, вбудованого в середу Visual C++).


1) Повільне виконання програми.


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


Можна резонно зауважити, що налагоджувати програму зовсім не обов’язково на великих робочих обсягах даних, а обійтися тестовими завданнями. На жаль, це не так. Неприємний сюрприз полягає в тому, що при розробці 64-бітових систем, Ви не можете бути впевнені в коректності роботи алгоритмів, тестуючи їх на невеликих обсягах даних, а не на робочих обсягах розміром в гігабайти.


Наведемо один простий приклад, який демонструє проблему необхідності тестування на великому обсязі даних.





#include <vector>
#include <boost/filesystem/operations.hpp>
#include <fstream>
#include <iostream>
int main(int argc, char* argv[])
{
std::ifstream file;
file.open(argv[1], std::ifstream::binary);
if (!file)
return 1;
boost::filesystem::path
fullPath(argv[1], boost::filesystem::native);
boost::uintmax_t fileSize =
boost::filesystem::file_size(fullPath);
std::vector<unsigned char> buffer;
for (int i = 0; i != fileSize; ++i)
{
unsigned char c;
file >> c;
if (c >= “A” && c <= “Z”)
buffer.push_back(c);
}
std::cout << “Array size=” << buffer.size()
<< std::endl;
return 0;
}

Дана програма читає файл і зберігає в масиві всі символи, пов’язані з заголовним англійським буквах. Якщо всі символи у вихідному файлі будуть великими англійськими літерами, то на 32-бітної системі ми не зможемо помістити в масив більше 2 * 1024 * 1024 * 1024 символів, а отже і обробити файл більше 2 гігабайт. Уявімо, що така програма коректно використовувалася на 32-бітної системі, з урахуванням цього обмеження і ніяких помилок не виникало.


На 64-бітної системі виникне бажання обробляти файли більшого розміру, так як знімається обмеження на розмір масиву в 2 гігабайти. На жаль, програма написана некоректно з точки зору моделі даних LLP64 (див. таблицю N1), яка використовується в 64-бітної операційної системи Windows. У циклі використовується змінна типу int, розмір якої, як і раніше становить 32 біта. У разі якщо розмір файлу буде дорівнює 6 гігабайт, то умова “i! = fileSize” ніколи не буде виконано і виникне вічний цикл.


Даний код наведено, щоб продемонструвати складність пошуку за допомогою отладчика помилок, які виникають тільки на великому обсязі пам’яті. Отримавши зациклення при обробці файлу в 64-бітної системі можна взяти для обробки файл розміром в 50 байт і переглянути роботу функції під відладчиком. Але помилка на такому обсязі даних не виникне, а дивитися в відладчик обробку 6000000000 елементів неможливо.


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


Більш докладно з подібними неприємними прикладами Ви можете познайомитися у статті “Забуті проблеми розробки 64-бітних програм” і “20 пасток перенесення Сі + + – коду на 64-бітну платформу”.


2) Нить.


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


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


Більш докладно з питаннями налагодження паралельних систем Ви можете познайомитися в наступних статтях: “Технологія налагодження програм для машин з масовим паралелізмом”, “Multi-threaded Debugging Techniques”, “Detecting Potential Deadlocks”.


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


Для налагодження паралельних систем слід звернути увагу в сторону таких інструментів як TotalView Debugger (TVD). TotalView це відладчик для мов Сі, Сі + + і фортран, який працює на Юнікс-сумісних ОС і Mac OS X. Він дозволяє контролювати нитки исполения (потоки, thread), показувати дані одного або всіх потоків, може синхронізувати нитки через точки зупину. Він підтримує також паралельні програми, що використовують MPI і OpenMP.


Іншим цікавими додатками є засоби аналізу багатопоточності Intel ® Threading Analysis Tools.


3.2. Використання системи логування.


Перераховані і залишилися за кадром інструменти, безумовно, корисні і можуть стати гарною підмогою при розробці високопродуктивних додатків. Але не варто забувати і про таку перевіреної часом методології, як використання систем логування. Налагодження методом логування за кілька десятиліть нітрохи не втратила актуальності і залишається вірним засобом, про який ми поговоримо більш докладно. Єдина зміна, яку накладає час на системи логування, це зрослі до них вимоги. Спробуємо перерахувати властивості, якими повинна володіти сучасна система логування, для високопродуктивних систем:



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


У статті не буде приведений конкретний код системи логування. Таку систему важко зробити універсальною, оскільки вона сильно залежить від середовища розробки, особливостей проекту, переваг розробника і багато чого іншого. Замість цього буде розглянуто ряд технічних рішень, які допоможуть Вам створити зручну і ефективну систему логування, якщо в тому виникне необхідність.


Найпростішим способом здійснити логування є використання функції аналогічної printf, як показано в прикладі:





  int x = 5, y = 10;

printf(“Coordinate = (%d, %d)n”, x, y);

Природним недоліком є ​​те, що інформація буде виводитися як в налагоджувальному режимі, так і в кінцевому продукті. Тому, слід модернізувати код наступним чином:





#ifdef DEBUG_MODE
#define WriteLog printf
#else
#define WriteLog(a)
#endif
WriteLog(“Coordinate = (%d, %d)n”, x, y);

Це вже краще. Причому зверніть увагу, що ми використовуємо для вибору реалізації функції WriteLog не стандартний макрос _DEBUG, а власний макрос DEBUG_MODE. Це дозволяє включати налагоджувальну інформацію в Release-версії, що важливо при налагодженні на великому обсязі даних.


На жаль, тепер при компіляції не налагоджувальної версії в середовищі Visual C++ виникає попередження: “warning C4002: too many actual parameters for macro” WriteLog “”. Можна відключити це попередження, але це є поганим стилем. Можна переписати код, як показано нижче:





#ifdef DEBUG_MODE
#define WriteLog(a) printf a
#else
#define WriteLog(a)
#endif
WriteLog((“Coordinate = (%d, %d)n”, x, y));

Наведений код не є елегантним, оскільки доводиться використовувати подвійні пари дужок, що часто забувається. Тому внесемо нове вдосконалення:





#ifdef DEBUG_MODE
#define WriteLog printf
#else
inline int StubElepsisFunctionForLog(…) { return 0; }
static class StubClassForLog {
public:
inline void operator =(size_t) {}
private:
inline StubClassForLog &operator =(const StubClassForLog &)
{ return *this; }
} StubForLogObject;

#define WriteLog
StubForLogObject = sizeof StubElepsisFunctionForLog
#endif
WriteLog(“Coordinate = (%d, %d)n”, x, y);


Цей код виглядає складним, але він дозволяє писати одинарні дужки. При вимкненому DEBUG_MODE цей код перетворюється в ніщо, і його можна сміливо використовувати в критичних ділянках коду.


Наступним удосконаленням може стати додавання до функції логування таких параметрів як рівня деталізації і типу виведеної інформації. Рівень деталізації можна задати як параметр, наприклад:





enum E_LogVerbose {
Main,
Full
};
#ifdef DEBUG_MODE
void WriteLog(E_LogVerbose,
const char *strFormat, …)
{

}
#else

#endif
WriteLog (Full, “Coordinate = (%d, %d)n”, x, y);

Цей спосіб зручний тим, що рішення, відфільтрувати або не відфільтрувати незначні повідомлення можна прийняти вже після завершення роботи програми, використовуючи спеціальну утиліту. Недолік такого методу в тому, що завжди відбувається виведення всієї інформації, як важливою, так і другорядною, що може знижувати продуктивність. Тому можна створити декілька функцій виду WriteLogMain, WriteLogFull і так далі, реалізація яких буде залежати від режиму складання програми.


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


 

Як можна бачити на малюнку, запис чергової порції даних відбувається в проміжний масив рядків фіксованої довжини. Фіксований розмір масиву і рядків в ньому дозволяє виключити дорогі операції виділення пам’яті. Це аніскільки не знижує можливості такої системи. Досить вибрати довжину рядків і розмір масиву з запасом. Наприклад, 5000 рядків довжиною в 4000 символів буде достатньо для налагодження практично будь-якої системи. А обсяг пам’яті в 20 мегабайт необхідний для цього, погодьтеся, не критичний для сучасних систем. Якщо ж масив все одно буде переповнений, то нескладно передбачити механізм дострокової запису інформації у файл.


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


Перевага описуваної системи в тому, що вона практично без змін здатна функціонувати при налагодженні паралельної програми, коли в лог пишуть відразу кілька потоків. Слід тільки додати збереження ідентифікатора процесу, щоб потім можна було дізнатися, від яких потоків були отримані повідомлення (дивися малюнок N3).


Останнє удосконалення, яке хочеться запропонувати, це організація показу рівня вкладеності повідомлень при виклику функцій або початку логічного блоку. Це можна легко організувати, використовуючи спеціальний клас який в конструкторі записує в лог ідентифікатор початку блоку, а в деструкції – ідентифікатор кінця блоку. Написавши невелику утилітки, можна трансформувати лог, спираючись на інформацію про ідентифікаторах. Спробуємо показати це на прикладі.


Код програми:





class NewLevel {
public:
NewLevel() { WriteLog(“__BEGIN_LEVEL__n”); }
~NewLevel() { WriteLog(“__END_LEVEL__n”); }
};
#define NEW_LEVEL NewLevel tempLevelObject;
void MyFoo() {
WriteLog(“Begin MyFoo()n”);
NEW_LEVEL;
int x = 5, y = 10;
printf(“Coordinate = (%d, %d)n”, x, y);
WriteLog(“Begin Loop:n”);
for (unsigned i = 0; i != 3; ++i)
{
NEW_LEVEL;
WriteLog(“i=%un”, i);
}
}

Вміст логу:





Begin MyFoo()
__BEGIN_LEVEL__
Coordinate = (5, 10)
Begin Loop:
__BEGIN_LEVEL__
i=0
__END_LEVEL__
__BEGIN_LEVEL__
i=1
__END_LEVEL__
__BEGIN_LEVEL__
i=2
__END_LEVEL__
Coordinate = (5, 10)
__END_LEVEL__

Лог після трансформації:





Begin MyFoo()
Coordinate = (5, 10)
Begin Loop:
i=0
i=1
i=2
Coordinate = (5, 10)

Мабуть, на цьому можна закінчити. Останнє про що хочеться ще згадати, це стаття “Logging In C + +” [11], Яка також може Вам знадобитися. Бажаємо Вам вдалої налагодження.


4. Використання правильних типів даних з точки зору 64-бітових технологій.


Використання відповідних апаратній платформі базових типів даних в мові Сі / Сі + + є важливим елементом для створення якісних і високопродуктивних програмних рішень. З приходом 64-бітних систем почали використовуватися нові моделі даних – LLP64, LP64, ILP64 (див. таблицю N1), що змінило правила і рекомендації використання базових типів даних. До таких типів можна віднести int, unsigned, long, unsigned long, ptrdiff_t, size_t і покажчики. На жаль, питання вибору типів практично не висвітлені в популярній літературі та статтях. А ті джерела, в яких вони освітлені, наприклад “Software Optimization Guide for AMD64 Processors” [12], Рідко читають прикладні програмісти.


Актуальність правильного вибору базових типів для обробки даних зумовлена ​​двома важливими причинами: коректністю роботи коду і його ефективністю.


Історично склалося, що базовим і найбільш використовуваним цілочисловим типом у мові Сі та Сі + + є int або unsigned int. Прийнято вважати, що використання типу int є найбільш оптимальним, так як його розмір збігається з довжиною машинного слова процесора. Машинне слово – це група розрядів оперативної пам’яті, що обирається, процесором за одне звернення (або оброблювана їм як єдина група), зазвичай містить 16, 32 або 64 розряди.


Традиція робити розмір типу int рівним розміром машинного слова до недавнього часу порушувалася рідко. На 16-бітних процесорах int складався з 16 біт. На 32-бітних процесорах – 32 біта. Звичайно, існували і інші співвідношення розміру int і машинного слова, але вони використовувалися рідко і не становлять зараз для нас інтересу.


Нас цікавить той факт, що з приходом 64-бітових процесорів розмір типу int в більшості систем залишився дорівнює 32-бітам. Тип int має розмір 32 біта в моделях даних LLP64 і LP64, які використовуються в 64-бітових операційних системах Windows та більшості Unix систем (Linux, Solaris, SGI Irix, HP UX 11).


Залишити розмір типу int рівним 32-м бітам є поганим рішенням з багатьох причин, але воно є обгрунтованим вибором меншого серед інших зол. У першу чергу воно пов’язане з питаннями забезпечення зворотної сумісності. Більш докладно про причини такого вибору можна прочитати в блозі “Why did the Win64 team choose the LLP64 model?” і статтю “64-Bit Programming Models: Why LP64?”.


Для розробників 64-бітних додатків все вищесказане є передумовою дотримуватися двох нових рекомендацій у процесі розробки програмного забезпечення.


Рекомендація 1. Використовувати для лічильників циклів і адресної арифметики типи ptrdiff_t і size_t, замість int і unsigned.


Рекомендація 2. Використовувати для індексації в масивах типи ptrdiff_t і size_t, замість int і unsigned.


Іншими словами, по можливості використовувати типи даних, які на 64-бітної системі мають розмір 64-біта. Звідси випливає твердження, що не слід більше використовувати конструкції виду:





for (int i = 0; i != n; i++)
array[i] = 0.0;

Так, це канонічний приклад коду. Так, його багато в безлічі програм. Та з нього починають навчання мови Сі та Сі + +. Але більше його використовувати не рекомендується. Використовуйте або ітератори, або типи даних ptdriff_t і size_t, як показано в покращеному прикладі:





for (size_t i = 0; i != n; i++)
array[i] = 0.0;

Розробники Unix-додатків можуть зробити зауваження, що вже досить давно виникла практика використання типу long для лічильників та індексації масивів. Тип long є 64-бітовим в 64-бітових Unix-системах і його використання виглядає більш елегантним, ніж ptdriff_t або size_t. Та це так, але слід врахувати два важливі обставини.


1) У 64-бітових операційних системах Windows розмір типу long залишився 32-бітовим (див. таблицю N1). І, отже, він не може бути використаний замість типів ptrdiff_t і size_t.


2) Використання типів long і unsigned long ще більше ускладнює життя розробників крос-платформних додатків для Windows і Linux систем. Тип long має в цих системах різний розмір і лише додає плутанини. Краще дотримуватися типів, що мають однаковий розмір в 32-бітних і 64-бітних Windows і Linux системах.


Прийшов час на прикладах пояснити, чому так наполегливо рекомендується відмовитися від звичного використання типу int / unsigned на користь ptrdiff_t / size_t.


Почнемо ми з прикладу, який демонструє класичну помилку використання типу unsigned для лічильника циклу в 64-бітному коді. Ми вже описували вище аналогічний приклад, але повторимо його ще раз в силу поширеності даної помилки:





size_t Count = BigValue;
for (unsigned Index = 0; Index != Count; ++Index)
{ … }

Це типовий код, варіанти якого можна зустріти в багатьох програмах. Він коректно виконується в 32-бітних системах, де значення змінної Count не може перевищити SIZE_MAX (який дорівнює в 32-бітної системі UINT_MAX). У 64-бітної системі діапазон можливих значень для Count може бути збільшений і тоді при значенні Count> UINT_MAX виникне вічний цикл. Коректним виправленням даного коду використання замість типу unsigned типу size_t.


Наступний приклад демонструє помилку використання типу int для індексації великих масивів:





double *BigArray;
int Index = 0;
while (…)
BigArray[Index++] = 3.14f;

Цей код зазвичай не викликає жодних підозр у прикладного розробника, який звик до практики використання як індексів масивів змінні типу int або unsigned. На жаль, наведений код на 64-бітної системі буде непрацездатний, якщо обсяг оброблюваного масиву BigArray перевищить розмір в чотири мільярди елементів. В цьому випадку відбудеться переповнення змінної Index, і результат роботи програми буде некоректний (буде заповнений не весь масив). Коригування коду знову укладена у використанні для індексів типу ptrdiff_t або size_t.


Як останній приклад, хочеться продемонструвати потенційну небезпеку смешенного використання 32-бітних і 64-бітових типів, якого слід по можливості уникати. На жаль не багато розробників замислюються, до чого може привести неакуратна змішання арифметика і для багатьох такий приклад виявляється несподіванкою (результати отримані з використанням Microsoft Visual C++ 2005, 64-бітний режим компіляції):





int x = 100000;
int y = 100000;
int z = 100000; intptr_t size = 1; / / Результат:
intptr_t v1 = x * y * z; // -1530494976
intptr_t v2 = intptr_t(x) * y * z; // 1000000000000000
intptr_t v3 = x * y * intptr_t(z); // 141006540800000
intptr_t v4 = size * x * y * z; // 1000000000000000
intptr_t v5 = x * y * z * size; // -1530494976
intptr_t v6 = size * (x * y * z); // -1530494976
intptr_t v7 = size * (x * y) * z; // 141006540800000
intptr_t v8 = ((size * x) * y) * z; // 1000000000000000
intptr_t v9 = size * (x * (y * z)); // -1530494976

Хочеться звернути увагу, що вираз виду “intptr_t v2 = intptr_t (x) * y * z;” зовсім не гарантує правильний результат. Воно гарантує лише те, що вираз “intptr_t (x) * y * z” матиме тип intptr_t. Більш докладно з цими питанням допоможе розібратися стаття “20 пасток перенесення Сі + + – коду на 64-бітну платформу” [4].


Тепер перейдемо до прикладу, демонструє переваги використання типів ptrdiff_t і size_t з точки зору продуктивності. Для демонстрації візьмемо простий алгоритм обчислення мінімальної довжини шляху в алгоритмі. З повним кодом програми можна познайомитися за посиланням: http://www.Viva64.com/articles/testspeedexp.zip.


У статті для стислості наведено тільки текст функцій FindMinPath32 і FindMinPath64. Обидві ці функції вираховують довжину мінімального шляху між двома точками в лабіринті. Решта код не представляє зараз інтересу.





typedef char FieldCell;
#define FREE_CELL 1
#define BARRIER_CELL 2
#define TRAVERSED_PATH_CELL 3
unsigned FindMinPath32(FieldCell (*field)[ArrayHeight_32], unsigned x,
unsigned y, unsigned bestPathLen,
unsigned currentPathLen) {
++currentPathLen;
if (currentPathLen >= bestPathLen)
return UINT_MAX;
if (x == FinishX_32 && y == FinishY_32)
return currentPathLen;
FieldCell oldState = field[x][y];
field[x][y] = TRAVERSED_PATH_CELL;
unsigned len = UINT_MAX;
if (x > 0 && field[x – 1][y] == FREE_CELL) {
unsigned reslen =
FindMinPath32(field, x – 1, y, bestPathLen, currentPathLen);
len = min(reslen, len);
}
if (x < ArrayWidth_32 – 1 && field[x + 1][y] == FREE_CELL) {
unsigned reslen =
FindMinPath32(field, x + 1, y, bestPathLen, currentPathLen);
len = min(reslen, len);
}
if (y > 0 && field[x][y – 1] == FREE_CELL) {
unsigned reslen =
FindMinPath32(field, x, y – 1, bestPathLen, currentPathLen);
len = min(reslen, len);
}
if (y < ArrayHeight_32 – 1 && field[x][y + 1] == FREE_CELL) {
unsigned reslen =
FindMinPath32(field, x, y + 1, bestPathLen, currentPathLen);
len = min(reslen, len);
}
field[x][y] = oldState;

if (len >= bestPathLen)
return UINT_MAX;
return len;
}
size_t FindMinPath64(FieldCell (*field)[ArrayHeight_64], size_t x,
size_t y, size_t bestPathLen,
size_t currentPathLen) {
++currentPathLen;
if (currentPathLen >= bestPathLen)
return SIZE_MAX;
if (x == FinishX_64 && y == FinishY_64)
return currentPathLen;
FieldCell oldState = field[x][y];
field[x][y] = TRAVERSED_PATH_CELL;
size_t len = SIZE_MAX;
if (x > 0 && field[x – 1][y] == FREE_CELL) {
size_t reslen =
FindMinPath64(field, x – 1, y, bestPathLen, currentPathLen);
len = min(reslen, len);
}
if (x < ArrayWidth_64 – 1 && field[x + 1][y] == FREE_CELL) {
size_t reslen =
FindMinPath64(field, x + 1, y, bestPathLen, currentPathLen);
len = min(reslen, len);
}
if (y > 0 && field[x][y – 1] == FREE_CELL) {
size_t reslen =
FindMinPath64(field, x, y – 1, bestPathLen, currentPathLen);
len = min(reslen, len);
}
if (y < ArrayHeight_64 – 1 && field[x][y + 1] == FREE_CELL) {
size_t reslen =
FindMinPath64(field, x, y + 1, bestPathLen, currentPathLen);
len = min(reslen, len);
}
field[x][y] = oldState;

if (len >= bestPathLen)
return SIZE_MAX;
return len;
}


Функція FindMinPath32 написана в класичному 32-біном стилі з використанням типів unsigned. Функція FindMinPath64 відрізняється від неї лише тим, що в ній всі типи unsigned замінені на типи size_t. Інших відмінностей немає! Погодьтеся, що це не є складною модифікацією програми. А тепер порівняємо швидкості виконання цих двох функцій (див. таблицю N2).
























  Режим і функція. Час роботи функції
1 32-бітний режим збірки. Функція FindMinPath32 1
2 32-бітний режим збірки. Функція FindMinPath64 1.002
3 64-бітний режим збірки. Функція FindMinPath32 0.93
4 64-бітний режим збірки. Функція FindMinPath64 0.85
Таблиця N2. Час виконання функцій FindMinPath32 і FindMinPath64.

У таблиці N2 показано наведене час відносно швидкості виконання функції FindMinPath32 на 32-бітної системи. Це зроблено для більшої наочності.


У першому рядку час роботи функції FindMinPath32 на 32-бітної системі дорівнює 1. Це викликано тим, що ми взяли саме її час роботи за одиницю виміру.


У другому рядку ми бачимо, що час роботи функції FindMinPath64 на 32-бітної системі також дорівнює 1. Це не дивно, так як на 32-бітної системі тип unsigned збігається з типом size_t і ніякої різниці між функцією FindMinPath32 і FindMinPath64 немає. Невелике відхилення (1.002) говорить тільки про невелику похибки у вимірюваннях.


У третє рядку ми бачимо приріст продуктивності рівний 7%. Це цілком очікуваний результат від перекомпіляції коду для 64-бітної системи.


Найбільший інтерес представляє 4 рядок. Приріст продуктивності становить 15%. Це означає, що просте використання типу size_t замість unsigned дозволяє компілятору побудувати більш ефективний код, працює ще на 8% швидше!


Це простий і наочний приклад, коли використання даних, не рівних розміром машинного слова знижує продуктивність алгоритму. Проста заміна типів int і unsigned на типи ptrdiff_t і size_t може дати істотний приріст продуктивності. У першу чергу це відноситься до використання цих типів даних для індексації масивів, адресної арифметики та організації циклів.


Хочеться сподіватися після всього вищесказаного, Ви задумаєтеся, чи варто продовжувати писати:





for (int i = 0; i !=n; i++)
array[i] = 0.0;

Для автоматизації пошуку помилок в 64-біном коді, розробники Windows-додатків можуть звернути увагу в сторону статичного аналізатора коду Viva64 [8]. По-перше, його використання дозволить виявити більшість помилок. По-друге, розробляючи програми під його контролем, Ви станете рідше використовувати 32-бітних змінні, будете уникати змішаної арифметики з 32-бітними і 64-бітними типами даних, що автоматично збільшить продуктивність Вашого коду. Для розробників під Unix системи інтерес можуть представляти статичні аналізатори Gimpel Software PC-Lint і Parasoft C + + test. Вони здатні діагностувати ряд 64-бітових помилок в коді з моделлю даних LP64, використовуваної в більшості Unix-систем.


Більш детально, Ви можете познайомитися з питаннями розробки якісного та ефективного 64-бітного коду в наступних статтях: “Проблеми тестування 64-бітних додатків”, “24 Considerations for Moving Your Application to a 64-bit Platform”, “Porting and Optimizing Multimedia Codecs for AMD64 architecture on Microsoft Windows”, “Porting and Optimizing Applications on 64-bit Windows for AMD64 Architecture”.


5. Додаткові способи підвищення продуктивності програмних систем.


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


5.1. Intrinsic-функції.


Intrinsic-функції це спеціальні системно-залежні функції, які виконують дії, які неможливо виконати на рівні Сі / Сі + + коду або які виконують ці дії набагато ефективніше. По суті, вони дозволяють позбутися від використання inline-асемблера, тому що його використання часто небажано або неможливо.


Програми можуть використовувати intrinsic-функції для створення більш швидкого коду за рахунок відсутності накладних витрат на виклик звичайного виду функцій. При цьому, природно, розмір коду буде трохи більше. В MSDN наводиться список функцій, які можуть бути замінені їх intrinsic-версією. Це, наприклад, memcpy, strcmp та інші.


У компіляторі Microsoft Visual C + + є спеціальна опція “/ Oi”, яка дозволяє автоматично замінювати виклики деяких функцій на intrinsic-аналоги.


Крім автоматичної заміни звичайних функцій на intrinsic-варіанти, можна явно використовувати в коді intrinsic-функції. Ось для чого це може бути потрібно:



Використання intrinsic-функцій в автоматичному режимі (за допомогою ключа компілятора) дозволяє отримати безкоштовно декілька відсотків приросту продуктивності, а “ручне” – навіть більше. Тому використання intrinsic-функцій цілком виправдано.


Більш докладно з застосуванням intrinsic-функцій можна ознайомитися в блозі команди Visual C + + [21].


5.2. Упаковка та вирівнювання даних.


Вирівнювання даних останнім часом не так сильно позначається на продуктивності коду, як, скажімо, 10 років тому. Однак іноді і тут можна отримати додатковий виграш в економії пам’яті та продуктивності.


Розглянемо приклад:





struct foo_original {int a; void *b; int c; };

У 32-бітному режимі дана структура займає 12 байт, але в 64-бітному – вже 24 байта. Для того щоб в 64-бітному режимі структура займала належні їй 16 байт слід змінити порядок проходження полів:





struct foo_new { void *b; int a;  int c; };

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





// 16-byte aligned data
__declspec(align(16)) double init_val [3.14, 3.14];
// SSE2 movapd instruction
_m128d vector_var = __mm_load_pd(init_val);

Джерела “Porting and Optimizing Multimedia Codecs for AMD64 architecture on Microsoft Windows”, “Porting and Optimizing Applications on 64-bit Windows for AMD64 Architecture” [20] Дають детальний огляд даних питань.


5.3. Файли, які відображаються в пам’ять.


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


5.4. Ключове слово __ restrict.


Одна з найбільш серйозних проблем для компілятора – це поєднання (aliasing) імен. Коли код читає і пише пам’ять, часто на етапі компіляції неможливо визначити, чи отримує до цієї області пам’яті доступ більш ніж один покажчик. Тобто, чи може більш ніж один покажчик “синонімом” для однієї і тієї ж області пам’яті. Тому, наприклад, всередині циклу, в якому і читається, і пишеться пам’ять, компілятор повинен бути дуже обережний зі зберіганням даних в регістрах, а не в пам’яті. Це недостатньо активне використання регістрів може істотно вплинути на продуктивність.


Ключове слово __ restrict використовується для того, щоб полегшити компілятору прийняття рішення. Воно говорить компілятору “бути сміливішими” з використанням регістрів.


Ключове слово __ restrict дозволяє компілятору не вважати відмічені покажчики синонімічні (aliased), тобто посилаються на одну й ту ж область пам’яті. Компілятор в такому випадку може зробити більш ефективну оптимізацію. Розглянемо приклад:





int * __restrict a;
int *b, *c;
for (int i = 0; i < 100; i++)
{
*a += *b++ – *c++ ; // no aliases exist
}

В даному коді компілятор може безпечно зберігати суму в регістрі, пов’язаному зі змінною “a”, уникаючи запису в пам’ять. Хорошим джерелом інформації про використання ключового слова __ restrict є MSDN.


5.5. SSE-інструкції.


Програми, що запускаються на 64-бітних процесорах (незалежно від режиму) будуть працювати більш ефективно, якщо в них використовуються SSE-інструкції замість MMX/3DNow. Це пов’язано з розрядністю оброблюваних даних. SSE/SSE2 інструкції оперують 128-бітними даними, у той час як MMX/3DNow – тільки лише 64-бітними. Тому код, що використовують MMX/3DNow, краще переписати з орієнтацією на SSE.


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


5.6. Певні правила використання мовних інструкцій.


64-бітова архітектура приносить нові можливості для оптимізації на рівні окремих операторів мови програмування. Це вже стали традиційними прийоми по “переписуванню” шматочків програми з тим, щоб компілятор ще краще їх оптимізував. Рекомендувати до масового використання ці прийоми, звичайно ж, не варто, але знати про них може бути корисно.


На першому місці з цілого списку даних оптимізацій варто ручне розгортання циклів (unroll the loop). Суть даного методу легко побачити з прикладу:





double a[100], sum, sum1, sum2, sum3, sum4;
sum = sum1 = sum2 = sum3 = sum4 = 0.0;
for (int i = 0; i < 100; I += 4)
{
sum1 += a[i];
sum2 += a[i+1];
sum3 += a[i+2];
sum4 += a[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

У багатьох випадках, компілятор сам може розгорнути цикл до такого подання (ключ / fp: fast для Visual C + +), але не завжди.


Інший синтаксичної оптимізацією є використання масивної (від слова “масив”) нотації замість вказівної (від слова “покажчик”).


Безліч подібних прийомів наведено в “Software Optimization Guide for AMD64 Processors”.


Висновок.


Незважаючи на те, що при створенні програмних систем, ефективно використовують апаратні можливості сучасної обчислювальної техніки, доведеться зіткнутися з великою кількістю труднощів, гра варта свічок. Паралельні 64-бітові системи відкривають нові можливості в побудові справжніх масштабованих рішень. Вони дозволяють підняти на новий рівень можливості сучасних програмних засобів в обробці даних, будь то ігри, CAD-системи або розпізнавання образів. Бажаємо Вам удачі в освоєнні нових технологій!

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


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

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

Ваш отзыв

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

*

*