Pull to refresh
3
2
Юрий Муравьёв@y_mur

User

Send message

При совмещении проходов код станет более "оптимальным", но при этом более "медленным" :) (тык)

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

Поэтому говорить об оптимальности без бенчмарков - как минимум странно.

Повторю свой ответ выше: А так я и не про такты, поэтому и бенчмарков тут нет. Я про "внутреннюю кухню" рекурсии, которая в этих простых примерах пока только "мусорит" но в более сложных примерах станет неотъемлемой частью алгоритма, поэтому я и подчёркиваю что уже здесь важно понимать что она там есть и как именно она выглядит.

А так я и не про такты. Я про "внутреннюю кухню" рекурсии, которая в этих простых примерах пока только "мусорит" но в более сложных примерах станет неотъемлемой частью алгоритма, поэтому я и подчёркиваю что уже здесь важно понимать что она там есть и как именно она выглядит.

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

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

какой тезис Вы пытаетесь отстоять в данной статье. То ли, что "рекурсия вредит читаемости кода/усложняет код", то ли что "рекурсия - это медленно"

Да, и что "рекурсия вредит читаемости кода/усложняет код" и то что рекурсия не просто бывает медленной, а может оказаться внезапно и трудно предсказуемо ресурсоёмкой (медленно лишь один из аспектов ресурсоёмкости), а это тоже про "вредит читаемости".

Двоичное разложение редко когда применимо к задачам. 

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

Как аналогию можно сравнить дерево отрезков и дерево фенвика.

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

Ещё моё объяснение такое длинное потому что я подробно разъясняю все шаги с примерами, хотя тому кто в курсе как устроена двоичная система достаточно сказать: "заметьте что: x⋅x=x^2 => x^2⋅x^2 = x^4 => x^4⋅x^4 = x^8 => x^8⋅x^8=x^{16} и здесь 4 умножения вместо 16. Но это работает только при n = 2^k. Дальше остаётся вспомнить как устроен двоичный формат числа и про математическое свойство x^{a+b}=x^a ⋅ x^b".

надо сначала посчитать значение половинной степени, потом это возвести в квадрат и подправить, если четность не та.

А ваше объяснение такое короткое и непонятное потому что вы просто опускаете какие-то очевидные лично вам, но совсем не очевидные мне пункты.
Мне из вашего объяснения понятно только то, что ваша "половинная степень" это аналог моего n>>1 , а ваше "подправить, если четность не та" - аналог моего x^{a+b}=x^a ⋅ x^b и за счёт этого ваша логика приводит к правильному результату. Но вы в это явно вкладываете какие-то другие смыслы, которые от меня ускользают ;)

Наглядно, но неправильно. Правильно смотреть вот здесь: https://godbolt.org/z/86nxErzxW - найдите 10 отличий.

Кстати на https://godbolt.org msvc совсем древний и он так не умеет. Сейчас смог докопаться до релиз варианта в последнем msvc v145 из VS2026, но и он не справился с задачей превратить рекуррентный факториал в цикл и сделал так как у меня в статье. Так что ваше "правильно" пока компиляторозависимо.
А clang в варианте с рекурсией ухитрился не только превратить в цикл, а ещё и сделать разворачивание цикла.

Наглядно, но неправильно. Правильно смотреть вот здесь: https://godbolt.org/z/86nxErzxW - найдите 10 отличий.

Спасибо интересно - я смотрел пошаговое выполнение в msvc в дебаг режиме (в релизе нужные точки останова просто не ставятся, поэтому трудно найти нужное место) поэтому не увидел способность С++ компилятора нормально превратить простую рекурсию в цикл. Поправлю в статье и учту на будущее. Но я по прежнему уверен что с более сложными примерами компилятор так не сможет. Да, дальше буду проверять и там тоже.

А что именно в Вашем понимании "алгоритмическая эффективность"?

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

Явно писать код со стеком - конечно не будет.

std::stack здесь исключительно для иллюстрации того что рекурсия неявно использует стек, во всяком случае на x86_64, где он есть. И в более сложных примерах это использование стека будет не "шумом", который умный компилятор может даже выкинуть как здесь, а неотъемлемой частью алгоритма, поэтому я и сомневаюсь в способности компилятора различать эти ситуации за пределами совсем уж тривиальных случаев.
А "не будет" относится к тому что не будет приписывать второй цикл, который выполняет исключительно бессмысленные действия.

