(пританцовывая на месте:) Блин, ну сколько уже ему намёков давать можно? Когда, когда же он наконец спросит «а какой версией gcc эти примеры скомпилированы были?»
Современной, поскольку вы сказали понадобится, а не понадобилось в бы прошлом:
Ему всего-то понадобится овердофига места в стеке.
то есть, без указания, что имеете ввиду не современные версии.
Но могли бы и сами проверить.
Сейчас самая древняя доступная работоспособная версия там — 3.4.6, это — 2005-ый год, 20 лет назад.
Если отключить заголовочный файл, то относительно работоспособной становится случайно затесавшаяся туда версия 1.27, которая, действительно, не выполняет TCO:
Я портировал максимально близко к вашему коду Elixir'а. О чём и написал.
Я никогда в жизни не зарабатывал денег написанием кода на c,
Это — абсолютно перпендикулярная тема, которая не может быть критерием, ибо можно успешно зарабатывать и безобразным кодом.
Но дело — не в этом.
но вот как примерно выглядит рекурсия здорового человека.
Собственно, ваш код получится, если упростить мой, выбросив рекурсивное вычисление длин операндов, а также сдвиг результата для случая отсутствия финального переноса, и с'merge'ить большинство функций (do_add1...do_add6, do_addn) в одну, отойдя от кода на Elixir'е.
Также из-за отсутствия сдвига результата вам пришлось память освобождать, используя ранее запомненный указатель, который вернула функция malloc, а не возвращённый указатель с результатом, потому что без сдвига результата возвращённый указатель может не совпадать со значением, которое изначально вернула функция malloc.
В другом комментарии вы пишете:
Рекурсии не требуется знать длину списка, а обычному циклу с индексом — требуется.
Но в своём примере почему-то дважды вызвали strlen. У меня эта работа выполнялась в pre_add.
Значит, чуда никакого нет, и рекурсии всё-таки требуется знать длину списка, просто это выглядит не так явно.
Если вашу рекурсивную реализацию сравнить с оригиналом (там — под спойлером), то выяснится, что ваша ничуть не меньше по объёму, и ничуть не более понятна.
Изящной её тоже не назовёшь, особенно, если доработать, чтобы не было упрощений, хотя реализация на Elixir'е ещё может таковой показаться.
Единственное изящное место у вас, по сравнению с моим кодом, это использование массива "1" в качестве числа с длиной 1 для, чтобы не обрабатывать отдельно случай распространения переноса с одним операндом, когда другой операнд уже "кончился", а скинуть эту работу на рекурсивную функцию, "прикинувшись" для неё, что второй операнд всё ещё не "кончился".
Однако, изящность в этом месте будет стоить быстродействия.
Собственно, когда я померил быстродействие своей рекурсивной реализации, то выяснилось, что она медленнее в разы по сравнению с не рекурсивной.
Ну, и самое главное, что касается рекурсии, TCO не гарантируется.
С вашим кодом MSVC справился, смог применить TCO. С моим, который сложнее, — нет.
Да, reverse по месту выполнить не удастся, это правда. Тут (и только тут) я ошибся.
Я бы не был столь категоричен насчёт "только тут".
Это каких например?
Да тот же reverse.
Эликсир полностью иммутабелен. Си, когда я в последний раз заглядывал в спецификацию, — нет. И я черным по белому написал: по месту,
Все реализации, и даже изначально предложенная C-шная, возвращают третье значение, не трогая операнды.
Кстати, в изначально предложенном варианте в тестах в качестве операндов используются строковые литералы. Вы предлагаете в них писать?
Это выходит за рамки задачи в том виде, как она была сформулирована. Если надо ничего не попортить — придется вызывать length и malloc, что попахивает.
Это придётся делать в любом случае. Вы почему-то верите в рекурсию как в волшебство какое-то.
Я в том же предложении пояснил: единицу накопленную может потребоваться добавить.
В realloc передаётся указатель. Этот указатель должен быть получен от функций malloc, calloc или той же realloc. Передача туда какого-то другого адреса приведёт к UB.
Проблемы? Проблем нет, я погорячился с вызовом length, который на алгоритмическую сложность не влияет; все остальное — в силе.
Не получится использовать realloc, это все равно будет malloc, хотя это и не принципиально.
Но в предложенном вами варианте возникает неудобство, потому что, если переноса нет, то следует ли освобождать память, зависит от того, динамически она была выделена под операнд на вызывающей стороне или нет. А если перенос был, то освобождать следует обязательно.
Более того, может быть использован один или другой операнд, что ещё больше усложняет дело.
Зато вам вообще не пришёл в голову единственно правильный и изящный способ решения, вы сразу приступили к код-ревью заведомо плохо поддерживаемого спагетти.
Вы имеет ввиду рекурсивную реализацию?
Я попробовал выполнить честную рекурсивную реализацию на C достаточно близко, насколько возможно, к вашему коду на Elixir'е, без reverse и без конкатенации.
Что интересно: 3 компилятора смогли применить TCO, а 4-ый не справился, а именно — MSVC.
Данный фрагмент как раз относится к случаю реально длинных чисел, о котором вы упоминали в том духе, что ваши тесты этот случай не покрывают.
Алё, гараж, я на Си последний раз писал двадцать лет назад, когда 16 мегабайт памяти было овердофига. Естественно, что мои рефлексы не заморачивались поддержкой гигабайтов памяти.
Разве в то время возвращаемое функцией malloc значение не требовалось проверять на NULL?
К тому же в то время ещё можно было реально встретить платформы с 16-битным int'ом.
Это значит, что достаточно было 32/64 КБайтных чисел, чтобы получить такой же эффект.
сознательно оставленные упущения
Вот про "сознательно оставленные" не верится совсем.
Все без исключения современные компиляторы применят в данном случае TCO, поэтому от синьёра я бы лично ожидал увидеть рекурсию.
Не очевидно, что это хорошее решение.
Рекурсивный код тут будет в три раза короче, ему не потребуются ни O(n) вызовы strlen в начале, ни куча условных операторов
Попробуйте выполнить reverse "in-place", и вы тут же обнаружите, что перед тем, как начать, вы будете вынуждены сначала найти конец строки.
Это и есть то, что, по сути, делает strlen.
И код не будет короче из-за всяких вспомогательных функций, которые потребуются при рекурсивном подходе.
и, самое главное, ему не потребуется malloc, потому что можно по месту перезаписывать любой из двух наборов данных, а когда ввод отыквится — опционально переключиться на другой через memcpy.
То, что она работающая — как бы показано результатами тестов непосредственно вслед за собственно программой.
Ваш набор тестов покрывает все случаи?
Мои варианты тестов, которые демонстрируют низкое качество вашего кода, вы предпочитаете не замечать?
А если Вам охота не «набросать код за пять минут и читать Хабр дальше»
В моменты "набросать код за пять минут" проявляются программистские рефлексы, потому что особо обдумывать некогда.
Ваши программистские рефлексы диктуют вам в любой непонятной ситуации использовать тип int и не обрабатывать возможные ошибки.
Да, это похоже на уровень джуна.
а придираться к запятым — то не смею Вам препятствовать.
То, что такие существенные вещи вы называете придиркой к запятым, дополнительно указывает на то, что гордиться и чваниться здесь нечем.
Что интересно, различные ИИ для этой задачи почти поголовно тоже пытаются применять int, и даже тот, кто сначала применил size_t, все равно потом использовал int.
Однако, ни один из 4-х не забыл проверить результат вызова malloc с самого начала.
И ни один, после указания на проблему с типом int, не пытался заменять int на unsigned long или, например, uint64_t, — все сразу переходили на size_t, очевидно, "понимая" его смысл.
Но это уже без меня.
Верно, чваниться умеете на хорошем уровне, а признавать ошибки не умеете совсем.
С одной стороны у вас запас памяти ограничен, а с другой - у вас числа по 2 гигабайта, чтобы знаковый int переполнялся. Вы там определитесь как-нибудь.
Поясняю: потому что после вызова функции malloc указатель, который она вернула, не был проверен на `NULL`.
То, что функция malloc может вернуть NULL, и что поэтому необходимо проверять на NULL это значение, ясно видно по распечатке значения result_str выше и по результату дальнейшего исполнения программы без проверки его на NULL.
Фрагмент кода, чтобы было понятно, причём тут result_str:
Отсутствием проверки значения, которое вернула функция malloc, часто грешит код современных программ, и в существенной степени именно поэтому они так себя ведут в случае очень малого запаса по памяти.
Переполнение знакового int'а может приводить к более широкому кругу глюков.
Замечу также, что в 2025-ом году 2 гигабайта — это уже относительно небольшой объём памяти, поэтому ваше "Вы там определитесь как-нибудь" звучит неубедительно.
Это подмена понятий, считаю. Я утверждаю, что в данной строке нет разыменовывания указателя.
Думаю, что это — терминологическая проблема. Или проблема того, под каким углом зрения на неё смотреть.
Вот этот самый indirection/dereference — это action, действие.
В терминах стандарта, видимо, это — evaluation.
Так именно действия здесь и нет
Evaluation'а нет. Unevaluated context. То есть, в терминах стандарта это — невычислимый контекст.
Однако, невычислимый контекст всё ещё считается выражением, в отличие от декларации. И разыменование есть, просто оно не вычисляется.
А в декларации разыменования нет, потому что это — вообще не выражение.
То есть, на это можно смотреть по-разному, но стандарт смотрит как на выражение, только невычислимое, что не отменяет наличие разыменования.
А вы пытаетесь отождествить вычислимость разыменования с самим наличием оного. Здесь, скорее, имеет место терминологическая разница или даже разница в углах зрения на одно и то же.
и замене в оригинальном коде проблемных int'ов на unsigned, чтобы оригинальный код заработал на этих конкретных данных, производительность растёт не очень сильно, хотя и растёт.
Типичное суммарное время исполнения вместе с инициализацией массива оригинального кода:
$ time ./1
real 0m4.365s
user 0m3.124s
sys 0m1.240s
$
а представленного выше:
$ time ./1
real 0m3.808s
user 0m2.656s
sys 0m1.152s
$
Хотя, разница на уровне чистого времени исполнения функции mega_add будет больше.
Есть такая каста людей, которая, не думая, любит бездоказательно и безответственно применять расхожее клише.
Спасибо за ответ.
А иначе — не выяснить, как оно на самом деле.
Приходится пробовать самому с чьей-то подачи.
Тут — вопрос выбора.
Каждый человек для себя выбирает, как поступить.
Кому-то важно одно, кому-то — другое.
Доказать?
Когда вы показали командную строку, как вы компилируете, всё стало понятно.
Вы компилируете без оптимизации.
Знаете, что означает "O" в аббревиатуре TCO?
Да, прямо сейчас у меня нет такого.
Но оно и не нужно.
Вы правда не понимаете или прикидываетесь?
Вот смотрите, компилятор gcc 3.4.6 НЕ осуществляет TCO:
И тот же самый компилятор теперь уже ОСУЩЕСТВЛЯЕТ:
Объяснять, почему, или сами догадаетесь?
Современной, поскольку вы сказали понадобится, а не понадобилось в бы прошлом:
то есть, без указания, что имеете ввиду не современные версии.
Но могли бы и сами проверить.
Сейчас самая древняя доступная работоспособная версия там — 3.4.6, это — 2005-ый год, 20 лет назад.
Вот результат:
Если отключить заголовочный файл, то относительно работоспособной становится случайно затесавшаяся туда версия 1.27, которая, действительно, не выполняет TCO:
Но это, всё-таки, 1988-ой год.
А вы говорили о 25-и годах, а не о 37-и.
Я портировал максимально близко к вашему коду Elixir'а.
О чём и написал.
Это — абсолютно перпендикулярная тема, которая не может быть критерием, ибо можно успешно зарабатывать и безобразным кодом.
Но дело — не в этом.
Собственно, ваш код получится, если упростить мой, выбросив рекурсивное вычисление длин операндов, а также сдвиг результата для случая отсутствия финального переноса, и с'merge'ить большинство функций (
do_add1
...do_add6
,do_addn
) в одну, отойдя от кода на Elixir'е.Также из-за отсутствия сдвига результата вам пришлось память освобождать, используя ранее запомненный указатель, который вернула функция
malloc
, а не возвращённый указатель с результатом, потому что без сдвига результата возвращённый указатель может не совпадать со значением, которое изначально вернула функцияmalloc
.В другом комментарии вы пишете:
Но в своём примере почему-то дважды вызвали
strlen
.У меня эта работа выполнялась в
pre_add
.Значит, чуда никакого нет, и рекурсии всё-таки требуется знать длину списка, просто это выглядит не так явно.
Если вашу рекурсивную реализацию сравнить с оригиналом (там — под спойлером), то выяснится, что ваша ничуть не меньше по объёму, и ничуть не более понятна.
Изящной её тоже не назовёшь, особенно, если доработать, чтобы не было упрощений, хотя реализация на Elixir'е ещё может таковой показаться.
Единственное изящное место у вас, по сравнению с моим кодом, это использование массива
"1"
в качестве числа с длиной 1 для, чтобы не обрабатывать отдельно случай распространения переноса с одним операндом, когда другой операнд уже "кончился", а скинуть эту работу на рекурсивную функцию, "прикинувшись" для неё, что второй операнд всё ещё не "кончился".Однако, изящность в этом месте будет стоить быстродействия.
Собственно, когда я померил быстродействие своей рекурсивной реализации, то выяснилось, что она медленнее в разы по сравнению с не рекурсивной.
Ну, и самое главное, что касается рекурсии, TCO не гарантируется.
С вашим кодом MSVC справился, смог применить TCO.
С моим, который сложнее, — нет.
Так ведь необходимо не только написать.
Необходимо ещё и правильно скомпилировать.
Если взглянуть на то, как с вашим кодом справились 4 компилятора при правильной компиляции, то выяснится, что все они применили TCO.
Ассемблерный код от gcc:
От clang'а:
От Intel'овского icx:
От MSVC:
Доработал ваш код, убрав UB и остановив рекурсию на моменте, когда кончатся положительные
int
'ы:Собрал правильным образом локально у себя на машине, и ассемблерный код получился таким:
По самой интересной его части:
Видно, что компилятор применил TCO.
Далее, я запустил программу, вырезав середину её вывода и оставив только первые 3 и последние 3 строки:
Пришлось подождать минуты 3, но, как видно по результату, весь запас положительных значений
int
'а был успешно исчерпан.Кстати, вы умудрились даже в таком простом коде допустить UB: функция
test
должна возвращать значение.Да, я уже слышал про обратностороннего салфеточника, который пишет на обратной стороне салфетки код, не являющийся production-ready.
Однако, компиляторам — все равно, по какой причине в программе имеется UB, они не читают habr.
При наличии UB в программе компиляторы имеют право генерировать любой код, в том числе, неправильный.
Но в данном случае они этим правом не воспользовались, видимо, потому, что рекурсия — бесконечная, о чём они, кстати, и пишут в своих предупреждениях.
Вот, сколько памяти потребляет процесс у меня через 2 с половиной минуты исполнения, незадолго до его завершения:
Даже до 1 МБайта не дотягивает.
Да, вы почему-то многого не знаете.
Вот и выходит, что — нечем вам чваниться перед молодёжью, кроме того, что лет вам больше, чем представителям этой самой молодёжи.
Нет, у вас написано по этому поводу:
Вы это написали?
Я бы не был столь категоричен насчёт "только тут".
Да тот же
reverse
.Все реализации, и даже изначально предложенная C-шная, возвращают третье значение, не трогая операнды.
Кстати, в изначально предложенном варианте в тестах в качестве операндов используются строковые литералы. Вы предлагаете в них писать?
Это придётся делать в любом случае.
Вы почему-то верите в рекурсию как в волшебство какое-то.
В
realloc
передаётся указатель.Этот указатель должен быть получен от функций
malloc
,calloc
или той жеrealloc
.Передача туда какого-то другого адреса приведёт к UB.
Не получится использовать
realloc
, это все равно будетmalloc
, хотя это и не принципиально.Но в предложенном вами варианте возникает неудобство, потому что, если переноса нет, то следует ли освобождать память, зависит от того, динамически она была выделена под операнд на вызывающей стороне или нет.
А если перенос был, то освобождать следует обязательно.
Более того, может быть использован один или другой операнд, что ещё больше усложняет дело.
Вы имеет ввиду рекурсивную реализацию?
Я попробовал выполнить честную рекурсивную реализацию на C достаточно близко, насколько возможно, к вашему коду на Elixir'е, без
reverse
и без конкатенации.Что интересно: 3 компилятора смогли применить TCO, а 4-ый не справился, а именно — MSVC.
При этом на тех компиляторах. которые справились, производительность оставляет желать лучшего.
Я перемерил оригинальный код, свой код и этот код, что выше.
Для такого теста:
оригинальный код на некоторой машине исполняется, примерно, 5.184 секунд, мой вариант — 3.275 секунды, рекурсивный вариант выше — 10.984 секунд.
Чуда не произошло.
Ну, и рекурсивная реализация на C явно не блещет изяществом.
Да хоть в гугольный раз.
Тот код, который вы называете production-ready, на самом деле является просто "грамотным" кодом.
Но говорите-то вы про сейчас:
В вашем тексте нет указания на то, что вы говорите о компиляторах 25-летней давности.
В отличие от вас, я проверил, что там происходит на самом деле.
Все 4 компилятора, которые я проверил, применили TCO, в этой части @cupraer прав.
А вы продолжаете генерировать безответственные заявления.
Ну, то есть, не все.
"Папа может" безобразно, на уровне джуна.
Очевидно, во фрагментах, которые я предоставлял:
Данный фрагмент как раз относится к случаю реально длинных чисел, о котором вы упоминали в том духе, что ваши тесты этот случай не покрывают.
Разве в то время возвращаемое функцией
malloc
значение не требовалось проверять наNULL
?К тому же в то время ещё можно было реально встретить платформы с 16-битным
int
'ом.Это значит, что достаточно было 32/64 КБайтных чисел, чтобы получить такой же эффект.
Вот про "сознательно оставленные" не верится совсем.
Не очевидно, что это хорошее решение.
Попробуйте выполнить reverse "in-place", и вы тут же обнаружите, что перед тем, как начать, вы будете вынуждены сначала найти конец строки.
Это и есть то, что, по сути, делает
strlen
.И код не будет короче из-за всяких вспомогательных функций, которые потребуются при рекурсивном подходе.
Потребуется.
Я визуализировал ваш код на Elixir'е:
Здесь видно, что
acc
— это отдельный третий список.Вот для него и нужен
malloc
.Точнее, в данной реализации — многократный
realloc
.А если передаются локальные или статические массивы?
Какой может быть
realloc
, если мы больший из них перезаписываем в качестве выходного?Это только так кажется.
Вам были не очевидны проблемы, которые я здесь обозначил.
Поэтому вы, наверняка, ошибаетесь и здесь.
Говорят, мудрость приходит с годами.
Но иногда годы приходят одни.
Ваш набор тестов покрывает все случаи?
Мои варианты тестов, которые демонстрируют низкое качество вашего кода, вы предпочитаете не замечать?
В моменты "набросать код за пять минут" проявляются программистские рефлексы, потому что особо обдумывать некогда.
Ваши программистские рефлексы диктуют вам в любой непонятной ситуации использовать тип
int
и не обрабатывать возможные ошибки.Да, это похоже на уровень джуна.
То, что такие существенные вещи вы называете придиркой к запятым, дополнительно указывает на то, что гордиться и чваниться здесь нечем.
Что интересно, различные ИИ для этой задачи почти поголовно тоже пытаются применять
int
, и даже тот, кто сначала применилsize_t
, все равно потом использовалint
.Однако, ни один из 4-х не забыл проверить результат вызова
malloc
с самого начала.И ни один, после указания на проблему с типом
int
, не пытался заменятьint
наunsigned long
или, например,uint64_t
, — все сразу переходили наsize_t
, очевидно, "понимая" его смысл.Верно, чваниться умеете на хорошем уровне, а признавать ошибки не умеете совсем.
Очевидно, вы не не поняли, на что был намёк.
В этом случае:
возникает Segmentation Fault.
Вы поняли, почему?
Поясняю: потому что после вызова функции
malloc
указатель, который она вернула, не был проверен на `NULL`.То, что функция
malloc
может вернутьNULL
, и что поэтому необходимо проверять наNULL
это значение, ясно видно по распечатке значенияresult_str
выше и по результату дальнейшего исполнения программы без проверки его наNULL
.Фрагмент кода, чтобы было понятно, причём тут
result_str
:Отсутствием проверки значения, которое вернула функция
malloc
, часто грешит код современных программ, и в существенной степени именно поэтому они так себя ведут в случае очень малого запаса по памяти.Переполнение знакового
int
'а может приводить к более широкому кругу глюков.Замечу также, что в 2025-ом году 2 гигабайта — это уже относительно небольшой объём памяти, поэтому ваше "Вы там определитесь как-нибудь" звучит неубедительно.
Думаю, что это — терминологическая проблема.
Или проблема того, под каким углом зрения на неё смотреть.
В терминах стандарта, видимо, это — evaluation.
Evaluation'а нет. Unevaluated context.
То есть, в терминах стандарта это — невычислимый контекст.
Однако, невычислимый контекст всё ещё считается выражением, в отличие от декларации. И разыменование есть, просто оно не вычисляется.
А в декларации разыменования нет, потому что это — вообще не выражение.
То есть, на это можно смотреть по-разному, но стандарт смотрит как на выражение, только невычислимое, что не отменяет наличие разыменования.
А вы пытаетесь отождествить вычислимость разыменования с самим наличием оного.
Здесь, скорее, имеет место терминологическая разница или даже разница в углах зрения на одно и то же.
Можете обосновать в данном случае?
Или просто так сказали про "не могут"?
То, что она неработающая, я аргументированно показал выше.
А это своё голословное утверждение можете обосновать?
Где я рекомендовал всё переписать в данном случае?
Тем более, что здесь не особо что-то оптимизируется:
С такой функцией main:
и замене в оригинальном коде проблемных
int
'ов наunsigned
, чтобы оригинальный код заработал на этих конкретных данных, производительность растёт не очень сильно, хотя и растёт.Типичное суммарное время исполнения вместе с инициализацией массива оригинального кода:
а представленного выше:
Хотя, разница на уровне чистого времени исполнения функции
mega_add
будет больше.Есть такая каста людей, которая, не думая, любит бездоказательно и безответственно применять расхожее клише.
Да, иногда очень разные.
Код — не рабочий, и я это аргументированно показал.
Именно из-за такого кода, как ваш, многие современные программы глючат, особенно в условиях ограниченного запаса по памяти.
А "критикукуете" здесь вы, и, в основном, не по делу.
И ещё чванитесь перед молодёжью, хотя и нечем.
Во-первых,
int
там не нужен, достаточно написатьunsigned long
.Во-вторых, это тоже — неправильный тип.
Незнания такого уровня, которые вы продемонстрировали, — это не о коде, готовом к выкату.
Но знания у вас с того времени остались на том уровне, на котором по любому поводу используют
int
, что вы наглядно и продемонстрировали.Керниган и Ритчи написали, что
int
— самый эффективный тип C.Видимо, вы на этом уровне понимания языка и остановились.
Опыт джуновского уровня?
Тип
size_t
появился в первом же стандарте C, C89/90.Имя этого типа совершенно точно не загромождает текст.
Вам не хватило опыта, чтобы догадаться взглянуть на прототип
strlen
, чтобы увидеть, какой тип — правильный.А чваниться перед молодёжью пытаетесь, как будто — сеньор.