Як писати ефективний код, думаючи про перформанс? Або performance-aware-programming

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

Хто я

Я 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. Наступні історії покажуть різні приклади коду та те, як їх можна оптимізувати. Я докладу всіх зусиль, щоб пояснити, чому саме зміни, які я запропоную, роблять код ефективнішим, щоб ви могли зрозуміти логіку, що стоїть за цим, і мали змогу застосувати цей підхід до різних ситуацій.

Висновок

Головна мета тут — показати, що можна написати кращий, ефективніший код без зайвих зусиль. Це не важко, але результат величезний. Ігнорування цих можливостей під час розробки означає уникнення змін інженерної культури, яка приведе всіх до більш ефективного програмного забезпечення та веб-сайтів.
Сьогодні будь-яке програмне забезпечення, яке ви використовуєте, може працювати швидше та плавніше. Але в якийсь момент розробники програмного забезпечення почали цінувати комфорт (можливо) для людей, для програмістів набагато більше, ніж швидкість коду, який вони пишуть. Більшість інженерів втратили ментальний зв’язок між кодом, який вони пишуть, і тим, як він виконуватиметься машиною. Занадто багато гібагітів оперативної пам’яті та диска, занадто швидкі процесори змусили всіх думати, що це нормально не піклуватися про продуктивність, доки ваші користувачі/клієнти не почнуть на це скаржитися.
Це не добре. Нам потрібно це змінити та повернутися до нормального режиму, коли ви пишете швидкий і ефективний код, замість роздутих монстрів, які виконують роботу задля роботи, а не лише вирішують поставлену задачу.

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

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось1
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Вітаю,
Непогане інтро, але хотілось би розуміти якою МП написані приклади- для нефахівця то все не дуже явно виглядає

Ви кажете, що краще оминати зовнішні функції, розуміти що відбувається під капотом, тощо

Уявімо, що в мене є табличка в БД з товарами на складі умовно назва, кількість в наявності, ціна за одиницю в у.о. . І я щодня туди дописую а) нові товари якщо такі зʼявилися б) оновлюю кількість наявних товарів та перераховую загальну вартість товару на складі в гривнях (по курсу НБУ щодня)

Як би Ви вирішували цю задачу з точки зору найефективнішого коду вбудованих функцій ну і з найменшою кількістю ASM інструкцій?
🤓

Стаття нагадує лозунг: Кінець епохи «неефективного» коду! Тільки кожен зрозуміє своє в залежності від domain-у в якому працює. Хтось для себе якийсь висновок зробив зі статті, пишіть в відповідях.

В примере 1 разницы в быстродействии не представлено. Разница в обьеме памяти в 200 инструкций попросту смешна, чтобы ее обсуждали всерьез. Подсказка: не все инструкции выполняются последовательно.
Пример 2 не выдерживает никакой критики, ибо Мшыфгд Ыегвшщ 20 лет назад и сейчас попросту не сопоставимы по своему feature set: ни с точки зрения компилятора, ни с точки зрения редактора, ни с точки зрения автоподстановок и команд рефакторинга и так далее по списку.
Если это был троллинг — то улыбнуло, в противном случае все печально.

Підтримую. Одне діло оптимізувати запити в базу чи у рідких випадках займатись оптимізацією алгоритма по bit O notation там де використовується обробка великих данних. Чи навіть викинути той lodash нафіг заради кілобайтів або використовувати не lodash.get, а імпортувати функції окремо щоб не тягнути до бандлу зайвого з бібліотеки (хай tree shaking зробить свою справу). Інше діло говорити що на if замість виклику функції можно зекономити кілька наносекунд)) ну а якщо це не допомогло то може слід сервіс написати на Go чи rust? Ну а якщо на фронті такі проблеми то зробити тонкий клієнт замість товстого наприклад...

Знову вимушений повторити — я пишу про те, щоб писати код розуміючи як саме він буде виконуватися процесором, бо в такому випадку ви зможете писати, використовуючи більш ефективні конструкції.
Я з вами абсолютно згоден, що для кожної задачі є своє унікальне рішення, і якщо стоїть задача зробити великий додаток то треба і вибирати відповідні технології.
Також ви пишете про «кілька наносекунд» і якось трошки це зневажливо звучить, саме про таке відношення я і кажу — треба трошки більше цьому приділяти уваги. От наприклад в IntelliJ IDEA в 2016 році було приблизно 3,5 мільйонів рядків, в ядрі Linux 30 мільйонів, в Android 12 мільйонів (цифри не 100% точні і можу помилятися). Уявіть що десь10-20% цього всього коду може працювати швидше або повільніше, ви б помітили цю різницю? Пишучі код одразу ефективним робить ваш продукт кращим. 🙂