Но моя сильно проще и понятнее. 

:)) Боюсь тут уже пошла вкусовщина - одним проще и понятнее одно, другим другое.

Лично мне значительно проще заметить что: "x⋅x=x^2 => x^2⋅x^2 = x^4 => x^4⋅x^4 = x^8 => x^8⋅x^8=x^{16}, обойдясь 4 умножениями вместо 16." Но это работает только при n = 2^k. Дальше остаётся вспомнить что любое целое положительное n в двоичной системе и есть сумма чисел 2^k"например, десятичное число 11 в двоичном виде 0b1011, где 1 в нулевом, первом и третьем битах означают 2^3+2^1+2^0 = 8+2+1 = 11, а 0 во втором бите означает что 2^2 в этой сумме нет (отсчёт битов начинается с нулевого бита, поэтому второй бит в 0b1011 визуально находится на третьей позиции)." А суммирование степеней при одинаковом основании эквивалентно перемножению, "например, 27=16+8+2+1 =>x^{27} = x^{16}⋅x^8⋅x^2⋅x. " И исходя из этой логики проверка очередного разряда как  n & 1 и переход к следующему разряду n>>1 выглядят значительно логичнее чем  n % 2 и n/2 хотя формально это одно и тоже.
И никакого умножения в столбик я здесь не вижу.

А вот ваша версия:

надо сначала посчитать значение половинной степени, потом это возвести в квадрат и подправить, если четность не та.

звучит конечно короче, но у меня в голове укладывается плохо ;)

 Как у вас из этого внезапно ДВА цикла появилось вообще не понимаю.

Просто возьмите Код 1.2. и Код 2.2. и пошагайте в них отладчиком и/или взгляните на приведённую в статье отладочную информацию которую выводит Код 1.4. и Код 2.4. и вы наглядно увидите что этот код ведёт себя именно как два цикла, выполняющиеся один за другим.

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

Ваши же примеры совершенно точно начинают использовать кучу 

Да, std::stack использует кучу, но он здесь исключительно для того чтобы проиллюстрировать логику поведения рекуррентного варианта, использующего стек неявно.
Сделать варианты с контейнером без лишних обращений к куче не проблема. Просто это не имеет отношения к алгоритмической логике рассматриваемого кода, поэтому не стал усложнять этим примеры.
В остальном вижу что мы говорим на разных языках. Если придумаю другие слова чтобы вам стало понятнее о чём я, то потом напишу дополнительно.

Ваш первый вариант полностью эквивалентен моему варианту Код 2.6. с поправкой на то что ваше n % 2 эквивалентно моему n & 1, а n/2 эквивалентно n>>1. И компилятор скорее всего скомпилирует на этих конструкциях одинаковый машинный код.

Второй вариант интереснее про него отвечу позже, возможно добавлю этот ответ в статью.

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

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

И всё это не отменяет моего последующего разбора этого варианта. Как его вывести из рекуррентной или цикловой логики это прелюдия - важнее как он потом реально работает.

Да, про числа Фибоначчи тоже будет, может кому-то это и баян, а по мне так яркий и наглядный пример.

Вся статья должна называться "Я не знаю, что такое хвостовая рекурсия".

Нет, это называется кто-то не заметил:

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

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

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

И переводится в код напрямую, сначала с двумя случаями четного/нечетного, а потом их можно объединить для краткости кода.

Да у меня там есть ссылка на эту логику с чётностью / не чётностью и комментарий к ней: "Однако с логикой там всё мутно и сложно - много букв и формул, а в конструкции (n-1)/2 усматривается сакральный смысл по преобразованию нечётных чисел в чётные, хотя на самом деле вычитание единицы из нечётного числа обнуляет младший двоичный разряд, а целочисленное деление на два этот младший разряд отбрасывает независимо от того был он предварительно обнулён вычитанием единицы или нет. "

почти любая работа с древовидными структурами данных в виде рекурсии получается читабельнее и понятнее.

Про древовидные структуры будет в следующих частях. Есть что сказать, но пока не буду спойлерить.

Но часто это тривиальная цена за более простой код, 

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

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

