Як писати ефективний код, думаючи про перформанс? Або performance-aware-programming
Чи думали ви колись що іноді програми
Хто я
Я Staff Engineer із Totango і хотів би поділитися з вами чимось цікавим. Я сподіваюся, що це допоможе комусь створювати краще програмне забезпечення з менш роздутою неефективною кодовою базою.
Про що ця серія публікацій
Деякий час тому, завдяки стрімеру і хорошому професіоналу ThePrimeAgen, я дізнався про Casey Muratori та його ідею програмування з урахуванням продуктивності коду, Performance-aware-programming. У Casey є курс, де ви можете дізнатися більше про цю ідею - «Perfromance Aware Programming» (en).
Ця ідея вразила мене настільки глибоко, що я відчув — це те, що я шукав багато років! Я хочу показати, як я бачу це зі своєї точки зору.
Ідея полягає в тому, що незважаючи на мову чи фреймворк, з якими ви працюєте, ви завжди повинні розуміти або «відчувати», як саме комп’ютер виконуватиме код, який ви пишете.
Це єдино правильний спосіб
Ні! Краса інженерії полягає в тому, що існує багато різних способів вирішення однієї проблеми. У кожної людини свій шлях. Вся справа в самій проблемі та компромісах, які ви можете собі дозволити для її вирішення.
Щоб уточнити — Я не намагаюся дати вам срібну кулю чи «єдино правильний спосіб» написання коду. Вся справа в балансі. Необхідно підтримувати баланс між читабельністю та продуктивністю коду. Ви повинні зробити його легким для тестування, легким для розуміння, але в той же час — швидким. Ви не можете продовжувати вимагати від комп’ютера робити багато речей, тоді як лише 30% від вашого коду або навіть менше — це те, що вам дійсно потрібно зробити, щоб вирішити проблему для вас. Іншими словами — ви пишете 1000 рядків коду, а по факту вам треба лише 300 рядків. В обох випадках проблема вирішена і результат вас задовольняє.
Що таке performance-aware-programming
Це мислення або підхід, який ви використовуєте, при написанні коду. Пишучи код, ви начебто розумієте, як комп’ютер насправді його виконуватиме. Як це працює з різними типами даних і чи дійсно вам потрібно використовувати цей тип даних у цьому конкретному випадку. Як саме він намагатиметься виконати різні математичні операції — додавання, віднімання, множення, ділення, квадратний корінь тощо...
Особисто для мене це сприймалося як «витрати». Я розумію, що призначення цілого числа змінній, скажімо, «коштуватиме мені 1 грн», а виклик функції коштуватиме мені 4 грн, виклик до БД — 20 грн тощо... І коли ви починаєте бачити все це таким чином — почніть звертати увагу на ці речі і намагайтеся уникати «дорогих операцій». Ви будете писати ефективний код, який не робитиме нічого зайвого. Він буде робити тільки те, що вам потрібно і нічого більше.
Це завжди було місце для конструктивних дискусій із цікавою аргументацією між мною та моїми колегами про те, «чому я прошу людей не використовувати якісь зовнішні функції, якщо можна використовувати щось внутрішнє». Одним із таких прикладів є те, що я просив людей у моїй компанії припинити використання lodash.get()
і замість нього використовувати obj?.prop
(optional chaining). Для ситуацій, коли вам не потрібний динамічний шлях до конкретного poperty. Для мене це завжди було очевидно — вбудовані фічі будуть ефективнішими ніж сторонні. Але люди не погоджувалися, тому кілька років тому я провів невелике дослідження, щоб отримати цифри, які підтверджують мою точку зору і написав невеличку статтю про це — Why you should use optional chaining instead of lodash.get?
Асамблер
В якості універсального мірила для «вимірювання» вартості операцій в коді Кейсі Мураторі вирішив використовувати ASM. Я згоден з ним, оскільки при компіляції будь-який код, який ми пишемо, перетворюється на інструкції ASM, а центральний або графічний процесор виконує їх.
Це означає, що якщо ви пишете код і зможете «побачити», що він буде врешті-решт перетворений (скомпільований) у 12 інструкцій ASM, але ви можете переписати кілька рядків цього коду та зробити його 6 інструкціями ASM — технічно ви «попросите» комп’ютер виконати в 2 рази менше роботи, щоб досягти того самого результату.
Це схоже на моє уявлення про вартість операції, але замінює «фальшиві гривні» на «перевірені інструкції ASM» 😎.
Приклад 01: Допоміжні функції замість вбудованих конструкцій
Я знову буду використовувати допоміжну функцію з бібліотеки lodash
.
Давайте подивимося на lodash.isString
.
Ось її вихідний код:
/** * Checks if `value` is classified as a `String` primitive or object. * * @static * @since 0.1.0 * @memberOf _ * @category Lang * @param {*} value The value to check. * @returns {boolean} Returns `true` if `value` is a string, else `false`. * @example * * _.isString('abc'); * // => true * * _.isString(1); * // => false */ function isString(value) { return typeof value == 'string' || (!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag); }
В кращому випадку буде виконано лише оце:
typeof value === 'string
Тому, якщо ми напише такий код (просто для тестування):
function isString(value) { return typeof value == 'string'; } const CONST_NUMBER = 7; function importantFunction(variable){ // check if variable is not a String if (isString(variable)) { return variable + CONST_NUMBER; } // otherwise just return it as it is return variable; }
Я просто хочу перевірити різницю коли нам треба викликати функцію і коли не треба.
Згідно з сервісом godbolt.org цей код(тіло функції importantFunction
) буде скомпільовано в 336 ASM інструкцій.
Але якщо ми трошки змінемо наш тестовий код і не будемо викликати isString(variable)
, а замість цього будемо робити перевірку на місці — typeof variable === 'string'
.
То отримаємо такий код:
const CONST_NUMBER = 7; function importantFunction(variable){ // check if variable is not a String if (typeof variable === 'string') { return variable + CONST_NUMBER; } // otherwise just return it as it is return variable; }
Тепер тіло функції importantFunction
компілюється всього в 172 ASM інструкції:
Тобто, якщо ви просто припините викликати функцію та зробите перевірку на місці, це буде в 336 / 172 = 1,95 (!) разів менше роботи для процессора, але результат буде такий самий.
Пам’ятайте — це сценарій «у найкращому випадку», у гіршому випадку він викличе ще 3 функції, щоб дізнатися, чи є змінна об’єктоподібним значенням. Так буде набагато повільніше.
Приклад 02: Код Visual Studio (старий великий, а не VSCode).
У цьому відео Кейсі Мураторі - Twitter і Visual Studio Rant. У нього була якась ситуація з підтримкою команди Visual Studio. Він виявив, що коли ви використовуєте дебагер і клацаєте «Наступний крок» з невеликою затримкою, вікно «локальних змінних» в дебагері не оновлюється миттєво. Це означає, що якщо вам потрібно побачити, де змінюється якась змінна, і ви робите це не вперше, ви перейдете до наступного рядка досить швидко, і далі до наступного і т.д., але ви не побачите жодних змін у вікні локальних зміннних, доки не зупините клацання «занадто швидко». Крім того, він виявив, що коли він відкриває простий файл «.exe» розміром 1,5 Мб — для відкриття такого проєкту потрібно занадто багато часу. Це повинно бути миттєво. Він намагається відкрити той самий «.exe» в RemedyBG, і він відкривається менш ніж за секунду. Миттєво.
Потім він йде далі і відновлює свій старий ПК з Windows XP.
Ось його характеристики:
- Intel Pentium 4 CPU 2.20GHz
- 512Mb RAM
І коли він використовує стару версію Visual Studio (на той час вона мала назву Microsoft Visual C++) з тим самим файлом «.exe» - він відкривається миттєво, а налагоджувач працює плавно та миттєво оновлює «локальні змінні».
Іншими словами — «20 років тому це працювало швидше, ніж зараз». На набагато повільніших ЦП і ОЗУ(!). Якби вони не оновлювали програму 20 років — вона б працювала швидше. Вони змусили софт працювати повільніше та гірше!
Чого очікувати далі
Це перша стаття з серії. В основному я працюю з JavaScript, і тому я хочу показати приклади використання JavaScript. Наступні історії покажуть різні приклади коду та те, як їх можна оптимізувати. Я докладу всіх зусиль, щоб пояснити, чому саме зміни, які я запропоную, роблять код ефективнішим, щоб ви могли зрозуміти логіку, що стоїть за цим, і мали змогу застосувати цей підхід до різних ситуацій.
Висновок
Головна мета тут — показати, що можна написати кращий, ефективніший код без зайвих зусиль. Це не важко, але результат величезний. Ігнорування цих можливостей під час розробки означає уникнення змін інженерної культури, яка приведе всіх до більш ефективного програмного забезпечення та веб-сайтів.
Сьогодні будь-яке програмне забезпечення, яке ви використовуєте, може працювати швидше та плавніше. Але в якийсь момент розробники програмного забезпечення почали цінувати комфорт (можливо) для людей, для програмістів набагато більше, ніж швидкість коду, який вони пишуть. Більшість інженерів втратили ментальний зв’язок між кодом, який вони пишуть, і тим, як він виконуватиметься машиною. Занадто багато гібагітів оперативної пам’яті та диска, занадто швидкі процесори змусили всіх думати, що це нормально не піклуватися про продуктивність, доки ваші користувачі/клієнти не почнуть на це скаржитися.
Це не добре. Нам потрібно це змінити та повернутися до нормального режиму, коли ви пишете швидкий і ефективний код, замість роздутих монстрів, які виконують роботу задля роботи, а не лише вирішують поставлену задачу.
Слідкуйте за цією серією публікацій, щоб побачити більше цікавих способів написання більш ефективного коду!
21 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарів