Как стать автором
Обновить

Комментарии 51

Вот мы и дожили до переоткрытия структурного программирования (нисходящего проектирования). Скоро кто-то догадается, что функции можно компоновать с данными, и даже описывать классы таких компоновок.
Язык должен способствовать написанию маленьких функций. Хотя бы что бы на каждую не требовалось писать длинные слова function и по несколько return.
Маленькая функция — это не та, в которой мало буков, а та в которой мало действий.
Обидно, когда букв приходится писать существенно больше, чем действий, которые они описывают. По этому если для написания лишней функции приходится набирать много лишних слов, возникает сильное желание объединить их в одну большую.
Интересно, а как это влияет на скорость работы кода? Даже компилируемые языки с их оптимизациями и встраиванием не могут 100% оптимизировать код, а уж в жабоскрипте…
НЛО прилетело и опубликовало эту надпись здесь
Здоровая простыня находится в одной единице трансляции, а куча маленьких файлов может быть разбросана по различным объектным файлам. С подобным может справиться ICC, но на больших проектах это приведет к взрывному росту времени компиляции.
Прежде всего, мой вопрос касался интерпретируемых языков, в частности, JavaScript. Не нужно быть гением, чтобы понимать, что чем больше нам нужно сделать переходов по DOM-дереву в поисках функции, тем дольше будет выполняться код. И чем меньше операций выполняет каждая вызываемая единица, тем больше становятся накладные расходы. Я поинтересовался конкретными цифрами, с чего вы сагрились, я не понимаю.

Теперь про оптимизации. Я не могу показать, что увеличение числа функций усложняет оптимизацию кода. Но достаточно очевидно, что оно их не упрощает. То есть в теории нам нет разницы, сколько вызовов функций есть, максима оптимизации кода всегда одна (или множество равнооптимальных). На практике же мы ограничены очень многими вещами, начиная от объёмов используемой памяти и заканчивая временем компиляции, а так же свойствами среды, такими, как многопоточное исполнение кода.
То есть, например, в том месте, где программисту очевидно, что эта данная (локальная/глобальная) переменная/поле объекта используется на запись только в одном потоке, компилятор такими знаниями не обладает. В том месте, где программист знает, что между вызовами двух процедур не произойдёт ничего, компилятор этого не знает. В конце концов, в том месте, где программист может в голове раскрутить рекурсивный вызов метода в цикл, компилятору может тупо не хватить глубины дерева, и он оставит не оптимальный вариант кода.
Я даже не говорю о том, что простое расчленение функции далеко не всегда корректно: очень часто приходится проверять различные предусловия, вроде проверки ссылки на null. Оставлять синтаксические единицы, которые не проверяют предусловия не корректно, а если проверки вставить в каждую функцию, компилятор далеко не всегда сможет понять, что между начальной и конечной точкой объект не сменит своё состояние, и оставит лишние, с точки зрения программиста, проверки.
Вот вам конкретный пример, хоть и немного отвлечённый от основной темы вашего вопроса: https://habrahabr.ru/post/309796/

А вы можете обосновать, что компилятору не сложнее оптимизировать кучку маленьких функций чем одну здоровую простыню?
Компилятор либо встроит тело функции, если её тело доступно, либо вставит вызов. Когда он вставит тело функции, то ему нет разницы, была ли это маленькая функция или часть одной большой функции. Специфика встраивания сильно зависит от компилятора и от языка программирования, но в любом случае, у вас есть выбор, например, в релизе компоновать все исходники в 1 единицу трансляции и компилировать как одно целое; либо можно без этого воспользоваться LTO. В общем, я не думаю, что компилятору «сложнее».

Языки типа «жабоскрипта» в нормальных движках как минимум частично выполняются в виде нативного кода, особенно если динамичные возможности языка используются в умеренном стиле.

А в интерпретируемых языках, где нет нормального JIT, я бы о таких мелочах не сильно заботился, там в одной версии i += 1 может быть медленнее, чем ++i, а потом эта разница может быть нивелирована. Очевидную, вероятно, вещь написал…
Ага, встроит, если будет уверен, что не будет последствий. И если это не виртуальный метод. И если не накосячил с исключениями. И если в плюсовом коде метод записан в заголовочнике. И если встраивание вообще поддерживается.

И что, разве это имеет отношение к встраиванию? Да нет, не имеет, как в доме искали, так и будут искать. От силы будут оптимизации вроде кеширования адреса.

Так-то мне всё равно, однако меня нервируют подобные призывы «делайте больше функций» в разделе интерпретируемых языков. У меня и без того хром лагает. При этом, если бы в статье написали про то, что это счастье не бесплатное, то и ладно. Ан нет, не написали.
> Так-то мне всё равно, однако меня нервируют подобные призывы «делайте больше функций» в разделе интерпретируемых языков
Пишу на luajit. Призываю — делайте больше функций. Маленькие функции оптимизируются лучше больших, потому что дают оптимизатору больше информации о структуре кода.

Для V8 это тоже справедливо.
https://habrahabr.ru/post/310590/#comment_9822368
Для 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

>V8 компилирует его в нативный код
Транслирует. Нет, конечно, сейчас могут набежать всякие умники и кричать «Ты не знаешь, что такое трансляция!», «Да ты не знаешь терминов!», «Кто тебя вообще на хабр пустил?!». А я под их крики и бурления минусов получаю из .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

Разница небольшая, но в пользу варианта с разнесением кода по двум отдельным функцию.

