All streams
Search
Write a publication
Pull to refresh
2
0

User

Send message

Спасибо за ответ.

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

А иначе — не выяснить, как оно на самом деле.
Приходится пробовать самому с чьей-то подачи.

Тут — вопрос выбора.
Каждый человек для себя выбирает, как поступить.
Кому-то важно одно, кому-то — другое.

Вы сначала объясните, что и кому Вы пытаетесь доказать.

Доказать?

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

Вы компилируете без оптимизации.

Знаете, что означает "O" в аббревиатуре TCO?

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

Да, прямо сейчас у меня нет такого.
Но оно и не нужно.

А он говорит такое:

~> gcc --version gcc (GCC) 4.2.1 20070719

~> gcc 1.c -S

Вы правда не понимаете или прикидываетесь?

Вот смотрите, компилятор gcc 3.4.6 НЕ осуществляет TCO:

test:
        push    %rbp
        mov     %rbp, %rsp
        sub     %rsp, 16
        mov     DWORD PTR [%rbp-4], %edi
        mov     %esi, DWORD PTR [%rbp-4]
        mov     %edi, OFFSET FLAT:.LC0
        mov     %eax, 0
        call    printf
        mov     %edi, DWORD PTR [%rbp-4]
        inc     %edi
        call    test
        leave
        ret

И тот же самый компилятор теперь уже ОСУЩЕСТВЛЯЕТ:

test:
        push    %rbx
        mov     %ebx, %edi
.L2:
        mov     %esi, %ebx
        mov     %edi, OFFSET FLAT:.LC0
        xor     %eax, %eax
        inc     %ebx
        call    printf
        jmp     .L2

Объяснять, почему, или сами догадаетесь?

(пританцовывая на месте:) Блин, ну сколько уже ему намёков давать можно? Когда, когда же он наконец спросит «а какой версией gcc эти примеры скомпилированы были?»

Современной, поскольку вы сказали понадобится, а не понадобилось в бы прошлом:

Ему всего-то понадобится овердофига места в стеке.

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

Но могли бы и сами проверить.

Сейчас самая древняя доступная работоспособная версия там — 3.4.6, это — 2005-ый год, 20 лет назад.

Вот результат:

test:
        push    %rbx
        mov     %ebx, %edi
.L2:
        mov     %esi, %ebx
        mov     %edi, OFFSET FLAT:.LC0
        xor     %eax, %eax
        inc     %ebx
        call    printf
        jmp     .L2

Если отключить заголовочный файл, то относительно работоспособной становится случайно затесавшаяся туда версия 1.27, которая, действительно, не выполняет TCO:

test:
        pushl %ebp
        movl %esp,%ebp
        pushl 8(%ebp)
        pushl $.LC0
        call printf
        movl 8(%ebp),%eax
        incl %eax
        pushl %eax
        call test
        leave
        ret

Но это, всё-таки, 1988-ой год.
А вы говорили о 25-и годах, а не о 37-и.

Мама дорогая.

Я портировал максимально близко к вашему коду Elixir'а.
О чём и написал.

Я никогда в жизни не зарабатывал денег написанием кода на c,

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

Но дело — не в этом.

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

Собственно, ваш код получится, если упростить мой, выбросив рекурсивное вычисление длин операндов, а также сдвиг результата для случая отсутствия финального переноса, и с'merge'ить большинство функций (do_add1...do_add6, do_addn) в одну, отойдя от кода на Elixir'е.

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

В другом комментарии вы пишете:

Рекурсии не требуется знать длину списка, а обычному циклу с индексом — требуется.

Но в своём примере почему-то дважды вызвали strlen.
У меня эта работа выполнялась в pre_add.

Значит, чуда никакого нет, и рекурсии всё-таки требуется знать длину списка, просто это выглядит не так явно.

Если вашу рекурсивную реализацию сравнить с оригиналом (там — под спойлером), то выяснится, что ваша ничуть не меньше по объёму, и ничуть не более понятна.

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

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

