Поради щодо оптимізації програм C # /. NET [закрито]


78

Здається, оптимізація - це втрачене мистецтво в наші дні. Хіба не було часу, коли всі програмісти вичавлювали кожну унцію ефективності зі свого коду? Часто роблячи це, проходячи п’ять миль по снігу?

У дусі повернення втраченого мистецтва, які кілька порад ви знаєте для простих (або, можливо, складних) змін для оптимізації коду C # /. NET? Оскільки це настільки широка річ, яка залежить від того, що хтось намагається досягти, це допомогло б надати контекст вашій підказці. Наприклад:

  • При об'єднанні багатьох рядків разом StringBuilder замість цього. Див. Посилання внизу щодо застережень щодо цього.
  • Використовуйте string.Compare для порівняння двох рядків, замість того, щоб робити щось подібнеstring1.ToLower() == string2.ToLower()

Загальний консенсус на даний момент, як видається, є ключовим. Цей тип пропускає сенс: вимірювання не говорить вам, що не так, або що з цим робити, якщо ви зіткнетеся з вузьким місцем. Одного разу я зіткнувся з вузьким місцем об’єднання рядків і не знав, що з цим робити, тому ці поради корисні.

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

Хоча я бачу, що може бути корисним також знати, чому підказка корисна і де її слід застосовувати. Для StringBuilderпідказки я знайшов допомогу, яку давно зробив тут, на сайті Джона Скіта .


10
Також важливо пройти межу між оптимізацією та зручністю читання.
Райан Елкінс,

3
«Купка струн»; номер не проблема - це вони в одному складеному заяві конкатенації або декількох операторів.
Марк Гравелл

2
StringBuilder часто повільніший за оператор +. Компілятор C # автоматично переводить повторене + у відповідне перевантаження String.Concat.
Річард Берг,

2
Вам доведеться важко боротися з CLR, поки оптимізується IL під час виконання, і ви намагалися зробити те саме під час компіляції - перетягування каната. У старі добрі часи ви оптимізували інструкції до машини, і машина безглуздо керувала ними.
John K

Відповіді:


106

Здається, оптимізація - це втрачене мистецтво в наші дні.

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

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

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

Протягом багатьох років мене десятки разів запитували у мене перелік "порад та підказок", які люди можуть використовувати для оптимізації своїх vbscript / jscript / своїх активних сторінок сервера / своїх VB / своїх кодів C #. Я завжди противлюсь цьому. Підкреслювати "поради та підказки" - це зовсім неправильний спосіб підійти до ефективності. Цей шлях призводить до коду, який важко зрозуміти, важко міркувати, важко підтримувати, який, як правило, не помітно швидший, ніж відповідний прямий код.

Правильний спосіб підійти до продуктивності - підійти до неї як до інженерної проблеми, як до будь-якої іншої проблеми:

  • Ставте значущі, вимірювані, орієнтовані на споживача цілі.
  • Створіть тестові набори, щоб перевірити свою ефективність щодо цих цілей в реалістичних, але контрольованих та повторюваних умовах.
  • Якщо ці апартаменти показують, що ви не досягаєте своїх цілей, використовуйте такі інструменти, як профілі, щоб з’ясувати, чому.
  • Оптимізуйте зловмисники того, що профілювач визначає як найгіршу підсистему. Продовжуйте профілювати кожну зміну, щоб ви чітко розуміли вплив продуктивності кожної з них.
  • Повторюйте, поки не відбудеться одна з трьох речей (1) ви досягнете своїх цілей і поставите програмне забезпечення, (2) ви переглянете цілі вниз до чогось, що ви можете досягти, або (3) ваш проект буде скасовано, оскільки ви не змогли досягти своїх цілей.

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

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


1
Збирався написати подібну стяжку, але ваша краще. Браво.
Річард Берг,