Швидше на скільки? Якщо б на 20 відсотків то можливо й помітив. Але ж може бути так, що ці оптимізації разом дадуть 1 відсоток в кращому випадку. Бо наприклад для ide треба читати файли, а це вже невелике але лейтенсі. І вже воно може перекрити мікроортимізації процесора. А з приводу lodash. Чи є у вас приклад того як його уникнувши ви змогли помітно пришвидшити апу? Можливо на процессінгу big data. А от на фронтенді чи бекенді зі стандартним крудом?

Перечитайте статтю ще раз, бо очевидно ви прочитали приблизно кожен другий рядок, інакше не пояснити чому ви не зрозуміли про що там написано...
Приклад 1: я пишу не про конкретні цифри на скільки представлений мною приклад швидший або повільніший, я пишу про те, щоб розуміти чому один підхід ефективніший і чому. Я вважаю що пишучи код треба намагатись робити його ефективним.
Приклад 2: перечитайте, ви знову упустили важливу деталь — я пишу про вікно локальних змінних дебаґґера. Як ви думаєте які такі нові фічі туди можна додати щоб воно стало на стільки повільним, причому не помітити значне сповільнення в його основному функціоналі?
Там файл розміром 1.5Mb, що саме можна так довго там аналізувати щоб такий «проєкт» так довго почав відкриватися? Чи маєте ви розуміння на скільки реально швидкі наразі носії інформації і їх інтерфейси. Спробуйте трошки поміркувати і уявити скільки дійсно часу потрібно щоб прочитати файл розміром 1.5Mb з диска і записати в ОЗУ. Ну і потім його проаналізувати...

Як ви там казали — «Якщо це був троллінг, то улибнуло, інакше все сумно»

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

Статья не заслуживает внимательного чтения.

я пишу про те, щоб розуміти чому один підхід ефективніший і чому.

В том то и дело, что вы не пишите «почему» — сравнения производительности нет. Есть сравнение «в лоб» обьема потребляемой памяти, которое вы не удосужились даже в байты перевести.

Я вважаю що пишучи код треба намагатись робити його ефективним.

Preliminary optimization is the root of all evil© D. Knuth. Читайте классиков и узрите бездны.

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

Я вам повторяю: вы сравниваете 2 совершенно разных продукта, которые работают с совершенно разными компиляторами, сгенерированной ими отладочной информации, возмодночти вычислять значения на лету. К тому же нынешний VS кроссплатформенный, поддерживает удаленную отладку и так далее по списку.

Там файл розміром 1.5Mb, що саме можна так довго там аналізувати щоб такий «проєкт» так довго почав відкриватися?

Вы представляете как поменялся стандарт С++ и насколько сложней стало построение AST?
Сколько include надо вычитать с диска, проанализировать и включить в построение AST?
Возьмите CLion и попробуйте в нем все тоже — там должно быть лучше потому что продукт на 30 лет моложе, доложите о результатах.
——
Разговор мужа с женой по телефону:
— дорогая, по радио срочные новости: какой то идиот едет по встречке. Ты как раз в том же районе ведешь машину. Будь осторожна.
— Один??? Да их тут сотни!!!

Preliminary optimization is the root of all evil© D. Knuth. Читайте классиков и узрите бездны.

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

Код

int sum = 0;
for (int i=0; i<10; ++i) {
  sum += data[i];
}

Стає

int sum = 0;
for (int i=0; i<10; ++i) {
  sum += data[i];
  ++i;
  sum += data[i];
}

І це дійсно пришвидшувало виконання. Цим дійсно займалися, і це дійсно заплутувало код.

Дык автор статьи предлагает ровно тоже самое: оптимизировать код вместе оптимизатора. У него пример ровно такой же, «давайте напишем маленькую функцию вместо использования стандартной — станет лучше», не задумываясь о том что маленькая свмописная функция запутывает код и источник багов.

Доводилось працювати на проектах де підключались бібліотеки без яких можна обійтись, але їх використання це модно. Де використовується новіша технологія, яка довше компілюється і вимагає більше памяті, але код гарніше виглядає. Де пишуться 2 однакові класи і в результаті в пам’яті 2 об’єкти з однаковими даними лише тому, що так виглядає стандартний патерн. Або дублюється код, але має різну назву і теж через патерн. А «складні» перевірки типу if (...) if else (...) else в межах однієї функції не тільки не гарні, а ще й не читабельні. Для таких людей лишнє використання ресурсів не аргумент, а от гарненький код і використання чогось дуже популярного і новенького, це — аргумент. Інколи здається, що за перформанс вже ніхто не думає... Чи це додаткові наносекунди чи лишні мегабайти памяті, але це на роки і для багатьох це має значення. Дякую за статтю!