Однако, изящность в этом месте будет стоить быстродействия.

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

Ну, и самое главное, что касается рекурсии, TCO не гарантируется.

С вашим кодом MSVC справился, смог применить TCO.
С моим, который сложнее, — нет.

Я вот это написал

Так ведь необходимо не только написать.
Необходимо ещё и правильно скомпилировать.

Если взглянуть на то, как с вашим кодом справились 4 компилятора при правильной компиляции, то выяснится, что все они применили TCO.

Ассемблерный код от gcc:

test:
        push    rbx
        mov     ebx, edi
.L2:
        mov     esi, ebx
        mov     edi, OFFSET FLAT:.LC0
        xor     eax, eax
        inc     ebx
        call    printf
        jmp     .L2

От clang'а:

test:
        push    r14
        push    rbx
        push    rax
        mov     ebx, edi
        lea     r14, [rip + .L.str]
.LBB0_1:
        mov     rdi, r14
        mov     esi, ebx
        xor     eax, eax
        call    printf@PLT
        inc     ebx
        jmp     .LBB0_1

От Intel'овского icx:

test:
        push    rbx
        mov     ebx, edi
.LBB0_1:
        mov     edi, offset .L.str
        mov     esi, ebx
        xor     eax, eax
        call    printf
        inc     ebx
        jmp     .LBB0_1

От MSVC:

test    PROC
$LN7:
        push    rbx
        sub     rsp, 32                             ; 00000020H
        mov     ebx, ecx
        npad    8
$LL3@test:
        mov     edx, ebx
        lea     rcx, OFFSET FLAT:$SG9630
        call    printf
        inc     ebx
        jmp     SHORT $LL3@test
test    ENDP

Доработал ваш код, убрав UB и остановив рекурсию на моменте, когда кончатся положительные int'ы:

#include <stdio.h>
#include <limits.h>

int test(int n) {
  printf("%d\n", n);

  if (n == INT_MAX)
    return n;

  return test(n + 1);
}

int main() {
  test(1);
}

Собрал правильным образом локально у себя на машине, и ассемблерный код получился таким:

test:
.LFB3:
        .cfi_startproc
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        lea     rbp, .LC0[rip]
        push    rbx
        .cfi_def_cfa_offset 24
        .cfi_offset 3, -24
        mov     ebx, edi
        sub     rsp, 8
        .cfi_def_cfa_offset 32
        jmp     .L3
        .p2align 4,,10
        .p2align 3
.L7:
        add     ebx, 1
.L3:
        xor     eax, eax
        mov     esi, ebx
        mov     rdi, rbp
        call    printf@PLT
        cmp     ebx, 2147483647
        jne     .L7
        add     rsp, 8
        .cfi_def_cfa_offset 24
        mov     eax, 2147483647
        pop     rbx
        .cfi_def_cfa_offset 16
        pop     rbp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc

По самой интересной его части:

.L7:
        add     ebx, 1
.L3:
        xor     eax, eax
        mov     esi, ebx
        mov     rdi, rbp
        call    printf@PLT
        cmp     ebx, 2147483647
        jne     .L7

Видно, что компилятор применил TCO.

Далее, я запустил программу, вырезав середину её вывода и оставив только первые 3 и последние 3 строки:

$ (exec 3> >(head -n 3; echo '...'; cat > /dev/null 2>&1); exec 4> >(tail -n 3); ./1 | tee >(cat >&3) >(cat >&4) > /dev/null)
1
2
3
...
2147483645
2147483646
2147483647
$ 

Пришлось подождать минуты 3, но, как видно по результату, весь запас положительных значений int'а был успешно исчерпан.

Кстати, вы умудрились даже в таком простом коде допустить UB: функция test должна возвращать значение.

Да, я уже слышал про обратностороннего салфеточника, который пишет на обратной стороне салфетки код, не являющийся production-ready.