Действительно, исключения замедляют большие куски, и это не вполне логично, учитывая отсутствие оптимизаций. С другой стороны, мы сравниваем немного не то, что я хочу сравнить. Хотя это тоже интересно и я капельку развил ваш код, но меня интересовали именно налкадные расходы от вызова функций, которые замечательно инлайнятся в нормальных компилируемых языках. (Ведь компиляция — это не просто трансляция в низкоуровневый код (потенциально обратимая), но и применение различных методов оптимизации транслируемого кода) То, что я увидел, наводит на размышления, потому что код действительно оптимизируется, хотя эти оптимизации и не выходят за рамки функции\класса. Это вполне логично для динамического языка, а результат в просадке на 6-8% при 10 вызовах на 10 умножений и одно взятие даты — это хороший результат.
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
Upd2: Был просто выброс, львиную долю времени выполняется Date.new(), внося недопустимо большие помехи.избавляемся и смотрим на результат.
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

Вы хоть читаете комменты или бомбите наугад? https://habrahabr.ru/post/310590/#comment_9821434

Меня жаваскрипт интересует прежде всего как браузерный скриптодвижок. Насколько я могу судить, node.js в плане оптимизаций далеко впереди браузерной версии, как минимум, потому, что у браузера есть всего 3 секунды на разбор полотна скриптов, что не даёт особого простора для оптимизации.
Вы хоть читаете комменты или бомбите наугад?

За всеми ветками комментов разве уследишь? С одной бы разобраться.


у браузера есть всего 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 — никак не контролируется)

вызов функции — операция крайне накладная

Тут вы ошибаетесь) См. ниже.
А вы на 100% правы, что дело не в инлайн метода, а в уменьшении обращений:



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

Однажды мой коллега уволился, потому что пытался справиться с 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 предметов разных габаритов (с учётом всех нюансов — габаритов, ограничений по весу, требований не класть в одну коробку тяжёлое с хрупким, таможенных особенностей). Сколько там можно написать микро-функций? Сколько времени у вас займёт найти среди них ошибку округления?
делать ему удобную структуру

Клиент и не должен делать удобно разработчику.
Разработчик должен доходчиво объяснить клиенту зачем надо выделять время не рефакторинг, а не просто сказать, что надо ХХХ часов, чтобы мне стало проще поддерживать эту программу.
Подобные статьи всегда вызывают смешанные чувства. С одной стороны, делать большие функции — это чаще всего неправильно. С другой стороны, делать функции длиной в одну строчку (и вызываемые всего один раз) только ради того, что возможно в будущем это может пригодиться — это ли не та самая преждевременная оптимизация, про которую столько статей было?

Ситуация напоминает ситуацию с нормализации реляционных баз данных: вроде теоретически для каждого факта должна быть своя таблица с внешними ключами, но на практике это ужасно неудобно. Думаю, во всем должно быть чувство меры…
А с каких пор JS стал дефолт-языком? Хаба соотвествущего у статьи нет, даже тегов. Однако специфичная терминология присутствует:
К счастью, ES2015 позволяет объявить const как read-only,
Можно использовать формат модулей CommonJS
Правда как всегда находится где-то посередине. Не нужно кидаться в крайности, нужно просто находить компромисс между производительностью и «удобством/красотой» кода. Но понимание этого приходит только после того как «пощупаешь» и то и другое…
Проблема хорошо видна. Функция getCollectionWeight() слишком раздутая и выглядит как черный ящик, полный сюрпризов.

Проблема отчетливо видна в другом. Ни одного комментария в коде по поводу что эта функция делает, stateless она или нет и прочие детали.


Лично мне плевать как функция написана внутри, если я знаю, что она протестирована и работает так как описана.


Если сильно приспичит — разберусь и с разбитой на подфункции и с непрерывным кодом одним блоком.
Особенно если код разбит осмысленно (здравый смысл) на блоки и прокомментирован.

В статье есть указание на get/set с ясностью и тут же let myMap, let my…
А мой — это какой и даже точнее — чей? К такой переменной, которая моя, хочется обращаться отовсюду: моя же, что хочу, то и делаю; а она хрясь и локальная. Логичнее префикс loc[al].
Насчёт двадцати строк тоже сомнительно: МакКонелл писал о семи, ссылаясь на свойство внимания и памяти о семи объектах.
Ну и, наконец, видно и написано, что статья — концентрат содержания других источников. Хорошо бы ссылки для полноты.
> МакКонелл писал о семи, ссылаясь на свойство внимания и памяти о семи объектах.
Я думаю отступы и скобки блоков в их число не входили, а с ними это как раз и будет около 20 реальных строк.

Аргументы за простые и маленькие функции:
1) Каждую в отдельности легко прочитать. Меньше сущностей нужно уложить в голову, когда воссоздаётся модель поведения.
2) Легче покрывать код тестами — относительно мало инвариантов, которые нужно проверять.


Против:
1) Увеличение связности кода. Т.е. меняя функцию в глубине, можно сломать вышележащие. На самом деле сложность из самой функции перемещается в связи между ними. У каждого кусочка кода растёт количество способов его использования. Может возникнуть ситуация, когда приходится какой-то кусочек обобщать настолько, что на самом деле было бы проще раскопипастить частные случаи выше по стеку. (Нарушили случайно single responsibility и ой).


2) Большое количество переключений контекста при написании кода (не очень страшно) и багфиксинге, который будут делать скорее всего другие люди и через полгода-год (а вот это беда). Т.е. чтобы понять почему вылетело то или иное исключение нужно пройтись сверху донизу (или снизу доверху), собрав в голове историю создания контекста в месте падения. IDE и дебаггеры немного купируют эту проблему, но не до конца.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории