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

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

Современные компиляторы умеют это делать. Оптимизация называется loop unswitching

Хотя автор поставил на первое место оптимизацию, вы правы - это за нас зачастую сделает компилятор. Но штука в том, что именно выразительность повышается: цикл однотипных действий можно воспринимать как одно действие над всем списком. И, скажем в Java, заменить на стрим

Когда так можно переписать (идеальный случай – перейти к векторным операциям) – прекрасно. Когда от этого код становится труднее читать (бывает и такое) – лучше лишний раз подумать, стоит ли овчинка выделки.

эмпирических

Ну как эмпирических. Всполне себе формальных.
O(1) vs O(N) в лучших случаях.

Может, я неправильно понял.
Есть функция с одним if, которая вызывается 15 раз.
Предлагается этот if вынести в вызывающий код, и у нас станет 15 if вместо одного?

На 15 штуках не сильно принципиально, но если штук заметно больше, то выгоднее сначала отфильтровать/поделить штуки по if и уже после вызывать обработку штук. Это во первых открывает путь к применению автовекторизации (то бишь когда в регистр процессора кладётся несколько штук сразу и все они обрабатываются за один такт), во вторых становится cache friendly - инвалидацию кэшей процессора можно будет делать реже, потому что предсказание веток уже сделано фильтром.

От силы 1% кода, который пишется, требует оптимизации на уровне регистров и кеша процессора. При этом 100% кода читается и модифицируется.

Так что критерии читабельности и поддерживаемости значительно превалируют.

И часто критична (ну или хотя бы очень важна) скорость написания кода. Что быстрее – воткнуть один if или анализировать вызовы функции на предмет "отфильтровать/поделить" и затем заниматься изменением дизайна кода?

От силы 1% кода, который пишется, требует оптимизации на уровне регистров и кеша процессора.

Citation needed.

И вопрос: если я пишу игру на C++ вместо Python'а, это я какой процент кода оптимизировал на уровне регистров и кеша процессора?

Вопрос: если я пишу бекенд на Python, я какой процент кода оптимизирую на уровне регистров и кеша процессора?

Вы резко сузили кейс (от "весь код" до "игра на C++") - и что этим пытаетесь доказать?

если я пишу бекенд на Python, я какой процент кода оптимизирую на уровне регистров и кеша процессора?

Не знаю, это вы меряете проценты как-то.

Вы резко сузили кейс (от "весь код" до "игра на C++") - и что этим пытаетесь доказать?

Я ничего не пытаюсь доказать, я вопрос задаю, как вы проценты считаете.

Ну давайте ещё более упростим, напишем программу, которая выполняет цикл от нуля до arvg[1]. На питоне и на C++. Алгоритмически абсолютно одно и то же. Что у них с процентами оптимизации на уровне регистра и кеша? Тоже одинаково?

Я не знаю. Именно потому, что задачи, которые я решаю, не требуют ручной оптимизации.

И когда я написал "требует оптимизации на уровне регистров и кеша процессора", я имел в виду усилия со стороны программиста. Если компилятор там чего-то оптимизирует автоматически, без моего участия - да пожалуйста.

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

До сих пор это не требовалось никогда, несмотря на то, что я в профессии не один год. И очень сильно подозреваю, что таких задач - подавляющее большинство. Именно поэтому я и написал про "1%".
А требовать от меня каких-то формул или методики подсчёта процентов может только человек, который никогда не сталкивался с понятием "образное выражение".

P.S. На самом деле я подозреваю, что таких задач сильно меньше одного процента. Но Вы, конечно, имеете полное право придерживаться иной точки зрения.

и получаем кучу копи-паста (мы же вынесли условие из функции в места ее вызовов) и удачи при изменении условия

и вот еще - в функции стоит условие проверки аргументов на валидность - тоже условие поднимать выше - выносить из функции?

Ну фиг знает. Иметь что-то вроде

shtuki
.filter(is_valid)
.filter(some_other_condition)
.split_by(process_condition)
.process_yes(/* обработка для if (true)*/)
.process_no(/* обработка для if (false)*/)

вроде выглядит не сказать чтобы нечитаемо даже с точки зрения адептов чистого кода. прочая обвязка в том или ином виде все равно будет иметь дикие if, но там и циклической обработки обычно нет, так что не знаю где вы там копипаст плодите.

Отдельно стоит помнить про количество данных - на малых размерах какой-нибудь вектор неотличим от хэшмапы и полтора десятка условий погоды не сделают даже при безумных способах их индексирования, так что перед оптимизациями необходимо иметь замеры, что данный участок кода действительно узкое горлышко. И иметь примерное представление о доступной производительности/пропускной способности.

Вы как-то пример приводите... Сравнивая теплое с мягким. У вас в filter передается функциональный параметр (но я не спец по языку). Возможно я ошибаюсь, не понимая глубину показываемой вами абстракции.

В примерах в статье предлагалось выносить if за пределы метода. Опять же внутри filter я так понимаю сидит оператор условия, который и предлагается вынести.

Лично я ничего не имею против вашего примера и подхода. Более того, я целиком его разделяю, используя linq в c#, но мы же обсуждаем статью.