Однако, компиляторам — все равно, по какой причине в программе имеется UB, они не читают habr.

При наличии UB в программе компиляторы имеют право генерировать любой код, в том числе, неправильный.

Но в данном случае они этим правом не воспользовались, видимо, потому, что рекурсия — бесконечная, о чём они, кстати, и пишут в своих предупреждениях.

Если у Вас 63 мегабайта, выжранное программой из трёх строчек — это не «как не в себя»,

Вот, сколько памяти потребляет процесс у меня через 2 с половиной минуты исполнения, незадолго до его завершения:

    PID VIRT    RES    %CPU  %MEM     TIME+ COMMAND
3342451 2460    892   100.0   0.0   2:31.19 1

Даже до 1 МБайта не дотягивает.

то я даже и не знаю.

Да, вы почему-то многого не знаете.

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

Эммммм.... алло, гараж?

Нет, у вас написано по этому поводу:

Ему всего-то понадобится овердофига места в стеке.

Вы это написали?

Да, reverse по месту выполнить не удастся, это правда. Тут (и только тут) я ошибся.

Я бы не был столь категоричен насчёт "только тут".

Это каких например?

Да тот же reverse.

Эликсир полностью иммутабелен. Си, когда я в последний раз заглядывал в спецификацию, — нет. И я черным по белому написал: по месту,

Все реализации, и даже изначально предложенная C-шная, возвращают третье значение, не трогая операнды.

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

Это выходит за рамки задачи в том виде, как она была сформулирована. Если надо ничего не попортить — придется вызывать length и malloc, что попахивает.

Это придётся делать в любом случае.
Вы почему-то верите в рекурсию как в волшебство какое-то.

Я в том же предложении пояснил: единицу накопленную может потребоваться добавить.

В realloc передаётся указатель.
Этот указатель должен быть получен от функций malloc, calloc или той же realloc.
Передача туда какого-то другого адреса приведёт к UB.

Проблемы? Проблем нет, я погорячился с вызовом length, который на алгоритмическую сложность не влияет; все остальное — в силе.

Не получится использовать realloc, это все равно будет malloc, хотя это и не принципиально.

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

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

Зато вам вообще не пришёл в голову единственно правильный и изящный способ решения, вы сразу приступили к код-ревью заведомо плохо поддерживаемого спагетти.

Вы имеет ввиду рекурсивную реализацию?

Я попробовал выполнить честную рекурсивную реализацию на C достаточно близко, насколько возможно, к вашему коду на Elixir'е, без reverse и без конкатенации.

Что интересно: 3 компилятора смогли применить TCO, а 4-ый не справился, а именно — MSVC.

char *do_addn(char const *const ns1,
              char const *const ns2,
              char const *const re,
              char const *const n1,
              char const *const n2,
              char *const r,
              char const c);

char *do_add1(char const *const re,
              char *const r)
{
  return memmove(r, r + 1, re - r);
}

char *do_add2(char *const r) {
  *r = '1';
  return r;
}

char *do_add3(char const *const ns1,
              char const *const ns2,
              char const *const re,
              char const *const n2,
              char *const r)
{
  char const val = (*n2 - '0') + 1;
  char const cry = val > 9;

  *r = (cry ? val - 10 : val) + '0';

  return do_addn(ns1, ns2, re, ns1, n2, r, cry);
}

char *do_add4(char const *const ns2,
              char const *const re,
              char const *const n2,
              char *const r)
{
  size_t const s = ns2 - n2 + 1;
  memcpy(r - s + 1, ns2, s);

  return do_add1(re, r - s);
}

char *do_add5(char const *const ns1,
              char const *const ns2,
              char const *const re,
              char const *const n1,
              char *const r,
              char const c)
{
  if (!c) {
    return do_add4(ns1, re, n1, r);
  }

  return do_add3(ns2, ns1, re, n1, r);
}