Саме так і є. Таке враження, що люди втратили якусь культуру відповідальності за свій код або за свій продукт. Я ще чув таку фразу колись, в контексті веб-додатків, — «cpu на стороні клієнта безкоштовний от хай і маслає».
Тому я й хочу почати говорити про це, може хоч на одного інженера який не забуває про перформанс стане більше, або хоч ті шо є не будуть почуватися самотніми в цьому питанні 🥹

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

Important Latencies:
Component Time (nanoseconds)

L1 cache reference 0.9
L2 cache reference 2.8
L3 cache reference 12.9
Main memory reference 100
Compress 1KB with Snzip 3,000 (3 microseconds)
Read 1 MB sequentially from memory 9,000 (9 microseconds)
Read 1 MB sequentially from SSD 200,000 (200 microseconds)
Round trip within same datacenter 500,000 (500 microseconds)
Read 1 MB sequentially from SSD with speed ~1GB/sec SSD 1,000,000 (1 milliseconds)
Disk seek 4,000,000 (4 milliseconds)
Read 1 MB sequentially from disk 2,000,000 (2 milliseconds)
Send packet SF->NYC 71,000,000 (71 milliseconds)

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

Думаю ви праві, що для формошльопства або крудошльопства це скоріше за все не дуже буде помітно 🙂

Загалом, вдячний автору за підняття теми «performance-aware-programming». Особисто мені ця тема цікава, проте технології, що були наведені в прикладі — ні (бо не працюю з JS).
Вважаю, що також важливим аспектом всієї цієї теми в розрізі компілюємих мов програмування є оптимізації компілятора та особливо його нішові оптимізації.
Так, в деяких випадках умовний gcc виграє clang і навпаки.
Отже, моя теза поляга в тому, аби вивчати технології на яких базується твій код, а не сліпо мімікрувати бест-практіси інших мов програмування, бо вони виглядають круто.

Дякую!
Є ще цікавий кейс, пов’язаний з тим, що Clean Code іноді сильно заважає компілятору зрозуміти як оптимізувати написаний код, бо він розкиданий маленькими шматочками по багатьом файлам. І тому вам зручно з таким кодом працювати, а компілятору — ні 🙂
Але треба тримати баланс і не перегинати сильно щоб не провалюватись в оверінжиніринг.

Для написання коду з огляду на продуктивність, по-перше, потрібно мати та знати нефункціональні вимоги (NFR). Не можна писати код, не розуміючи умов його використання, але, на жаль, розробники часто цього не розуміють. По-друге, надзвичайно важливо мати спостережуваність (observability) над системою, без цього взагалі нічого не можна сказати. І вже після цього можна займатися усуненням несправностей, оптимізацією та іншими «покращеннями».

Одним із таких прикладів є те, що я просив людей у ​​моїй компанії припинити використання lodash.get() і замість нього використовувати obj?.prop (optional chaining).

Ви не дали визначення, тому будемо виходити з даного прикладу:
Порівнювати ці речі в контексті перформансу — це беззмістовне задродство. Числодробилку на джавасріпті ви навряд чи писати будете, тому доступ до полів об’єкту не буде вузьким місцем, оптимізація роботи з БД чи якогось іншого ІО, або флоу обробки даних (доцьний їх об’єм) дадуть краще результат.

Проблема з lodash.get і obj?.prop в тому що вони покривають різні сценарії:
— lodash.get — доступ до динамічного шляху в об’єкті (на етапі білда ми не знаємо які поля нам потрібні); цей сценарій ви не зауриєте опшинал чейнінгом;
— obj?.prop — доступ до значення з хендлінгом пустих значень; для цього сценарію lodash.get банально погіршує читабельність і перевірку типів у випадку з тайпскріптом.

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

Для мене це завжди було очевидно — вбудовані фічі будуть ефективнішими ніж сторонні.

А для людини, що знає про JIT або в принципі знайома з тим як працюють компілятори та інтерпретатори це ніфіга не очевидно.
2 приклади:
— В джаві були проблеми, що елементи стандартної бібліотеки працювали гірше ніж кастомні ліби, бо мали покривати більше типових сценаріїв. Прилад який ще напевне буде відтворюватись — це мап з примітивними типами (за рахунок боксінгу) і наче навіть строками використовував більше пам’яті і був повільніше ніж Trove (trove4j.sourceforge.net/html/benchmarks.shtml)
— Давно в ноді (і наче в хромі теж таке було) map/forEach і тд працювали повільніше ніж цикл for, бо він був типовим сценарієм і тому був оптимізований, тому кастомний метод мап через цикл працював швидше.

UPD.

Давайте подивимося на lodash.isString

Давайте
> var a = new String("a");
undefined
> a
String {’a’}
> _.isString(a)
true
> typeof a
’object’

Дякую за коментар. Згоден з вами — не вистачає контексту, тому я оновив приклад про який ви кажете і дописав трошки контексту, вказуючи на те, що використання optional chaining замість lodash.get() має сенс здебільшого для випадків, коли нам не треба динамічний шлях до пропа.

Числодробилку на джавасріпті ви навряд чи писати будете, тому доступ до полів об’єкту не буде вузьким місцем

Я саме про це і кажу. Більшість так думає і пише не задумуючись що можна трошки ефективніше це зробити. В тексті є посилання на іншу мою стару статтю на medium.com — Why you should use optional chaining instead of lodash.get? де я саме міряв overhead від lodash.get() в порівнянні з optional chaining в продакшн проєкті. Я працюю над великим проєктом з великою командою, і у нас є сторінки які після оновлення викликають lodash.get() від 1200 до 25000 разів, це приблизно від 14-310мс процесорного часу, в брузері на швидкому комп’ютері, які можна зберегти просто замінивши lodash.get() на optional chaining.

— В джаві були проблеми, що елементи стандартної бібліотеки працювали гірше ніж кастомні ліби, бо мали покривати більше типових сценаріїв. Прилад який ще напевне буде відтворюватись — це мап з примітивними типами (за рахунок боксінгу) і наче навіть строками використовував більше пам’яті і був повільніше ніж Trove (trove4j.sourceforge.net/html/benchmarks.shtml)

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

— Давно в ноді (і наче в хромі теж таке було) map/forEach і тд працювали повільніше ніж цикл for, бо він був типовим сценарієм і тому був оптимізований, тому кастомний метод мап через цикл працював швидше.

Трошки не погоджуюсь з вами в цьому конкретному випадку. Адже стандартний for працює швидше бо йому не треба для кожної ітерації викликати функцію, і плюс до цього він по-іншому використовує event loop. Тому він «працює швидше» лише тому, що процесору треба робити менше роботи і event loop буде заповнений по-іншому.

new String("a«)

В нас таке не використовується 🙂
Але навіть якщо мій тестовий код трошки дописати і замість isString(variable) написати typeof variable === 'string' || variable instanceof String то godbolt.org показує що v8 скомпіює це в 212 ASM інструкцій, тобто навіть тут буде швидше, але трошки більше треба писати. Щодо «читабельності» то це дуже суб’єктивне поняття і цей приклад для мене однаково читабельний як з lodash так и без нього.

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

Спробуйте перечитати комент, я зовсім про інше писав :)

Трошки не погоджуюсь з вами в цьому конкретному випадку. Адже стандартний for працює швидше бо йому не треба для кожної ітерації викликати функцію, і плюс до цього він по-іншому використовує event loop. Тому він «працює швидше» лише тому, що процесору треба робити менше роботи і event loop буде заповнений по-іншому.

1) Ви «непогоджуєтесь» з перевіреним фактом.
2) Самописні функції через цикл, що приймали функцію обробки, працювали швидше, тобто перша «інтуітивна здогадка» — помилкова. Яким чином у вбудованих мап/фоЕач задіяний евентлуп з ходу не знаю, могли б ви пояснити?

В нас таке не використовується 🙂

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

Щодо порівняння for і map/forEach — думаю я розгляну ці кейси в наступних статтях. Тоді і буде видно результат 🙂
А на разі я залишусь при своїй думці — виклик функції буде повільніший, ніж виконання «на місці».

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

Я «виокремив» з коду lodash.isString тільки позитивний шлях виконання. В реальності там значно більше буде виконуватись роботи.
А що саме для вас є «критичним»?
Розуміння того, що ваша програма/веб-додаток, може працювати швидше, якби ви і ваші колеги писали ефективніший код, якось додає критичності цій ситуації?

Мушу повторити — мій посил тут про те, що треба більше думати про те, як саме ваш код буде виконуватись. Не ігнорувати «невеличкі» або «не критичні» оптимізації коду. Треба тримати баланс і не скочуватись в оверінжиніринг або недоінжиніринг. В кожній мові програмування, фреймворку, бібліотеці, утілітах це все буде означати різні техніки, випадки і приклади.

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

Підписатись на коментарі