В примерах в статье предлагалось выносить if за пределы метода. Опять же внутри filter я так понимаю сидит оператор условия, который и предлагается вынести.

собственно использование filter и есть вынос - условие оказывается выше, а обработка в цикле ниже.

Предикат вынесли, а оператор условия, который будет использовать этот предикат - остался внутри filter

Я так понимаю, не 15 раз из разных мест, а 15 раз подряд из одного места, в цикле. Это материал для новичков же, тут про простые вещи.

Основное преимущество здесь — производительность. А крайних случаях — огромный рост производительности.

Я, конечно, не настоящий программист (пишу на фортране). Но невольно возникает вопрос: а так ли уж часто сверхсложная логика и требование высокой производительности переплетены настолько, что их не разделишь? Лично я просто стараюсь

писать понятно

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

а дальше пусть голова болит у оптимизатора. То же перечисление - это же по сути массив, а массивы, по идее, любой компилятор должен распараллеливать на ура?

Как выше заметил redf1sh2, современные компиляторы умеют многое делать. Особенно если им не мешать. Может, у меня однобокий взгляд, но у нас в фортране простые for-циклы обычно пишутся в виде массивного оператора, например:

if (condition) A=1/B,

(тут A и B - массивы). Такой код оборачивать в функцию часто не нужно вообще. Соответственно, рекомендация автора статьи выполнена автоматически ;-). Но, вовсе не ради какой-то оптимизации, а просто мне так запись понятнее.

При этом, однако, никто не мешает вставить if внутрь цикла:

where (B > 1) A=1./B

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

Я понимаю, что массивные операции есть не во всех языках, и что у компилятора могут возникнуть трудности, если, например, мы работаем не с нативным массивом, а с массивом структур, и там надо обработать какое-то поле. Но, если речь идет о второй древнейшой профессии втором древнейшем языке, то ведь обычно справляется же?! Значит, и в современных языках тоже должен справляться

ну или вот-вот научится

Конечно, из любых правил есть исключения. Но строить всю парадигму на базе тех нескольких процентов случаев, где
1) эффективность критична, но
2) компилятор не умеет превратить хорошо структурированный (=читаемый) код в быстродействующую программу -

это как то странно, не правда ли?

Поэтому я согласен с автором статьи в том, что логику обычно стоит поднять повыше (в разумных пределах, конечно). Но, глядя со своей колокольни, не очень согласен в том, что

во главе угла при этом должна стоять именно эффективность, а не читаемость кода

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

> dedmagic1> От силы 1% кода, который пишется, требует оптимизации на уровне регистров и кеша процессора. При этом 100% кода читается и модифицируется (...).

Вот именно что ;-)

Это вроде как логически понятно, если условие не индивидуально для каждого walrus, зачем проверять его каждый раз?

По поводу примера с условием в цикле. Создать функциональную переменную, которой по условию присвоить необходимую рутину. В цикле вызывать - в результате нет дублирования цикла

функция используется в том числе чтобы сократить основной код, ну а через оптимизации компилятора можно попросить его развернуть её во время сборки, так что не вижу причин не делать функцию из if.

На древних tms320c40 длина конвеера команд была 4, и любой if при обработке массива его сшибал. Тем самым скорость вычислений могла быть легко уменьшена в 4 раза) в следующих процессорах TI создали аж оптимизирующий ассемблер для перетасовки команд для VLIW, и количество упоминаний про использование if уменьшилось.

Для современных процессоров общего назначения с их предвыборками инструкций и данных, эта проблема не очень актуальна, имхо. Если какое то dsp на каких то простых архитектурах, то жизнь заставит поразбираться)

if внутри функции это удобнее тем, что вся ответственность лежит на самой функции, а не на программисте, который её вызывает. Допустим я беру некую библиотечную функцию, откуда я знаю, что туда можно подать вектор, который содержит 3 или 5 элементов, а если я подам вектор, который содержит 4 элемента то она ничего не будет делать. Поэтому вот тебе, функция, какой-то вектор, делай с ним, что хочешь

Также вызов функции в цикле компилятор сам оптимизирует. Функция для одного элемента позволяет вызвать себя как для одного элемента, так и во время итерации по списку, вектору, мапе и тд. А так получается надо n разных функций писать/использовать шаблоны, если такие есть в языке

Что бы вы могли понять, что нужен вектор из 3-ёх элементов, а не любой, существует система типов. Через неё можно указать чего ждёт функция на вход.
Например сделать тип Vec3, который в своих конструкторах выполняет все нужные проверки.
В итоге у вас появляется возможность доп. оптимизаций, если одни и те же данные используются несколько раз. Например сразу хранить все нужные данные в виде Vec3, что бы, обрабатывая их повторно, не выполнять лишние проверки.

Похоже на танцы вокруг цикломатической сложности(вы же запускаете статический код анализатор). Кстати первый совет может породить мега монстра+оркестратора, которого будет не реально обложить тестами)

Господи, как мы оказались в этой точке?

Что характерно, если писать в функциональном стиле с итераторами и комбинаторами, оно примерно так само собой получается)

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

Публикации