char *do_add6(char const *const ns1,
              char const *const ns2,
              char const *const re,
              char const *const n1,
              char const *const n2,
              char *const r,
              char const c)
{
  char const val = (*n1 - '0') + (*n2 - '0') + c;
  char const cry = val > 9;

  *r = (cry ? val - 10 : val) + '0';

  return do_addn(ns1, ns2, re, n1, n2, r, cry);
}

char *do_addn(char const *const ns1,
              char const *const ns2,
              char const *const re,
              char const *const n1,
              char const *const n2,
              char *const r,
              char const c)
{
  if (ns1 == n1) {
    if (ns2 == n2) {
      if (!c) {
        return do_add1(re, r - 1);
      }

      return do_add2(r - 1);
    } else {
      if (!c) {
        return do_add4(ns2, re, n2 - 1, r - 1);
      }

      return do_add3(ns1, ns2, re, n2 - 1, r - 1);
    }
  } else if (ns2 == n2) {
    return do_add5(ns1, ns2, re, n1 - 1, r - 1, c);
  }

  return do_add6(ns1, ns2, re, n1 - 1, n2 - 1, r - 1, c);
}

char *pre_add(char const *const ns1,
              char const *const ns2,
              char const *const n1,
              char const *const n2)
{
  if (!*n1) {
    if (!*n2) {
      size_t const s1 = n1 - ns1;
      size_t const s2 = n2 - ns2;
      size_t const s = (s1 > s2 ? s1 : s2) + 1;

      char *const r = malloc(s + 1);

      if (!r) {
        return NULL;
      }

      r[s] = '\0';
      return do_addn(ns1, ns2, r + s, n1, n2, r + s, 0);
    } else {
      return pre_add(ns1, ns2, n1, n2 + 1);
    }
  } else if (!*n2) {
    return pre_add(ns1, ns2, n1 + 1, n2);
  }

  return pre_add(ns1, ns2, n1 + 1, n2 + 1);
}

char *mega_add_recur(char const *const n1,
                     char const *const n2)
{
  return pre_add(n1, n2, n1, n2);
}

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

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

Для такого теста:

int main2(void) {
  static char a[((size_t)1 << (sizeof(int) * CHAR_BIT - 1)) + 1];

  memset(a, '9', sizeof a - 1);
  test(a, "9");
  return EXIT_SUCCESS;
}

оригинальный код на некоторой машине исполняется, примерно, 5.184 секунд, мой вариант — 3.275 секунды, рекурсивный вариант выше — 10.984 секунд.

Чуда не произошло.

Ну, и рекурсивная реализация на C явно не блещет изяществом.

Повторяю в шестой раз: Вы реально ожидаете в комментарии на Хабре production-ready кода?

Да хоть в гугольный раз.

Тот код, который вы называете production-ready, на самом деле является просто "грамотным" кодом.

Не было ещё TCO 25 лет назад, прикиньте?

Но говорите-то вы про сейчас:

Ему всего-то понадобится овердофига места в стеке.

В вашем тексте нет указания на то, что вы говорите о компиляторах 25-летней давности.

Ему всего-то понадобится овердофига места в стеке.

В отличие от вас, я проверил, что там происходит на самом деле.

Все 4 компилятора, которые я проверил, применили TCO, в этой части @cupraer прав.

А вы продолжаете генерировать безответственные заявления.

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

Ну, то есть, не все.

но я уже потерял счёт количеству раз, когда я сказал, что это proof‑of‑concept, демонстратор «смотри, как папа может» кода, а не o

"Папа может" безобразно, на уровне джуна.

Я «Ваших тестов» вообще в упор не вижу. Мои были прямо под кодом — а Ваши где?

Очевидно, во фрагментах, которые я предоставлял:

int main(void) {
  static char a[((size_t)1 << (sizeof(int) * CHAR_BIT - 1)) + 1];

  memset(a, '9', sizeof a - 1);
  test(a, "9");
  return EXIT_SUCCESS;
}..

