Комментарии 51
Теперь про оптимизации. Я не могу показать, что увеличение числа функций усложняет оптимизацию кода. Но достаточно очевидно, что оно их не упрощает. То есть в теории нам нет разницы, сколько вызовов функций есть, максима оптимизации кода всегда одна (или множество равнооптимальных). На практике же мы ограничены очень многими вещами, начиная от объёмов используемой памяти и заканчивая временем компиляции, а так же свойствами среды, такими, как многопоточное исполнение кода.
То есть, например, в том месте, где программисту очевидно, что эта данная (локальная/глобальная) переменная/поле объекта используется на запись только в одном потоке, компилятор такими знаниями не обладает. В том месте, где программист знает, что между вызовами двух процедур не произойдёт ничего, компилятор этого не знает. В конце концов, в том месте, где программист может в голове раскрутить рекурсивный вызов метода в цикл, компилятору может тупо не хватить глубины дерева, и он оставит не оптимальный вариант кода.
Я даже не говорю о том, что простое расчленение функции далеко не всегда корректно: очень часто приходится проверять различные предусловия, вроде проверки ссылки на null. Оставлять синтаксические единицы, которые не проверяют предусловия не корректно, а если проверки вставить в каждую функцию, компилятор далеко не всегда сможет понять, что между начальной и конечной точкой объект не сменит своё состояние, и оставит лишние, с точки зрения программиста, проверки.
Вот вам конкретный пример, хоть и немного отвлечённый от основной темы вашего вопроса: https://habrahabr.ru/post/309796/
А вы можете обосновать, что компилятору не сложнее оптимизировать кучку маленьких функций чем одну здоровую простыню?
Языки типа «жабоскрипта» в нормальных движках как минимум частично выполняются в виде нативного кода, особенно если динамичные возможности языка используются в умеренном стиле.
А в интерпретируемых языках, где нет нормального JIT, я бы о таких мелочах не сильно заботился, там в одной версии i += 1 может быть медленнее, чем ++i, а потом эта разница может быть нивелирована. Очевидную, вероятно, вещь написал…
И что, разве это имеет отношение к встраиванию? Да нет, не имеет, как в доме искали, так и будут искать. От силы будут оптимизации вроде кеширования адреса.
Так-то мне всё равно, однако меня нервируют подобные призывы «делайте больше функций» в разделе интерпретируемых языков. У меня и без того хром лагает. При этом, если бы в статье написали про то, что это счастье не бесплатное, то и ладно. Ан нет, не написали.
Пишу на luajit. Призываю — делайте больше функций. Маленькие функции оптимизируются лучше больших, потому что дают оптимизатору больше информации о структуре кода.
Для V8 это тоже справедливо.
Для V8 всё очень спорно. В каком-то месте всё работаает быстрее. В каком-то — медленнее. Стабильного результата тестов добиться, пожалуй, сложнее всего.
За остальных говорить не буду. Если для языков с сильной оптимизацией подготовка кода к оптимизации больше похоже на массонство, то для динамических языков это скорее техники вуду, в которых все твои действия направлены на ублажения духов. А что они пожелают в этот раз, фруктов или девственницу, и понравятся ли им твои дары — совершенно не известно.
мой вопрос касался интерпретируемых языков, в частности, JavaScript.
JS — очень даже компилируется. V8 компилирует его в нативный код, Rhino в байт-код JVM, etc.
переходов по DOM-дереву в поисках функции
DOM не имеет никакого отношения компиляции и выполнению кода
Теперь про оптимизации.
Раз был упомянут JS, давайте на примере движка V8. Есть ряд способов помешать V8 оптимизировать функцию (см, допустим, тут — https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments). К примеру, нам нужно использовать try/catch при парсинге json. Если такое происходит в большой функции, то она вся не оптимизируется. А если ее разбить на небольшие функции, то бОльшая часть из них будет оптимизирована. Неоптимизированной останется только та, в которой остался наш try/catch
Транслирует. Нет, конечно, сейчас могут набежать всякие умники и кричать «Ты не знаешь, что такое трансляция!», «Да ты не знаешь терминов!», «Кто тебя вообще на хабр пустил?!». А я под их крики и бурления минусов получаю из .class переоптимизированных файлов код, практически эквивалентный начальному. Нет, конечно, ряд оптимизаций выполняется на ходу, и из кешей можно достать весьма оптимальные… Нет, без смеха я не могу подобную хрень писать!
DOM очень даже имеет отношение, так как .js файлы нужно _парсить_. Быть может, я не до конца в тренде, и в v8 уже всё поменялось…
Хорошо, покажите мне цифры.
Транслирует. Нет, конечно, сейчас могут набежать всякие умники и кричать
А могут и не кричать, а попросить внятно объяснить разницу между трансляцией и компиляцией, как вы ее видите. Ибо в моем понимании преобразование JS кода в машинный (ассемблер) — это именно что компиляция.
DOM очень даже имеет отношение, так как .js файлы нужно парсить
Ок, нужно парсить. А DOM (Document Object Model) при чем? Или речь о чем-то вроде AST (Abstract Syntax Tree)?
Хорошо, покажите мне цифры.
https://gist.github.com/amakhrov/e52a9c1430d2103f676c75118aa5eba6
Для простоты обе функции объявлены в одном файле, но для бОльшей изоляции каждый раз запускаю только одну из них (последние две строчки — раскомментарена только одна)
-> node -v
v6.3.1
-> node perf.js
"mainSeparate()" duration: 9922ms
-> node perf.js
"mainInline()" duration: 10148ms
Разница небольшая, но в пользу варианта с разнесением кода по двум отдельным функцию.
http://pastebin.com/JrzYQngS
«mainSeparate()» duration: 1787.7ms
«mainInline()» duration: 1919.7ms
«mainSeparateNoExc()» duration: 1878ms
«mainInlineNoExc()» duration: 2004.2ms
«Mult1()» duration: 305.8ms
«Mult2()» duration: 257ms
«Mult1()» duration: 277.7ms
«Mult2()» duration: 261.6ms
«Mult1()» duration: 261.2ms
«Mult2()» duration: 236.4ms
«Mult1()» duration: 256.8ms
«Mult2()» duration: 258.5ms
«Mult1()» duration: 248.8ms
«Mult2()» duration: 233.3ms
«Mult1()» duration: 248.8ms
«Mult2()» duration: 231.4ms
«Mult1()» duration: 265.1ms
«Mult2()» duration: 232.6ms
«Mult1()» duration: 272.3ms
«Mult2()» duration: 231.8ms
«Mult1()» duration: 248.9ms
«Mult2()» duration: 231.9ms
«Mult1()» duration: 246.9ms
«Mult2()» duration: 231.2ms
>node -v
v6.6.0
Upd: Поторопился, простой вызов Date.now() отрабатывает ещё медленнее. К чёрту JS
http://pastebin.com/d7dns8yL
«Mult1()» duration: 15.8ms
«Mult2()» duration: 123ms
«Mult3()» duration: 123.1ms
«Mult1()» duration: 137.9ms
«Mult2()» duration: 123ms
«Mult3()» duration: 122.8ms
«Mult1()» duration: 137ms
«Mult2()» duration: 123.1ms
«Mult3()» duration: 122.9ms
«Mult1()» duration: 137ms
«Mult2()» duration: 122.9ms
«Mult3()» duration: 122.9ms
«Mult1()» duration: 136.8ms
«Mult2()» duration: 123.3ms
«Mult3()» duration: 122.9ms
«Mult1()» duration: 137ms
«Mult2()» duration: 122.9ms
«Mult3()» duration: 123.2ms
«Mult1()» duration: 137.9ms
«Mult2()» duration: 123.2ms
«Mult3()» duration: 123.1ms
«Mult1()» duration: 137.2ms
«Mult2()» duration: 131ms
«Mult3()» duration: 136ms
«Mult1()» duration: 138.1ms
«Mult2()» duration: 122.7ms
«Mult3()» duration: 122.6ms
«Mult1()» duration: 136.8ms
«Mult2()» duration: 122.5ms
«Mult3()» duration: 122.6ms
Из которого видно, что накладные вызовы 10 вызовов методов составляют примерно 10% про полном отсутствии остального кода. Не то, чтобы это было много.
которые замечательно инлайнятся в нормальных компилируемых языках
Какой-нибудь полиморфный вызов метода в джаве — и инлайна уже не будет.
С другой стороны, V8 тоже отлично умеет инлайнить. Напр., http://www.mattzeunert.com/2015/08/21/toggling-v8-function-inlining-with-node.html
Меня жаваскрипт интересует прежде всего как браузерный скриптодвижок. Насколько я могу судить, node.js в плане оптимизаций далеко впереди браузерной версии, как минимум, потому, что у браузера есть всего 3 секунды на разбор полотна скриптов, что не даёт особого простора для оптимизации.
https://github.com/mrdoob/three.js/pull/8938 Я всего лишь избавился от вызова функции, сделав её, условно говоря, инлайновой, получил от этого более 20% прироста производительности. В действительности, зависит от задач. На скорость выполнения это безусловно влияет и в худшую сторону, иногда даже сильно, покуда вызов функции обходится дорого. С другой стороны, есть обратная сторона медали. v8 не может компилировать функции в нативный код если у них выбрасывается исключение (есть try-catch блок, точнее). Таким образом, если в одной большой функции будет try-catch блок, то не будет оптимизирована вся логика, а если часть этой функции вынести — то можно получить большой прирост. Но это уже совсем ниньзя-техники оптимизации.
Строго говоря, вы не просто заинлайнили функцию. Вы еще и общую логику кода изменили.
было:
te[0] = a;
t[0] = t[0] * detInv; // посредством вызова метода
стало:
te[0] = a * detInv;
На одно чтение/запись элемента массива меньше — это само по себе оптимизация.
Так что из данного примера неочевидно, что именно вынос кода в отдельную функцию дает ощутимый эффект.
Безусловно. Однако так или иначе, вызов функции — операция крайне накладная: много операций со стеком, виртуальными таблицами (не всегда, зависит от реализации), промахи по кешу, сброс предсказателя ветвлений — скрытых эффектов вагон и целая тележка, которая еще и слабо контролируется (в случае с js — никак не контролируется)
Если просто заинлайнить метод — скорость будет одинаковой, что и с вынесеным методом.
Однажды мой коллега уволился, потому что пытался справиться с REST API на Ruby, который было трудно поддерживать.
Слабак :)
Слабак :)
Слабак остался бы, и преумножал г-но.
Если бы было написано, что-то вроде «Однажды мой коллега уволился, потому что пытался справиться с REST API на Ruby, который было трудно поддерживать, но руководство не разрешило выделить время на рефакторинг», то я бы не написал «слабак» :)
но руководство не разрешило выделить время на рефакторинг
Там так и написано: «Клиент не хотел перестраивать приложение, делать ему удобную структуру, и разработчик принял правильное решение — уволиться.»
Очевидно, что нужно разбивать сложные функции на части. Но у меня часто возникает проблема из-за того, что внутренние функции очевидно не будут где-либо повторно использованы. Довольно часто это специфическая функция со специфическим набором параметров и специфическим результатом. И как её тогда называть? Использовать имя родительской функции в качестве префикса? Более того, если мы выносим код в отдельную функцию, то появляется вопрос проверки входящих параметров, который наверняка выполнялся в родительской функции. В родительской функции мы точно знали, что параметры верные, а тут получается мы или ничего не проверяем, что плохо для самостоятельности новоиспеченной функции, или в очередной раз проверяем то, что уже проверяли раньше, что негативно сказывается на быстродействии. В основном я программирую на php и там у меня с этим совсем беда, так как в языке нет локальных функций. Их наличие могло исправить ситуацию за счет того, что внутренние функции имеют локальную область видимости, а значит не должны иметь уникальные понятные имена, плюс не могут быть вызваны из вне, а значит нет смысла по несколько раз проверять параметры.
let myArray = [null, { }, 15]; let myMap = new Map([ ['functionKey', function() {}] ]); let myObject = { 'stringKey': 'Hello world' }; getCollectionWeight(myArray); // => 7 (1 + 4 + 2) getCollectionWeight(myMap); // => 4 getCollectionWeight(myObject); // => 2
function getMapValues(map) {
return [...map.values()];
}
Вот это вообще гениально. На ровном месте создаётся обёртка для синтаксиса языка, которая не несёт вообще никакой пользы.
Просто представьте, что через пару лет после написания этого кода у вас возникла ошибка — вы захотели вывести несколько коллекций в порядке возрастания суммы, но результат вас удивил — порядок явно не тот. В чём проблема — неправильные исходные данные ('null' вместо null), ошибка в сортировке или в функции определения веса?
Вы начинаете копать функцию определения веса и видите этот спагетти-код из микро-функций, которые разбросаны по разным местам разных файлов вашего проекта (код-то «переиспользуемый»). И каждый раз, встретив функцию, вам нужно её индивидуально искать, разбираться, что она делает, и потом возвращаться к тому месту, где остановились.
Не забывайте, мы лишь рассматривали простую функцию подсчёта веса коллекций. Примитив. А теперь представьте, что у нас что-то сложнее и ближе к реальному миру — например, функция, которая определяет, сколько нужно коробок, чтобы переслать N предметов разных габаритов (с учётом всех нюансов — габаритов, ограничений по весу, требований не класть в одну коробку тяжёлое с хрупким, таможенных особенностей). Сколько там можно написать микро-функций? Сколько времени у вас займёт найти среди них ошибку округления?
делать ему удобную структуру
Клиент и не должен делать удобно разработчику.
Разработчик должен доходчиво объяснить клиенту зачем надо выделять время не рефакторинг, а не просто сказать, что надо ХХХ часов, чтобы мне стало проще поддерживать эту программу.
Ситуация напоминает ситуацию с нормализации реляционных баз данных: вроде теоретически для каждого факта должна быть своя таблица с внешними ключами, но на практике это ужасно неудобно. Думаю, во всем должно быть чувство меры…
К счастью, ES2015 позволяет объявить const как read-only,
Можно использовать формат модулей CommonJS
Проблема хорошо видна. Функция getCollectionWeight() слишком раздутая и выглядит как черный ящик, полный сюрпризов.
Проблема отчетливо видна в другом. Ни одного комментария в коде по поводу что эта функция делает, stateless она или нет и прочие детали.
Лично мне плевать как функция написана внутри, если я знаю, что она протестирована и работает так как описана.
Если сильно приспичит — разберусь и с разбитой на подфункции и с непрерывным кодом одним блоком.
Особенно если код разбит осмысленно (здравый смысл) на блоки и прокомментирован.
А мой — это какой и даже точнее — чей? К такой переменной, которая моя, хочется обращаться отовсюду: моя же, что хочу, то и делаю; а она хрясь и локальная. Логичнее префикс loc[al].
Насчёт двадцати строк тоже сомнительно: МакКонелл писал о семи, ссылаясь на свойство внимания и памяти о семи объектах.
Ну и, наконец, видно и написано, что статья — концентрат содержания других источников. Хорошо бы ссылки для полноты.
Я думаю отступы и скобки блоков в их число не входили, а с ними это как раз и будет около 20 реальных строк.
Аргументы за простые и маленькие функции:
1) Каждую в отдельности легко прочитать. Меньше сущностей нужно уложить в голову, когда воссоздаётся модель поведения.
2) Легче покрывать код тестами — относительно мало инвариантов, которые нужно проверять.
Против:
1) Увеличение связности кода. Т.е. меняя функцию в глубине, можно сломать вышележащие. На самом деле сложность из самой функции перемещается в связи между ними. У каждого кусочка кода растёт количество способов его использования. Может возникнуть ситуация, когда приходится какой-то кусочек обобщать настолько, что на самом деле было бы проще раскопипастить частные случаи выше по стеку. (Нарушили случайно single responsibility и ой).
2) Большое количество переключений контекста при написании кода (не очень страшно) и багфиксинге, который будут делать скорее всего другие люди и через полгода-год (а вот это беда). Т.е. чтобы понять почему вылетело то или иное исключение нужно пройтись сверху донизу (или снизу доверху), собрав в голове историю создания контекста в месте падения. IDE и дебаггеры немного купируют эту проблему, но не до конца.
Искусство написания простых и коротких функций