7
Є лише деякі випадки, коли є відомий кращий спосіб виконати одне і те ж завдання, не будучи при цьому достатнім ресурсом. Я не купую, що цілком нормально програмувати, однак ви хочете, поки ви досягнете якоїсь мети, і це, здається, виходить нормально. Або що найкраще програмувати, потім запустити профайлер, а потім повернутися і змінити проблемні області. Що поганого в тому, що людина добре уявляє, що потрібно для оптимізації певних бітів коду ще до їх початку?
Боб,

5
@Bob: Нічого поганого в тому, що ти розумний у використанні ресурсів. Де все йде не так, коли люди (1) витрачають багато часу (= гроші) на мікро-оптимізацію, яка не робить різниці, (2) пише неправильні програми та (3) пише незрозумілі програми. Для чого вам слід оптимізувати - це по-перше, правильність. По-друге, хороший стиль кодування. По-третє, продуктивність. Як тільки код буде правильним та елегантним, набагато простіше буде зробити його ефективним.
Ерік Ліпперт,

3
Це добре, але ви помітите, що я не кажу, що не слід кодувати правильність спочатку, або стиль по-друге, або що у вас є. Але це також правда, що іноді (або, можливо, багато разів у ці дні) програмісти взагалі не враховують продуктивність чи оптимізацію. Чи достатньо просто мати 1 і 2, щоб компенсувати загальний обхід 3? Я не можу зрозуміти, як погано замислюватися про повагу до оптимізації та про те, щоб дізнатись щось інше про те, що потрібно
Боб,

7
@Bob: Я згоден, що деякі програмісти не дбають про продуктивність. Але я не дотримуюся вашої думки. Список порад та підказок не дозволить раптово перетворити їх на людей, які піклуються про ефективність. Якщо припустити заради аргументу, що ви можете зробити людей, які зараз не зацікавлені, зацікавленими людьми, перелік порад та підказок не допоможе їм досягти хорошої ефективності. Ви можете застосовувати підказки та підказки до основного коду протягом усього дня і ніколи не знати, чи досягаєте ви взагалі будь-якого прогресу проти своїх цілей. Ви повинні мати цілі та вимірювати свій прогрес.
Ерік Ліпперт,

45

Отримайте хороший профайлер.

Не турбуйтеся, навіть намагаючись оптимізувати C # (насправді, будь-який код) без хорошого профайлера. Насправді це надзвичайно допомагає мати під рукою як дискретизацію, так і трасування.

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

Першими трьома кроками до профілювання завжди повинні бути 1) Міра, 2) Міра, а потім 3) Міра ....


1
Я б сказав, не вимірюй , захоплюй . stackoverflow.com/questions/406760/…
Mike Dunlavey

23
Ви забули4) measure
Ніфле

1
@Nifle: Якщо ви полюєте на слонів, чи потрібно вам їх виміряти?
Mike Dunlavey,

1
@RobbieDee: Дивіться відповідь Конрада Альбрехта .
Mike Dunlavey

1
@MikeDunlavey Вибачте, я просто трохи веселився з вами, але дякую ... :-)
Роббі Ді

21

Рекомендації щодо оптимізації:

  1. Не робіть цього, якщо вам не потрібно
  2. Не робіть цього, якщо дешевше кинути на проблему нове обладнання замість розробника
  3. Не робіть цього, якщо ви не зможете виміряти зміни у виробничому еквівалентному середовищі
  4. Не робіть цього, якщо ви не знаєте, як користуватися центральним процесором та профайлером пам'яті
  5. Не робіть цього, якщо це зробить ваш код нечитабельним або неможливим

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

Почніть з дальнього кінця: використовуйте YSlow, щоб зрозуміти, чому ваш веб-сайт повільний для кінцевих користувачів, а потім поверніться назад і зафіксуйте доступ до бази даних, щоб він був не надто широким (стовпці) і не надто глибоким (рядки).

У дуже рідкісних випадках, коли варто зробити що-небудь для оптимізації використання центрального процесора, будьте обережні, щоб не впливати негативно на використання пам’яті: я бачив „оптимізацію”, коли розробники намагалися використовувати пам’ять для кешування результатів для збереження циклів процесора. Мережевий ефект полягав у зменшенні доступної пам’яті на кеш-сторінки та результати баз даних, що робило роботу програми набагато повільнішою! (Див. Правило щодо вимірювання.)

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


