Гаразд, ви визначаєте проблему в тому, де, здавалося б, не так багато можливостей для вдосконалення. На моєму досвіді це досить рідко. Я спробував пояснити це в статті доктора Доббса в листопаді 1993 року, починаючи з звичайно добре розробленої нетривіальної програми без очевидних відходів і приймаючи її через низку оптимізацій, поки час її настінного годинника не скоротився з 48 секунд до 1,1 секунди, а розмір вихідного коду було зменшено в 4 рази. Мій діагностичний інструмент був таким . Послідовність змін була така:
Першою виявленою проблемою було використання кластерів списків (тепер їх називають "ітератори" та "класи контейнерів"), що займають більше половини часу. Вони були замінені досить простим кодом, скоротивши час до 20 секунд.
Зараз найбільший показник часу - це створення списків. У відсотках вона раніше не була такою великою, але зараз це тому, що більша проблема була усунена. Я знаходжу спосіб її пришвидшити, і час падає до 17 секунд.
Зараз важче знайти очевидних винуватців, але є кілька менших, з якими я можу щось зробити, і час падає на 13 сек.
Тепер я, здається, потрапив у стіну. Зразки говорять мені саме про те, що вона робить, але я не можу знайти щось, що можу вдосконалити. Потім я замислююся над базовим дизайном програми, її структурою, що керується транзакціями, і запитую, чи всі списки пошуку, які вона робить, насправді зобов'язані вимогам проблеми.
Потім я торкнувся перепроектування, де програмний код фактично генерується (за допомогою макросів препроцесора) з меншого набору джерел, і в якому програма не постійно з'ясовує речі, про які знає програміст, досить передбачувані. Іншими словами, не "інтерпретуйте" послідовність дій, а "складайте" це.
- Це оновлення зроблено, зменшуючи вихідний код в 4 рази, а час скорочується до 10 секунд.
Тепер, оскільки воно стає таким швидким, важко зробити вибірку, тому я даю йому в 10 разів більше роботи, але наступні рази базуються на початковому навантаженні.
Більш діагноз показує, що він витрачає час на керування чергою. Накладки зменшують час до 7 секунд.
Зараз великим зайняттям часу є діагностичний друк, який я робив. Промийте це - 4 секунди.
Зараз найбільші споживачі часу - це дзвінки на безіменний та безкоштовний . Переробити предмети - 2,6 секунди.
Продовжуючи вибірку, я все ще знаходжу операції, які не є строго необхідними - 1,1 секунди.
Загальний коефіцієнт швидкості: 43,6
Зараз жодна дві програми не схожі, але в неіграшному програмному забезпеченні я завжди бачив такий прогрес. Спочатку ви отримуєте легкі речі, а потім більш складні, поки не дістанетесь до точки зменшення віддачі. Тоді зрозуміння, яке ви отримаєте, цілком може призвести до зміни дизайну, починаючи новий раунд прискорень, доки ви знову не потрапите на зменшувані прибутки. Тепер це точка , в якій вона може мати сенс засумніватися в тому , ++i
чи i++
або for(;;)
чи while(1)
швидше: види питань , які я бачу так часто на переповнення стека.
PS Може бути цікаво, чому я не використовував профілер. Відповідь полягає в тому, що майже кожна з цих "проблем" була функціональним сайтом виклику, який стекує зразки чітко. Профілі, навіть сьогодні, ледве стикаються з думкою, що заяви та інструкції для викликів важливіше знайти та легше виправити, ніж цілі функції.
Я фактично створив профайлер для цього, але для справжньої близькості з тим, що робить код, немає ніякої заміни для того, щоб отримати пальці прямо в ньому. Справа не в тому, що кількість зразків невелика, тому що жодна з проблем, які знайдені, не є настільки крихітною, що їх легко пропустити.
ДОДАТИ: jerryjvl попросив кілька прикладів. Ось перша проблема. Він складається з невеликої кількості окремих рядків коду, разом займаючи половину часу:
/* IF ALL TASKS DONE, SEND ITC_ACKOP, AND DELETE OP */
if (ptop->current_task >= ILST_LENGTH(ptop->tasklist){
. . .
/* FOR EACH OPERATION REQUEST */
for ( ptop = ILST_FIRST(oplist); ptop != NULL; ptop = ILST_NEXT(oplist, ptop)){
. . .
/* GET CURRENT TASK */
ptask = ILST_NTH(ptop->tasklist, ptop->current_task)
Вони використовували кластер списку ILST (подібно до класу списку). Вони реалізовані звичайним способом, "приховування інформації" означає, що користувачі класу не повинні були дбати про те, як вони були реалізовані. Коли ці рядки були написані (приблизно з 800 рядків коду), думка не замислювалася над тим, що вони можуть бути «вузьким місцем» (я ненавиджу це слово). Вони просто рекомендований спосіб робити речі. Неважко сказати заднім числом, що цього слід було уникати, але, на моєму досвіді, всі проблеми з виконанням роботи є такими. Загалом, добре намагатися уникати проблем із продуктивністю. Ще краще знайти та виправити створені, хоча їх "слід було уникати" (заднім числом).
Ось друга проблема, у двох окремих рядках:
/* ADD TASK TO TASK LIST */
ILST_APPEND(ptop->tasklist, ptask)
. . .
/* ADD TRANSACTION TO TRANSACTION QUEUE */
ILST_APPEND(trnque, ptrn)
Це списки складання, додаючи елементи до їх кінців. (Виправлення полягало в тому, щоб збирати елементи в масивах та створювати списки всі відразу.) Цікавим є те, що ці висловлювання коштували лише (тобто були у стеку викликів) 3/48 початкового часу, тому їх не було в фактично велика проблема на початку . Однак після усунення першої проблеми вони коштували 3/20 часу і тому тепер були «більшою рибою». Загалом, так воно і йде.
Можу додати, що цей проект відгонили від реального проекту, в якому я допомагав. У цьому проекті проблеми з продуктивністю були набагато драматичнішими (як і прискорення), як-от виклик розпорядження доступу до бази даних у внутрішньому циклі, щоб побачити, чи завдання виконано.
ДОВІДКА ДОПОМОГА: Вихідний код, як оригінальний, так і перероблений, можна знайти на веб-сайті www.ddj.com за 1993 рік у файлі 9311.zip, файлах slug.asc та slug.zip.
EDIT 2011/11/26: Зараз існує проект SourceForge, що містить вихідний код у Visual C ++ та опис того, як його було налаштовано. Він проходить лише першу половину описаного вище сценарію, і він не дотримується абсолютно тієї ж послідовності, але все ж отримує прискорення на 2-3 порядку.