Данный фрагмент как раз относится к случаю реально длинных чисел, о котором вы упоминали в том духе, что ваши тесты этот случай не покрывают.

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

Разве в то время возвращаемое функцией malloc значение не требовалось проверять на NULL?

К тому же в то время ещё можно было реально встретить платформы с 16-битным int'ом.

Это значит, что достаточно было 32/64 КБайтных чисел, чтобы получить такой же эффект.

сознательно оставленные упущения

Вот про "сознательно оставленные" не верится совсем.

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

Не очевидно, что это хорошее решение.

Рекурсивный код тут будет в три раза короче, ему не потребуются ни O(n) вызовы strlen в начале, ни куча условных операторов

Попробуйте выполнить reverse "in-place", и вы тут же обнаружите, что перед тем, как начать, вы будете вынуждены сначала найти конец строки.

Это и есть то, что, по сути, делает strlen.

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

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

Потребуется.

Я визуализировал ваш код на Elixir'е:

(6) do_add([h1 | t1]: [8, 4, 2], [h2 | t2]: [6, 8, 9], {remainder, acc}: {0, []})
  (6) do_add([h1 | t1]: [4, 2], [h2 | t2]: [8, 9], {remainder, acc}: {1, [4]})
    (6) do_add([h1 | t1]: [2], [h2 | t2]: [9], {remainder, acc}: {1, [3, 4]})
      (2) do_add([], [], {1, acc: [2, 3, 4]})
      (2) return: [1, 2, 3, 4]
    (6) return: [1, 2, 3, 4]
  (6) return: [1, 2, 3, 4]
(6) return: [1, 2, 3, 4]

Здесь видно, что acc — это отдельный третий список.

Вот для него и нужен malloc.
Точнее, в данной реализации — многократный realloc.

Когда вызов вернется, потенциально придется разок вызвать realloc, если осталась неучтенная единица.

А если передаются локальные или статические массивы?

Какой может быть realloc, если мы больший из них перезаписываем в качестве выходного?

Я уже приводил реализацию на эликсире, переписать её на си — не так уж сложно.

Это только так кажется.

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

Конечно! Я что, зря этого момента джва года двадцать пять лет ждал?

Говорят, мудрость приходит с годами.
Но иногда годы приходят одни.

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

Ваш набор тестов покрывает все случаи?

Мои варианты тестов, которые демонстрируют низкое качество вашего кода, вы предпочитаете не замечать?

А если Вам охота не «набросать код за пять минут и читать Хабр дальше»

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

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

Да, это похоже на уровень джуна.

а придираться к запятым — то не смею Вам препятствовать.

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

Что интересно, различные ИИ для этой задачи почти поголовно тоже пытаются применять int, и даже тот, кто сначала применил size_t, все равно потом использовал int.

Однако, ни один из 4-х не забыл проверить результат вызова malloc с самого начала.

И ни один, после указания на проблему с типом int, не пытался заменять int на unsigned long или, например, uint64_t, — все сразу переходили на size_t, очевидно, "понимая" его смысл.

Но это уже без меня.

Верно, чваниться умеете на хорошем уровне, а признавать ошибки не умеете совсем.

С одной стороны у вас запас памяти ограничен, а с другой - у вас числа по 2 гигабайта, чтобы знаковый int переполнялся. Вы там определитесь как-нибудь.

Очевидно, вы не не поняли, на что был намёк.

В этом случае:

$ ./1
mega_add: len1: -3, len2: -3, res_len: -1
mega_add: result_str: (nil)
Segmentation fault
$ 

возникает Segmentation Fault.

Вы поняли, почему?

Поясняю: потому что после вызова функции malloc указатель, который она вернула, не был проверен на `NULL`.

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

Фрагмент кода, чтобы было понятно, причём тут result_str:

  char* result_str = malloc(res_len);
  printf("%s: result_str: %p\n", __func__, result_str);

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

Переполнение знакового int'а может приводить к более широкому кругу глюков.