10
Так само, не захоплюйтеся аналізом великих значень. Алгоритм пошуку наївних рядків O (nm) для типових бізнес-випадків у тисячі разів швидший, ніж алгоритми O (n + m), які попередньо обробляють пошукові рядки у пошуках шаблонів. Пошук наївних рядків, що відповідає першому символу, часто компілюється до однієї машинної інструкції, яка надзвичайно швидка для сучасних процесорів, які активно використовують оптимістичні кеші пам'яті.
Ерік Ліпперт,

16

При роботі з ORM пам’ятайте про N + 1 Selects.

List<Order> _orders = _repository.GetOrders(DateTime.Now);
foreach(var order in _orders)
{
    Print(order.Customer.Name);
}

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


14
  • Не використовуйте магічні числа, використовуйте перелічення
  • Не варто твердо кодувати значення
  • Використовуйте дженерики, де це можливо, оскільки це безпечно для типу та уникає боксу та розпакування
  • Використовуйте обробник помилок там, де це вкрай необхідно
  • Утилізувати, розпоряджатися, розпоряджатися. CLR не знає, як закрити підключення до бази даних, тому закрийте їх після використання та утилізуйте некеровані ресурси
  • Використовуйте здоровий глузд!

15
Як би я не погоджувався, що це хороші справи, перші дві речі тут не впливають на продуктивність - лише ремонтопридатність ...
Рід Копсі,

правда, але це все одно оптимізований код.
SoftwareGeek

4
До того ж третій (бокс) рідко буває справжньою точкою дотику; це перебільшено як питання; як і винятки - зазвичай не проблема.
Марк Гравелл

1
"але це все-таки оптимізований код" - це велика претензія; єдине, що я мав би розраховувати на важливу проблему, це "розпорядитися"; і це, швидше за все, з’являється як винятки (поза ручками тощо), а не погіршення продуктивності.
Марк Гравелл

Насправді, схема фіналізатора досить погана, якщо ваша мета - оптимізація. Об'єкти з фіналізаторами автоматично підвищуються до Gen-1 (або гірше). Крім того, примушувати код вашого фіналізатора працювати в потоці GC зазвичай не є оптимальним, якщо в цьому списку Todo є щось віддалено дороге. Підсумок: це функція, спрямована на зручність та коректність, а не функція, призначена для швидкості. Подробиці: msdn.microsoft.com/en-us/magazine/bb985010.aspx
Річард Берг,

9

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

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

Зробіть це 10-20 разів, і ви зрозумієте, яка оптимізація може насправді змінити ситуацію.


1
++ Амінь. Я роблю це ще до існування профілістів. & ваша програма DrawMusic виглядає неймовірно!
Mike Dunlavey,

6
По суті, це роблять профілісти, за винятком того, що вони роблять це краще за вас приблизно тисячею різних способів (швидше, частіше, точніше тощо). Вони також дають стеки дзвінків. Це рішення бідного (і старого, хто боїться навчитися нових речей).
BlueRaja - Danny Pflughoeft

@ BlueRaja-DannyPflughoeft: Вони вас обманюють. Вони з великою точністю говорять, що нічого особливо не можна зробити. Різниця між цим методом і профайлерами полягає в тому, що в цьому методі ви можете бачити пришвидшення того, що неможливо вивести з простої статистики. Натомість вони беруть 1000 зразків, коли інформація, яка може призвести вас до проблеми, виявляється в перших 10, якщо ви дійсно можете побачити необроблені зразки. Я впевнений, що ви бачили цей пост .
Майк Данлавей

@ BlueRaja-DannyPflughoeft: Подивіться на результати. Який найбільший коефіцієнт прискорення, який ви коли-небудь отримували за допомогою профілювача?
Mike Dunlavey