В смысле без единого? - везде под "код x.2" идёт пример с рекурсией, а сопровождающие его два примера с циклами иллюстрируют как это нормально запрограммировать и что на самом деле делает рекурсия если посмотреть её под пошаговым отладчиком и/или через добавление отладочной информации.

Это не считая того что зачем-то ещё и стек впендюриваете, хотя он там нафиг не нужен ни в одном из вариантов.

Затем что рекурсия Всегда неявно использует стек процессора и в данных примерах делает это именно так как описано в статье, но если смотреть только на код рекурсии не пытаясь вникнуть в происходящее то этого не видно. Об этом и статья что "вы не видите суслика, а он есть!"

Ещё и циклы никак не ограничены.

При применении рекурсии всегда "автоматически" формируется как минимум два (может значительно больше) "псевдоцикла" и программист никак не может на это повлиять или оптимизировать. Точнее может если откажется от рекурсии.

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

Можно пример такой "чистой" рекурсивной функции?

Да, планируется серия статей с постепенным усложнением примеров.

Одиночный конкурс с небольшим количеством призёров хорошо, но хотелось бы побольше системности в поддержке Open Source.
Хочу чтобы:
- подобные конкурсы были регулярными и количество призёров за каждый конкурс больше;
- а ещё лучше чтобы это были не отдельные конкурсы, а постоянно действующая программа, рассматривающая входящие завки по мере их поступления;
- публикацию информационо образовательных материалов о том где и как начинающие разработчики Open Source могут получить материальную поддержку своих проектов.

Никакой свободой распространения там даже не пахнет.

Что значит не пахнет? Вы знаете примеры, когда к кому то были претензии за то что он "по пиратски" распространял GPL программы?
В проприентарном мире "пиратское" использование и распространение это преступление, административно/уголовно/судебно наказуемое. В мире GPL это абсолютно законно поэтому и свобода.

и как в этой юрисдикции работают экспортные ограничения.

Вы видели новую версию GPL в которой прописаны экспортные ограничения или какие либо ещё - национальные, территориальные и т.п.? Если нет, то храбро берите GPL программы и используйте/распространяйте! Если для фактического скачивания таких программ есть какие-то экспортные/территориальные и т.п. технические ограничения, то обходить их и затем распространять GPL программу за пределами этих ограничений - законно!
И если вдруг когда нибудь такая ограничивающая версия GPL появится, то она:
- не будет совместима с существующими версиями, где такие запреты явно запрещены;
- не будет иметь обратной силы - уже существующие свободные программы такими и останутся.

Потому-то вся эта история с т.н «свободным ПО» не идет во благо развитию индустрии.

Да с финансовой стороной тут есть серьёзные проблемы. Идея GPL отлично подошла бы к осознанному коммунистическому обществу, где никто не "тянет одеяло на себя". В таком обществе нет денег за их ненадобностью, потому что "каждому по потребностям - от каждого по способностям" и уклоняться от того чтобы вносить вклад в общее благо (по мере способностей и постоянно заниматься развитием этих способностей) - не принято, не прилично, стрёмно, себя не уважать.
А с капитализмом, где про "урвать", "взломать систему", "дать поменьше - взять побольше", "торговаться до усрачки за каждую копейку", да, идея Свободного ПО плохо совмещается.

Но сходу возражать, даже не читая, про что был тред, это GPL тоже не запрещает :)

Читал, но потерял мысль в этом "многа букв". Теперь понял в чём суть спора. Разделяю мысль что IDE для программиста эквивалентна Блендеру для 3Dшника.

Бизнес-модель Гугла предполагает бесплатные и открытые средства доступа для весьма платных и очень даже закрытых сервисов Гугла, к слову.

Как бы возможно да, но не припомню когда я использовал свой телефон на андроиде в качестве средства доступа к платным сервисам Гугла и платил за это. Получается Андроид типа "благотворительный проект" который Гугл просто может себе позволить на уровне "хобби", зарабатывая на другом? Или всё таки они как то на нём зарабатывают?

отчасти, есть ответы на Ваши вопросы.

Не нашёл там этих ответов. Поэтому и написал.

Information

Rating
1,443-rd
Location
Саратов, Саратовская обл., Россия
Date of birth
Registered
Activity