Замечу также, что в 2025-ом году 2 гигабайта — это уже относительно небольшой объём памяти, поэтому ваше "Вы там определитесь как-нибудь" звучит неубедительно.

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

Думаю, что это — терминологическая проблема.
Или проблема того, под каким углом зрения на неё смотреть.

Вот этот самый indirection/dereference — это action, действие.

В терминах стандарта, видимо, это — evaluation.

Так именно действия здесь и нет

Evaluation'а нет. Unevaluated context.
То есть, в терминах стандарта это — невычислимый контекст.

Однако, невычислимый контекст всё ещё считается выражением, в отличие от декларации. И разыменование есть, просто оно не вычисляется.

А в декларации разыменования нет, потому что это — вообще не выражение.

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

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

А это особая каста людей, сами ничего работоспособного создать не могут

Можете обосновать в данном случае?
Или просто так сказали про "не могут"?

зато всегда наготове с придирками к чужой работающей кодовой базе.

То, что она неработающая, я аргументированно показал выше.

Они еще всегда рекомендуют «всё переписать» и начинают имплементацию FizzBuzz с оптимизаций тиков на делении.

А это своё голословное утверждение можете обосновать?
Где я рекомендовал всё переписать в данном случае?

Тем более, что здесь не особо что-то оптимизируется:

char *mega_add_eptr(char const *inp1, char const *inp2) {
  size_t len1 = strlen(inp1);
  size_t len2 = strlen(inp2);

  if (len1 > len2) {
    char const *const p = inp1;
    size_t const l = len1;

    inp1 = inp2;
    inp2 = p;

    len1 = len2;
    len2 = l;
  }

  char *const res = malloc(len2 + 1 + 1);

  if (res) {
    int carry = 0;
    char const *p1 = inp1 + len1;
    char const *p2 = inp2 + len2;
    char *p3 = res + len2;

    *p3 = '\0';

    while (p1 > inp1) {
      int const s = (*--p1 - '0') + (*--p2 - '0') + carry;

      *--p3 = s + ((carry = s > 9) ? '0' - 10 : '0');
    }

    while (p2 > inp2 && carry) {
      int const s = (*--p2 - '0') + carry;

      *--p3 = s + ((carry = s > 9) ? '0' - 10 : '0');
    }

    if (p2 > inp2) {
      memcpy(res, inp2, p2 - inp2);
    }

    if (carry) {
      memmove(res + 1, res, len2 + 1);
      *res = '1';
    }
  }

  return res;
}

С такой функцией main:

int main(void) {
  static char a[((size_t)1 << (sizeof(int) * CHAR_BIT - 1)) + 1];

  memset(a, '9', sizeof a - 1);
  test(a, "9");
  return EXIT_SUCCESS;
}

и замене в оригинальном коде проблемных 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 будет больше.

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

Во-первых, сеньоры разные бывают.

Да, иногда очень разные.

А во-вторых, я хотя бы написал рабочий код — а Вы только критикукуете.

Код — не рабочий, и я это аргументированно показал.

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

А "критикукуете" здесь вы, и, в основном, не по делу.
И ещё чванитесь перед молодёжью, хотя и нечем.

Ну да, ну да, unsigned long int «нисколько не загромождает текст».

Во-первых, int там не нужен, достаточно написать unsigned long.
Во-вторых, это тоже — неправильный тип.

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

Незнания такого уровня, которые вы продемонстрировали, — это не о коде, готовом к выкату.

И да, я на C вплотную не писал уже лет эдак 25.

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

Керниган и Ритчи написали, что int — самый эффективный тип C.
Видимо, вы на этом уровне понимания языка и остановились.

Но опыт всё-таки не пропьёшь.

Опыт джуновского уровня?

Тип size_t появился в первом же стандарте C, C89/90.
Имя этого типа совершенно точно не загромождает текст.

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

А чваниться перед молодёжью пытаетесь, как будто — сеньор.

Information

Rating
Does not participate
Registered
Activity