2
@ BlueRaja-DannyPflughoeft: Я впевнений, що ти б цього не зробив, і коли ти досягнеш мого віку, ти натрапиш на таких, як ти. Але залишимо це осторонь. Ось декілька вихідних кодів. Якщо ви можете пришвидшити його на 3 порядки, не дивлячись на те, як я це зробив, використовуючи будь-який інший метод, ви отримаєте права на хвастощі :)
Mike Dunlavey

9

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

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

  • Спробуйте вирішити проблему, використовуючи лише (або: переважно) низькорівневі типи чи масиви з них.
  • Проблеми часто невеликі - використання розумного, але складного алгоритму не завжди змушує вас перемагати, особливо якщо менш розумний алгоритм може бути виражений у коді, який використовує (масиви) типи низького рівня. Візьмемо для прикладу InsertionSort vs MergeSort для n <= 100 або алгоритм пошуку Dominator Tarjan проти використання бітвекторів для наївного вирішення форми потоку даних задачі для n <= 100. (100, звичайно, просто для того, щоб дати вам якусь ідею - профіль !)
  • Подумайте про написання особливого випадку, який можна вирішити за допомогою лише низькорівневих типів (часто екземпляри проблем розміром <64), навіть якщо вам доведеться тримати інший код для більших екземплярів проблем.
  • Вивчіть порозрядну арифметику, щоб допомогти вам у двох наведених вище ідеях.
  • BitArray може бути вашим другом, порівняно зі словником, або ще гірше, List. Але пам’ятайте, що реалізація не є оптимальною; Ви можете написати швидшу версію самостійно. Замість того, щоб перевіряти, що ваші аргументи виходять за межі діапазону тощо, ви часто можете структурувати свій алгоритм так, щоб індекс все одно не міг вийти за межі діапазону - але ви не можете видалити перевірку зі стандартного BitArray, і це не безкоштовно .
  • Як приклад того, що ви можете зробити лише з масивами низькорівневих типів, BitMatrix - це досить потужна структура, яка може бути реалізована як просто масив ulong, і ви навіть можете пройти її, використовуючи ulong як "фронт", тому що ви можете взяти найнижчий біт за постійний час (порівняно з чергою у першому пошуку ширини - але очевидно порядок інший і залежить від індексу елементів, а не від чистого порядку, в якому ви їх знаходите).
  • Ділення та модуль дійсно повільні, якщо правий бік не є константою.
  • Математика з плаваючою точкою - це не так в цілому повільніша за цілочисельну математику (не "те, що ти можеш зробити", а "те, що ти можеш пропустити")
  • Розгалуження не є безкоштовним . Якщо ви можете уникнути цього, використовуючи просту арифметику (що завгодно, окрім ділення або за модулем), іноді ви можете отримати певні результати. Переміщення гілки за межі петлі майже завжди є гарною ідеєю.

Деякі хороші речі, які мені дуже допомогли - дякую!
Роббі Ді

8

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

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


6

Правда полягає в тому, що не існує такого поняття, як ідеально оптимізований код. Однак ви можете оптимізувати для певної частини коду у відомій системі (або наборі систем) на відомому типі центрального процесора (і підрахунку), відомій платформі (Microsoft? Mono ?), Відомому фреймворку / версії BCL , відома версія CLI, відома версія компілятора (помилки, зміни специфікацій, налаштування), відома кількість загальної та доступної пам'яті, відоме походження збірки ( GAC «диск? віддалений»), з відомими фоновими діями системи з інших процесів.

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


5

Скажіть компілятору, що робити, а не як це робити. Як приклад, foreach (var item in list)краще ніж for (int i = 0; i < list.Count; i++)і m = list.Max(i => i.value);краще ніж list.Sort(i => i.value); m = list[list.Count - 1];.

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

Зрештою (і це стосується всього програмування) мінімізуйте цикли та мінімізуйте те, що ви робите в циклах. Ще більш важливим є мінімізація кількості петель всередині ваших петель. Яка різниця між алгоритмом O (n) та O (n ^ 2)? Алгоритм O (n ^ 2) має цикл всередині циклу.


ironacly LINQ додає додаткову ковбасу, і варто задатися питанням, чи існує рішення без нього.
user3800527

2

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.