Просматривая недавно соцсети, я наткнулся на этот скриншот. Разумеется, его сопровождало множество злобных комментариев, критикующих попытку этого новичка в программировании решить классическую задачу computer science: операцию деления с остатком.

В современном мире, где ИИ постепенно заменяет программистов, отнимая у них работу и совершая переворот в том, как мы подходим к рассуждениям о коде, нам, возможно, следует быть более открытыми к мыслям людей, недавно пришедших в нашу отрасль? На самом деле, показанный выше код — идеальный пример компромисса между временем и задействованной памятью. Мы жертвуем временем и в то же время памятью и временем компьютера! Поистине чудесный алгоритм!

Поэтому я решил изучить эту идею проверки чётности числа при помощи одних сравнений, чтобы понять, насколько хорошо она работает в реальных ситуациях. Я сторонник высокопроизводительного кода, поэтому решил реализовать это на языке программирования C, потому что он и сегодня остаётся самым быстрым языком в мире с большим отрывом от других (благодаря гению Денниса Ричи).

Итак, я приступил к кодингу.

/* Copyright 2023. Любое неавторизованное распространение этого исходного кода 
   будет преследоваться по всей строгости закона */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
    uint8_t number = atoi(argv[1]); // Здесь никаких проблем
    if (number == 0)
        printf("even\n");
    if (number == 1)
        printf("odd\n");
    if (number == 2)
        printf("even\n");
    if (number == 3)
        printf("odd\n");
    if (number == 4)
        printf("even\n");
    if (number == 5)
        printf("odd\n");
    if (number == 6)
        printf("even\n");
    if (number == 7)
        printf("odd\n");
    if (number == 8)
        printf("even\n");
    if (number == 9)
        printf("odd\n");
    if (number == 10)
        printf("even\n");
}

Красота! Давайте скомпилируем код, отключив оптимизации при помощи /Od, чтобы надоедливый компилятор не вмешивался в наш алгоритм. После компиляции мы можем выполнить быстрый тест программы и получить положительные результаты:

PS > cl.exe /Od program.c
PS > .\program.exe 0 
even
PS > .\program.exe 4
even
PS > .\program.exe 3
odd
PS > .\program.exe 7
odd

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

PS > .\program.exe 50
PS > .\program.exe 11
PS > .\program.exe 99

Программа ничего не выводит! Похоже, она работает только с числами меньше 11! Вернувшись к коду, мы можем найти проблему прямо за последним оператором if — нам нужно больше if!

Конечно, это компромисс между временем и используемой памятью, но моё время в этом мире ограничено, поэтому я решил воспользоваться мета-программированием операторов if при помощи программы-программатора на другом языке программирования. Чтобы компенсировать это жульничество, я решил использовать самый медленный (благодаря гению Росса ван дер Гуссома) язык в мире — Python.

print("/* Copyright 2023. Любое неавторизованное распространение этого исходного кода
print("   будет преследоваться по всей строгости закона */")

print("#include <stdio.h>")
print("#include <stdint.h>")
print("#include <stdlib.h>")

print("int main(int argc, char* argv[])")
print("{")
print("    uint8_t number = atoi(argv[1]); // Здесь никаких проблем")

for i in range(2**8):
    print("    if (number == "+str(i)+")")
    if i % 2 == 0:
        print("        printf(\"even\\n\");")
    else:
        print("        printf(\"odd\\n\");")

print("}")

Отлично! Теперь мы можем сгенерировать программу, решающую задачу чётности для всех 8-битных целых чисел!

PS > python programmer.py > program.c
PS > cl.exe /Od program.c
PS > .\program.exe 99
odd
PS > .\program.exe 50
even
PS > .\program.exe 240
even
PS > .\program.exe 241
odd

Посмотрите-ка на это! Работает идеально! Теперь давайте увеличим масштабы до 16 битов!

print("    uint16_t number = atoi(argv[1]); // Здесь никаких проблем")
…
for i in range(2**16):

Мы получаем красивый жирный файл .c примерно из 130 тысяч строк. На самом деле, это ничто по сравнению с некоторыми кодовыми базами, с которыми я сталкивался за годы работы. Давайте его скомпилируем!

PS > python programmer.py > program.c
PS > cl.exe /Od program.c
PS > .\program.exe 21000
even
PS > .\program.exe 3475 
odd
PS > .\program.exe 3   
odd
PS > .\program.exe 65001
odd
PS > .\program.exe 65532
even

Красота! Похоже, наш алгоритм масштабируется вместе с данными! Исполняемый файл имеет размер примерно 2 МБ, но это ерунда для моей мощной игровой машины с 31,8 ГБ памяти.

Ну, 16 битов — это очень прекрасная битовая ширина, но, как мы знаем, Святой Грааль вычислений — это 32 бита, и это последняя битовая ширина, которой достаточно для решения всех практических инженерных и научных задач. В конце концов, IPv4 чувствует себя как никогда хорошо, хотя его уже шестьдесят лет назад признали устаревшим из-за так называемого «исчерпания адресов».

Так что давайте без лишних церемоний увеличим масштаб до нашего последнего размера. В 32 битах всего в 65536 раз больше чисел, чем в 16 битах, что может пойти не так?

print("    uint32_t number = atoi(argv[1]); // Здесь никаких проблем")
…
for i in range(2**32):

Позволим могучей змее выполнить свою работу: выпив кофе и вернувшись к программе спустя 48 часов, я обнаружил красивый файл .c размером почти 330 ГБ! Я практически уверен, что это самый большой файл .c в истории. Когда я вводил следующую команду, мои пальцы дрожали: MSVC наверняка раньше никогда не сталкивался с таким мощным исходным кодом. После активных получасовых издевательств над файлом подкачки моего бедного мощного компьютера я получил следующее:

PS > cl /Od program.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.32.31329 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

program.c
program.c(134397076): warning C4049: compiler limit: terminating line number emission
program.c(134397076): note: Compiler limit for line number is 16777215
program.c(41133672): fatal error C1060: compiler is out of heap space

Жалкое зрелище!

И нас подвёл не только компилятор: изучив ограничения формата Portable Executable (.exe) для Windows, я обнаружил, что он не может обрабатывать больше, чем жалкие 4 ГБ! Учитывая, что в исполняемом файле должно быть закодировано 4 миллиарда сравнений, это серьёзное препятствие для реализации нашего алгоритма. Даже если каждое сравнение будет использовать меньше одного байта, файл всё равно будет слишком тяжёлым.

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

Начнём с написания функции IsEven на языке ассемблера x86-64, потому что это нативный язык моей машины с процессором Intel. Функция выглядит примерно так:

; Аргумент хранится в ECX, возвращаемое значение в EAX
XOR EAX, EAX ; Присваивам eax значение 0 (возвращаемое значение для нечётного числа)
CMP ECX, 0h ; Сравниваем аргумент с 0 
JNE 3h ; Пропускаем следующие две команды в случае неравенства
INC EAX ; Если число чётное, задаём возвращаемое значение чётного числа (1)
RET ; Возврат
CMP ECX, 1h ; Сравниваем аргумент с 1
JNE 2 ; Пропускаем следующую команду в случае неравенства
RET ; Возвращаемое значение нечётного числа уже находится в EAX, просто делаем RET
; сюда вставляем следующие 2...2^32-1 сравнений
RET ; Аварийный возврат

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

Как я это сделал? Я вышел в Интернет с таким вопросом. Воспользовался своим опытом написания эмуляторов и хакинга, изучил руководства по архитектуре x86(-64), чтобы найти нужные опкоды и форматы для каждой команды.

… Да нет, шучу, это было бы ужасно. Я просто спросил у ChatGPT правильный опкод для каждой команды; к счастью для нас, она не сгаллюцинировала новые расширения для x86-64.

Теперь нам просто достаточно написать «компилятор» для вывода этого кода. Нужно учесть, что мы будем писать опкоды, которые получили от ИИ непосредственно для команд. Вот, как это выглядит на нашем любимом Python:

import struct

with open('isEven.bin', 'wb') as file:
   
    file.write(b"\x31\xC0")                     # XOR EAX, EAX

    for i in range(2**32):
        ib = struct.pack("<I", i)               # Кодируем i как 32-битное целое в little endian

        file.write(b"\x81\xF9" + ib)            # CMP ECX, i

        if i%2 == 0:
            file.write(b"\x75\x03")             # JNE +3
            file.write(b"\xFF\xC0")             # INC EAX
            file.write(b"\xC3")                 # RET
        else:
            file.write(b"\x75\x01")             # JNE +1
            file.write(b"\xC3")                 # RET

    file.write(b"\xC3")                         # Аварийный RET 

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

Мы получаем милый файл на 40 ГБ, содержащий все 4,2 миллиарда сравнений, необходимых для определения чётности любого 32-битного числа! Теперь нам просто нужно написать хост-программу, которая сможет загружать и использовать эти команды. Для повышения производительности (в нашем случае это очень важно) я решил отобразить файл в адресное пространство, а не читать его целиком. Сделав так, мы притворимся, что весь файл уже находится в памяти, и позволим бедной операционной системе разбираться с размещением блоба на 40 ГБ в виртуальной памяти. Отобразив файл с разрешениями READ и EXECUTE, мы можем вызывать код при помощи указателя функции. Это выглядит так:

#include <stdio.h>
#include <Windows.h>
#include <stdint.h>

int main(int argc, char* argv[])
{
    uint32_t number = atoi(argv[1]); // Здесь никаких проблем

    // Открываем файл с кодом
    HANDLE binFile = CreateFileA(
                        "isEven.bin",
                        GENERIC_READ | GENERIC_EXECUTE, FILE_SHARE_READ,
                        NULL,
                        OPEN_EXISTING,
                        FILE_ATTRIBUTE_NORMAL,
                        NULL);
   
    // Получаем 64-битный размер файла
    LARGE_INTEGER codeSize;
    GetFileSizeEx(binFile, &codeSize);

    // Создаём отображение файла в памяти
    HANDLE mapping = CreateFileMapping(
                        binFile,
                        NULL,
                        PAGE_EXECUTE_READ,
                        0,
                        0,
                        NULL);

    // Получаем указатель на код
    LPVOID code = MapViewOfFile(
                    mapping,FILE_MAP_EXECUTE | FILE_MAP_READ,
                    0,
                    0,
                    codeSize.QuadPart);

    // Создаём функцию, указывающую на код
    int (*isEven)(int) = (int(*)(int))code;

    if (isEven(number))
        printf("even\n");
    else
        printf("odd\n");

    CloseHandle(binFile);
}

Вот и всё! Теперь у нас есть всё необходимое для проверки чётности любого 32-битного числа. Давайте проверим:

PS >.\program.exe 300
even
PS >.\program.exe 0
even
PS >.\program.exe 1000000
even
PS >.\program.exe 100000007
odd
PS >.\program.exe 400000000
even
PS >.\program.exe 400000001
odd
PS >.\program.exe 400000006
even
PS >.\program.exe 4200000000
odd <---- ОШИБКА!

Почти! Похоже, у алгоритма есть какие-то проблемы со знаками: любое значение более 2^31 выдаёт произвольные результаты. Печально!

Давайте устраним последний баг.

Оказалось, функция atoi не может работать с чистой беззнаковостью, поэтому ей не удаётся распарсить наши крупные числа. Всё это можно исправить заменой её на strtoul.

uint32_t number = strtoul(argv[1], NULL, 10);// Здесь никаких проблем
PS >.\program.exe 4200000000
even
PS >.\program.exe 4200000001
odd

Стоит также отметить, что программа на удивление производительна. Для небольших чисел результат возвращается мгновенно, а для больших чисел, близких к 2^32, результат всё равно возвращается примерно за 10 секунд. Учитывая, что компьютеру приходится считывать 40 ГБ с диска, отображать их на физическую память, а затем обрабатывать их CPU без особых шансов на кэширование, это потрясающий результат. Для справки — в моём компьютере установлен Core i5 12600K с 32 ГБ памяти, а файлы хранятся на SSD-диске M.2. При вычислениях я наблюдал пиковую скорость чтения с SSD примерно в 800 МБ/с (что совершенно нелогично, ведь при этом скорость исполнения была бы больше сорока секунд, но компьютеры — это магия, так что кто знает, что там происходит).

И на этом мы закончили! Мы снова доказали, что в Интернете кто-то не прав — не только можно написать полнофункциональную и высокопроизводительную программу в стиле поста в TikTok, но это ещё и очень захватывающе.

Комментарии (140)


  1. lgorSL
    28.12.2023 09:57
    +5

    ОС могла закешировать часть файла и не читать каждый раз (например, первые 10 Гб), во вторых ssd на m.2 обычно заметно быстрее (на уровне 3 или даже 7 гигабайт в секунду на чтение).

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


    1. falcon4fun
      28.12.2023 09:57
      +2

      Именно. Про файл маппинг в память видимо кто-то не слышал. Ох уж этот мир девопсеров.


      1. atd
        28.12.2023 09:57
        +1

        Чтобы что-то замапить в память, надо это прочитать с диска, магическим образом байты в рам не залетают. При размере файла 40гб, рам 32гб и линейном чтении файловый кэш поучаствовать не сможет (даже при всём желании, ну разве что перед измеряющим запуском несколько раз запустить программу и нажать Ctrl+C до того, как она завершится, если подгадать момент, тогда можно успеть оставить бОльшую часть файла в кэше фс).


        1. rexen
          28.12.2023 09:57

          Погодите, маппинг в память и кэширование - это не одно и то же. Первое - это лишь таблица трансляции виртуальных адресов, доступных программе в реальные, доступные ОС.


          1. atd
            28.12.2023 09:57

            А где я заявлял, что это одно и то же?

            Профит от маппинга будет только если файл уже где-то лежит в памяти (например, в кэше ФС), если его там нет, то данные придётся читать с диска.


        1. kh0
          28.12.2023 09:57

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


    1. echo10
      28.12.2023 09:57
      +2

      добавьте еще ELSE к IFам, пожалуйста, а то получается какой-то брут-форс )))


      1. saboteur_kiev
        28.12.2023 09:57

        а что изменится?


        1. boldape
          28.12.2023 09:57
          +1

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

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


          1. vldF
            28.12.2023 09:57
            -1

            О, заодно и константную ассимтотику можно получить так!


  1. red75prim
    28.12.2023 09:57
    +18

    И ещё это демонстрация того, как легко получить undefined behavior в программе на C. Если запустить программу без параметров, то значение argv[1] - NULL, а поведение atoi c параметром NULL не определено в стандарте. Если argc равно 0 (впрочем, под Windows этого случится не может), то значение argv[1] не определено.


    1. MinimumLaw
      28.12.2023 09:57
      +1

      Даешь повторение эксперимента на Rust! Я бы посмотрел... на итоговую разницу. И результа запуска без параметров.


      1. red75prim
        28.12.2023 09:57
        +7

        Потенциальные ошибки придётся обрабатывать каким-то образом. Что-то вроде

        let number: u32 =
            args().nth(1)
            .expect("The program needs an argument")
            .parse()
            .expect("The first argument should be a positive number");
        

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

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


        1. MinimumLaw
          28.12.2023 09:57
          +1

          Не пойдет, ибо

          if (argc < 2) {
            printf("Need argument!\n");
            return -1;
          }

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


          1. red75prim
            28.12.2023 09:57
            +4

            Чудес не бывает. Undefined behavior нужно как-то определить, иначе он останется undefined. Раст доопределяет работу с аргументами программы как: "args().nth(n) возвращает None, если аргумента n не существует". Что с этим делать дальше зависит от программиста. Можно даже вернуть UB, используя unreachable_unchecked() в ветке обрабатывающей отсутствие аргумента.


            1. MinimumLaw
              28.12.2023 09:57

              Т.е. вопрос в целом не к atoi(), а к программисту, который не проверил ввод от пользователя и в целом это не зависит от языка? Я правильно понял?

              А что произойдет с вашим кодом, если мы запустим

              $ ./prog fortytwo

              Вводим дополнительную проверку? Или все же "мусор на входе - мусор на выходе"?


              1. Cerberuser
                28.12.2023 09:57
                +6

                Дополнительная проверка в комментарии выше уже есть - parse вернёт ошибку, если входная строка не будет представлять значение требуемого типа (здесь - u32). Для этого, собственно, и нужен второй expect.


                1. MinimumLaw
                  28.12.2023 09:57

                  Т.е. .pase() ведет себя не как atoi(), а как scanf(). Впрочем, при использовании scanf() в C можно было и argv[1] на NULL не проверять...

                  В целом очень странные чувства от анализа совершенно простого кода. С одной стороны да - при нештатном использовании (запуск без аргумента или с неподобающим аргументом) - и даже откровенно некрасиво написанная программа падает в корку. Чем явно и недвухсмысленно говорит программисту - ты не прав, надо исправляться. С другой стороны... Падение программы, вызывающее DoS - несколько не то поведение, которого ожидает заказчик. Тем более, что для такого поведения надо "нарушить правила" - т.е. создать специально условия, приводящие к ошибке. А война меча и щита - она вечная.

                  Да, пожалуй, DoS в большинстве случаев лучше, чем RCE. Но нужно быть гурманом...


      1. CodeRush
        28.12.2023 09:57
        +3

        В результате запуска без параметров там будет что-нибудь вроде thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', т.е. ничего интересного.

        Если же повторять эксперимент на Rust, то лучше засунуть все в здоровенный блок match и посмотреть, прожует компилятор 4 миллиарда записей в нем, или тоже решит, что это не для него.


        1. MinimumLaw
          28.12.2023 09:57

          Точно так. И код возврата не нулевой (надеюсь).


          1. CodeRush
            28.12.2023 09:57
            +1

            Не нулевой, а 101.


        1. aamonster
          28.12.2023 09:57

          Match не годится, он может быть соптимизирован и вместо полного перебора получите O(log(N)).


  1. Javian
    28.12.2023 09:57
    +31

    Хорошая статья для пятницы.


  1. Shura_m
    28.12.2023 09:57
    +36

    По моему, автор доказал только одну вещь:

    -можно реализовать любую фигню, любым образом, особенно если ты не ограничен ресурсами.


    1. NickyX3
      28.12.2023 09:57
      +68

      ru_perl 90-х
      – Парни, а можно на Perl зачитать тектовый файл в 30 миллионов строк?
      – А че за железо?
      – Sun StarFire, 32 CPU, 196 GB RAM.
      – ТЕБЕ - МОЖНО!


    1. Didimus
      28.12.2023 09:57
      -17

      Советским программистам платили за количество строк кода.


      1. terthon
        28.12.2023 09:57
        +6

        я такое слышал только про индусов


      1. rombell
        28.12.2023 09:57
        +1

        такого не было


      1. a-cherepanov
        28.12.2023 09:57

        • Ложь

        • Да нет, "Клади"

        • Ты что, в русской школе учился?

        Советским программистам платили оклад.


    1. falcon4fun
      28.12.2023 09:57
      +1

      Именно так подумали про оптимизацию в иммортал оф авеум и прочих проектах этого года (:


  1. Ivan22
    28.12.2023 09:57
    +20

    я думал статья будет про то как нейросети работают....


    1. qfox
      28.12.2023 09:57
      +4

      Нейросеть писала статью!


      1. nochkin
        28.12.2023 09:57

        Нейросеть сделала пост в ТикТоке, а потом просто тихо хихикала над глупыми людишками в своём нейродоме на берегу нейроморя.


  1. panzerfaust
    28.12.2023 09:57
    +44

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


  1. WFF
    28.12.2023 09:57
    +43

    Все равно надо писать на Питоне: из полученного в качестве параметра числа он должен сделать программу на C, скомпилировать ее, запустить и вернуть результат.

    Но глобально хотелось бы то же самое, но на микросервисах..


    1. Conung_ViC
      28.12.2023 09:57
      +40

      для каждого числа - свой микросервис?


      1. aamonster
        28.12.2023 09:57
        +5

        Причём на Erlang, с соблюдением принципа "let it crash".


      1. gun_dose
        28.12.2023 09:57
        +12

        Именно, неизвестно только что потом делать с 32-битным счётом от Amazon


        1. halfworld
          28.12.2023 09:57
          +9

          Проверить на четность?


    1. edogs
      28.12.2023 09:57
      +9

      Ага, и полное покрытие тестами.


    1. antonguzun
      28.12.2023 09:57
      +5

      Не забыть про слоеную архитектуру


      1. Didimus
        28.12.2023 09:57
        +5

        И георезервирование


        1. Boilerplate
          28.12.2023 09:57

          И желательно потом оптимизировать углеродный след


  1. TEMN1J
    28.12.2023 09:57

    Было бы интересно посмотреть сравнение производительности. ИМХО куча операторов сравнения медленнее чем операция остатка от деления. Однако, если сделать некую таблицу (число-чётное или нет) и маппинг оффсета на каждое число, и хранить этот файл в RAM, то уже возможно, что будет быстрее. Но опять же, это все надо проверять.


    1. navferty
      28.12.2023 09:57
      +38

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


      1. Alexey2005
        28.12.2023 09:57
        -3

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

        1. Прочитать аргумент, представленный в виде строки.

        2. Зачем-то сконвертить эту строку в число.

        3. Получить бит от этого числа

        4. В зависимости от бита осуществить переход.

        Но ведь можно же никуда строку не конвертировать, а просто взять из неё последнюю цифру и проверять только её символьный код. Там и if'ов будет меньше, и переход можно вообще таблицей переходов (или switch) сделать.


        1. code07734
          28.12.2023 09:57
          +34

          А можно таки взять младший бит последней цифры) Расположение символов цифр в ascii соответствует четности самих чисел


        1. edogs
          28.12.2023 09:57
          +16

          Вы на полном серьезе рассуждаете как можно оптимизировать код из статьи?:)


          1. aamonster
            28.12.2023 09:57
            +9

            Ну да, а что? Я считаю, что нужна пачка функций типа isEqualTo0, isEqualTo1, isEqualTo2 и так далее, которые проверяют (тоже перебором) равенство конкретному числу, а уже потом проверять пачкой вызовов типа if(isEqualTo3(number)) printf("odd\n"), так хотя бы O(N^2) будет, а не жалкое O(N).


            1. yokotoka
              28.12.2023 09:57
              +3

              Перебором неспортивно. Нужна рекурсия


              1. aamonster
                28.12.2023 09:57

                Ну можно. Каждый из вызовов isEqualToX вызывает все остальные isEqualToY, чтобы убедиться, что каждый из них вернёт false. А чтобы избежать зацикливания – для каждого храним флаг, что он уже вызван. Правда, это будет не thread-safe, но, сдаётся мне, это наименьшая из проблем.


                1. Boilerplate
                  28.12.2023 09:57

                  isEqual(num) {
                  if(num ==0)

                  return "odd"
                  if (num == 1)
                  return "even";

                  return isEqual(num - 1);
                  }


                  1. mrqak
                    28.12.2023 09:57

                    Ответом всегда будет even, если num > 0

                    fix:

                    return isEqual(num - 2);

                    А еще odd и even местами перепутаны)


                    1. Boilerplate
                      28.12.2023 09:57

                      Ну тестировщиков тоже нельзя без работы оставлять. А если в исходном варианте с ифами добиваться 100% покрытия, это же сколько тестов-то будет!


  1. mayorovp
    28.12.2023 09:57
    +18

    1. denis-19
      28.12.2023 09:57
      +1

      Автор выбрал другие хабы, там 5 ограничение, а так да может подойти.


      1. mayorovp
        28.12.2023 09:57
        +2

        Он не просто "может подойти", он подходит для этого поста больше всего.


    1. Didimus
      28.12.2023 09:57
      +13

      Ещё здоровье подойдёт


  1. edogs
    28.12.2023 09:57
    +6

    if (number == 5)

    printf("odd\n");

    4 миллиарда сравнений (каждое на 2 строчках), вот что бывает когда за код платят построчно:)


  1. Myclass
    28.12.2023 09:57
    +7

    Забавно. А можно кластер из 2^32 нодов создать, чтобы каждый за свою цифру отвечал и ваш код как функцию распределения взять и ву-а-ля - можно поток цифр в реальном времени обрабатывать. :)


    1. CrazyElf
      28.12.2023 09:57
      +8

      Кажется, вы заново открыли map->reduce ))


  1. pythonist1234
    28.12.2023 09:57
    +13

    Это не троллинг 80 лвла. Это троллинг 40 ГБ лвла)))


    1. Didimus
      28.12.2023 09:57
      +5

      В следующей версии виндоус калькулятор будет больше


  1. LF69ssop
    28.12.2023 09:57
    -3

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

    Хорошая попытка, но нет. Автор кода просто не понимает что он делает.


  1. bear11
    28.12.2023 09:57
    +5

    а gcc -O3 это во сколько раз сожмет?


    1. blind_oracle
      28.12.2023 09:57
      +2

      Просто превратит это в `if i % 2 ...` :)


      1. cb_ein
        28.12.2023 09:57

        Хм, а если после N миллиардов if будет else { printf("Unknown number\n"); } ?


    1. TheAthlete
      28.12.2023 09:57

      42


  1. iliasam
    28.12.2023 09:57
    +3

    Вот это сразу вспомнил: https://github.com/AceLewis/my_first_calculator.py


    1. 8street
      28.12.2023 09:57
      -1

      Если это кажется тупым и работает, то это не тупо. (с)


      1. kAIST
        28.12.2023 09:57

        Дурацкая поговорка, наравне с "работает - не трогай" ;)


  1. Fil
    28.12.2023 09:57
    +65

    Куча if-ов - это уровень мидла, не выше. Сеньерское же решение будет таким:

    bool is_even(int n) {
        return n == 0 ? true : is_odd(n - 1);
    }
    
    bool is_odd(int n) {
        return n == 0 ? false : is_even(n - 1);
    }
    


    1. Mayurifag
      28.12.2023 09:57
      +16

      Стало быть, в техлидском решении ещё и n == 0 в отдельную функцию вынесется, чтобы соблюдался DRY? :)


    1. VADemon
      28.12.2023 09:57
      +3

      У сеньера стек переполняется, тогда как у мидла нерекурсивное решение и лучше масштабируется (покуда хватит памяти и времени).


      1. vadimr
        28.12.2023 09:57
        +5

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


        1. gev
          28.12.2023 09:57
          -4

          Только ее тут нет =)


          1. zagayevskiy
            28.12.2023 09:57

            есть


    1. Osnovjansky
      28.12.2023 09:57
      +11

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


      1. IkaR49
        28.12.2023 09:57
        +4

        Ну и отлично! Устраняем будущих конкурентов ;)


  1. zzzzzzzzzzzz
    28.12.2023 09:57
    +3

    изучив ограничения формата Portable Executable (.exe) для Windows, я обнаружил, что он не может обрабатывать больше, чем жалкие 4 ГБ

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

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

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


    1. nronnie
      28.12.2023 09:57
      +3

      "Разделяй и "властвуй" это будет:

      public static bool IsEven(int n) => n switch {
          < 0 => IsEven(-n),
          0 => true,
          1 => false,
          _ => IsEven(n / 2) && IsEven(n - n /2) || IsOdd(n / 2) && IsOdd(n - n / 2)
      }
      
      public static bool IsOdd(int n) => !IsEven(n);
      


    1. DrGluck07
      28.12.2023 09:57

      Тоже сразу подумал про несколько DLL. Или можно вообще ничего не генерировать заранее, а создавать код в рантайме для диапазонов по миллиону чисел, например. Тогда exe-шник будет маленький, и даже не очень много памяти попросит.

      Upd: Кстати, это позволит не ограничивать себя 32-битами.


    1. UGivi
      28.12.2023 09:57

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


  1. viordash
    28.12.2023 09:57
    +4

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


    1. ProMix
      28.12.2023 09:57
      +3

      Требую бинарный поиск на if-else!


      1. DBalashov
        28.12.2023 09:57

        в switch/case он и так может соптимизироваться в бинарный поиск.


  1. Dredlock
    28.12.2023 09:57

    А зачем писать столько много?

    Ведь можно отбросить все цифры, кроме последней. И работать с ней. Всего 10 if.

    А можно и двумя обойтись

    if(n==1||3||5.... И тд


    1. jaqjaq
      28.12.2023 09:57
      +1

      В вашем примере разве 3 и 5 не будут всегда давать True?

      может тогда:

      if(n==1 || n==3 || n==5)


      1. Dredlock
        28.12.2023 09:57

        Да, вы правы. Спасибо что поправили. Я писал в виде псевдокода, в общем вижу что идею мою поняли


  1. krabdb
    28.12.2023 09:57
    -2

    А когда русскоязычное написание фамили Денниса как «Ритчи» сменили на «Ричи»? А Кернигана тоже на кого-то поменяли?


    1. artemisia_borealis
      28.12.2023 09:57
      +1

      Да, Росс ван дер Гуссом предложил несколько вариантов.


  1. glebe
    28.12.2023 09:57
    -2

    А разве недостаточно было проверять только последнюю цифру числа. Десяток ифов всего.


    1. nochkin
      28.12.2023 09:57
      +3

      С таким успехом мы можем скатиться до позорной проверки младшего бита. Это не наш стиль. Мы должны дать отпор возрастающей мощности современных вычислительных систем.


  1. Moog_Prodigy
    28.12.2023 09:57
    +2

    Надо было просто на CUDA все переложить. Я думаю, последние ускорители типа N100 должны справиться с подсчетом четных чисел. /s . Далее нам понадобится суперкомпьютер из топ100 для подсчета числа счастливых билетов.

    Ну честно, какая то машина Голдберга получается с этими подходами)


  1. belch84
    28.12.2023 09:57
    -1

    Вот, с помощью BING'а набросал нечто подобное на SQL

    Код для определения четности/нечетности числа на TRANSACT SQL

    -- Create a stored procedure named CheckEven with one int parameter
    CREATE PROCEDURE CheckEven @num int
    AS
    BEGIN
    -- Declare a variable to store the result of the query
    DECLARE @result int, @numint ;

    -- Create a table named EvenNumbers with one field of int type
    CREATE TABLE EvenNumbers (
    Number int
    );

    -- Set a variable to store the current number
    SET @num = 2;

    -- Use a while loop to insert numbers from 2 to 32766 into the table
    WHILE @num <= 32766
    BEGIN
    -- Insert the current number into the table
    INSERT INTO EvenNumbers (Number) VALUES );

    -- Increment the current number by 2
    SET @num = @num + 2;
    END

    -- Select the count of the number from the EvenNumbers table
    SELECT @result = COUNT(Number) FROM EvenNumbers
    -- Use the parameter as the filter condition
    WHERE Number = @num;

    -- If the result is greater than zero, the number exists in the table
    IF @result > 0
    BEGIN
    -- Print 'even' as the output
    PRINT 'even';
    END
    -- Else, the number does not exist in the table
    ELSE
    BEGIN
    -- Print 'odd' as the output
    PRINT 'odd';
    END
    END

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


    1. nronnie
      28.12.2023 09:57

      WITH
          OddOrEven(Num, IsEven)
          AS
          (
              SELECT CAST(0 AS int) Num, CAST(1 AS bit) IsEven
              UNION ALL
              SELECT
                  oev.Num + 1 Num,
                  CASE
                      WHEN oev.IsEven = 0 THEN CAST(1 AS bit)
                      ELSE Cast(0 AS bit)
                  END IsEven
              FROM OddOrEven oev
              WHERE oev.Num < POWER(CAST(2 AS bigint), 31) - 1
          )
      SELECT IsEven
      FROM OddOrEven
      WHERE Num = @num;
      


  1. iboltaev
    28.12.2023 09:57
    -3

    Я понимаю, что это чисто поржать, но хоть бы массив забил что ли битовый статический, чтоб за о(1), или каскадировал через <, чтоб хотяб за логарифм...


  1. debagger
    28.12.2023 09:57
    +4

    Тут уже предлагали выше решить такую задачу на микросервисах. Я предлагаю пойти дальше. Чтобы по-настоящему хайпануть на трендах, надо взять какую-нибудь LLM архитектуру где-нибудь на 40B параметров и обучить исключительно на эту задачу. Считаю что это будет гораздо эпичнее.


    1. Melirius
      28.12.2023 09:57
      +5

      И получить после обучения на пятизначное число долларов точность ответа более 90%!


    1. bolk
      28.12.2023 09:57
      +2

      Думаю, нужно сделать клиентское приложение, которое надо качать и ставить себе. Установившие такие приложения становятся нодами единой сети. Нода будет брать задачи из единого реестра, вычислять чётность/нечётность и отдавать результат в сеть. Задачу должны взять себе несколько нод, так чтобы один из ответов (чёт или нечет) был больше 50% мощности сети.


      1. debagger
        28.12.2023 09:57
        +1

        С таким подходом можно уже и на 64 бит замахнуться


  1. prinv
    28.12.2023 09:57
    -18

    Хабр неостановимо пробивает днище за днищем


    1. DrGluck07
      28.12.2023 09:57
      +3

      А можно няню, плиз? В чём конкретно заключается пробивание дна в данном случае?


      1. nronnie
        28.12.2023 09:57
        +1

        Не забивайте голову, это у некоторых чувство юмора пробивает дно :)


    1. kAIST
      28.12.2023 09:57
      +1

      Редкая статья про программирование, пусть и ненормальное. Это хуже чем все остальное?


  1. Error1024
    28.12.2023 09:57

    Я сторонник высокопроизводительного кода, поэтому решил реализовать это на языке программирования C, потому что он и сегодня остаётся самым быстрым языком в мире с большим отрывом от других (благодаря гению Денниса Ричи).

    Мне кажется, или тут полное не понимание того, что секрет скорости не в некой «гениальности» си, а в миллиардах, вбуханных, в конкретные компиляторы? Странно такое видеть от человека, заявляющего что он «сторонник высокопроизводительного кода». Впрочем, современные последователи карго культа «блейзинг фаст» +/- все что-то подобное заявляют.


    1. DimaFromMai
      28.12.2023 09:57

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


    1. mayorovp
      28.12.2023 09:57
      +3

      Если бы автор написал как-то по-другому, ему не удалось бы написать шутку про Python ниже по тексту. А зачем вы пытаетесь искать глубокое понимание чего бы то ни было в откровенно стёбной статье?


    1. Kelbon
      28.12.2023 09:57

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


  1. alexeyinkin
    28.12.2023 09:57

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

    Не факт.


  1. Mdm3
    28.12.2023 09:57
    +1

    Надеюсь эта статья не попадет в обучение следующих версий gpt


    1. lamer84
      28.12.2023 09:57

      Так наоборот, пускай попадает! Меньше конкуренция будет!


    1. KvanTTT
      28.12.2023 09:57

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


      1. nochkin
        28.12.2023 09:57

        Его восприятие будет сильно зависеть от комментов.


  1. Panzerschrek
    28.12.2023 09:57

    https://godbolt.org/z/7qE9863j4
    Clang с O2 оптимизирует цепочку сравнений до выборки указателя из таблицы с последующим вызовом puts. Другие компиляторы не осиливают оригинальный код и оставляют кучу инструкций ветвления.


  1. AleksejMsk
    28.12.2023 09:57
    +1

    На C# я могу хоть триллион IF сделать.
    Шаг №1 - Генерирую DLL на N штук IF - загружаю в память и использую
    ...
    Шfг №M - Генерирую DLL на N штук IF - загружаю в память и использую

    В итоге N * F операторов IF.

    Вуаля !


  1. Iridar
    28.12.2023 09:57
    -8

    Зарегался чтобы поставить лайк.


  1. Nikolyanich
    28.12.2023 09:57
    -6

    А не проще проверять последний бит числа

    Если 1 четное иначе нечётное?

    Всего 1 строка...


    1. DrGluck07
      28.12.2023 09:57
      +3

      Это не путь самурая.


    1. zagayevskiy
      28.12.2023 09:57
      +9


  1. Serdjuk
    28.12.2023 09:57
    -7

    Простите, это что за наркомания ?
    Value And 1 для слабаков ?
    Проверка нулевого бита, алле...


    1. DrGluck07
      28.12.2023 09:57
      +4

      Этот комментарий пост-мета-ирония? Правда же?


      1. Serdjuk
        28.12.2023 09:57

        Нет


        1. starik-2005
          28.12.2023 09:57

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

          И да, программа бесполезная, но рабочая. Неужели это приходится объяснять?


    1. Wesha
      28.12.2023 09:57
      +12

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


  1. z3apa3a
    28.12.2023 09:57

    Тема с оптимизацией алгоритмов не раскрыта КМК, можно было бы на питоне сгенерировать дерево if'ов. Код конечно слегка вырастет, зато какой прирост производительности!


  1. Maccimo
    28.12.2023 09:57

    Не совсем корректный ассемблерный код

    Судя по подсветке синтаксиса, это вообще код на 1С ;)
    Считаю, что о читаемости кода забывать нельзя и потому вместо INC EAX обязательно нужно было использовать SETE EAX.

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

    Досовские com-файлы восстают из пепла под 64-битной виндой, это ли не новогоднее чудо???

    Автор выбрал другие хабы, там 5 ограничение, а так да может подойти.

    @denis-19 «Ненормального программирования» здесь всё же больше, чем «алгоритмов».

    P.S. При переводе потерялась часть шуток-прибауток из оригинала, читайте оригиналы!


  1. akaluth
    28.12.2023 09:57

    Прошу рассмотреть возможность оформить это как npm-пакет через ffi, постоянно возникает такая задача


  1. yri066
    28.12.2023 09:57
    +2

    Когда учился универе, было групповое задние на семестр заниматься разработкой 16-бит ОС (не обязательно чтобы была полностью рабочей), и у меня было одно из заданий: реализовать динамическое распределение памяти malloc. При работе нужно было для теста выводить в консоль распределение памяти, но при этом была проблема, что функция для печати числа использовала память для перевода его в строку при печати. Для того чтобы тестировать память и печатать числа без выделения памяти, пришлось сгенерировать файл на 40к строк:

    if(num == 123) {kprint("123");};


  1. x67
    28.12.2023 09:57
    +1

    Ну, нет предела совершенству, можно же через if-else реализовать бинарные деревья, что сохранит нам один условный оператор и сильно увеличит скорость выполнения программы


  1. KvanTTT
    28.12.2023 09:57

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


  1. kh0
    28.12.2023 09:57
    +2

    Шикарная статья, получил море удовольствия, жду следующую:
    "жервуем дисковым пространством ради скорости проверки на четность", где автор забьет ССД файлами с именами четных чисел и потом будет проверять четность наличием файла на диске.
    Можно значительно уменьшить пенальти времени поиска файла, если хранить файлы не в виде папки с файлами, а в виде дерева подпапок с файлами, чтобы в каждой папке было не больше. Это был путь слабых.
    А путь сильных это создать 4Гб файл на диске из 1 и 0, и при провере сикать по параметру сразу на позицию чтения. Смех-смехом, но подобная парадигма скорей всего это будет самой быстрой проверкой чисел "на простоватость".)


    1. Boggard
      28.12.2023 09:57

      это уже напоминает попытку хранить настройки файлового менеджера в оракл бд.


  1. AtmosferaVA
    28.12.2023 09:57

    Очень любопытная тема с написанием огрызка компилятора Си/Ассемблера на Питоне который собирает исходники прямо в код процессора!

    Существуют ли примеры компиляторов прямо в машинный код написанных на Питоне?


  1. nronnie
    28.12.2023 09:57
    +1

    Еще вариант (C#) - для int тут не получается (из-за встроенного лимита на длину массивов), но для short вполне:

    using System.Linq.Expressions;
    
    var param = Expression.Parameter(typeof(short));
    
    var trueFalse = new[] {
        Expression.Constant(true),
        Expression.Constant(false)
    };
    
    var body = Expression.Switch(
        param,
        defaultBody: trueFalse[1],
        Enumerable.Range(0, short.MaxValue + 1)
            .Select(i =>
                Expression.SwitchCase(
                    body: trueFalse[i % 2],
                    Expression.Constant((short)i)))
            .ToArray());
    
    var isEven = Expression.Lambda<Func<short, bool>>(body, param).Compile();
    
    for (short i = 0; i <= 42; i++)
    {
        Console.WriteLine($"{i}: {isEven(i)}");
    }
    


    1. maxcat
      28.12.2023 09:57

      можно создать массив с длиной расзмером с long или же int64, но конечно не через Enumerable.Range(..)


      1. nronnie
        28.12.2023 09:57
        +1

        Не, по-моему таки нельзя. Для одномерных массивов максимальное число элементов ограничено значением свойства Array.MaxLength - и, во-первых, оно уже объявлено как int и больше чем int.MaxValue быть не может, во-вторых, если покопаться в исходниках, то выясняется, что там оно захардкожено как 0X7FFFFFC7, что даже немного меньше чем int.MaxValue. Для многомерных массивов теоретически методы класса Array поддерживают размеры в long, но, на деле (опять-таки, если посмотреть в исходники) этот long там везде приводится к int, и если он в него не влезает, то мы сразу получаем ArgumentOutOfRangeException, и, даже более того, если он помещается в int, но не помещается в Array.MaxLength то тогда получаем OutOfMemoryException с сообщением "Array dimensions exceeded supported range."


  1. EDIsaev
    28.12.2023 09:57

    Не уж то чудооптимизатор сей не соптимизировал мильён ифов? Не пробовали? Просто интересно... Он, порой, такие оптимизации лепит, что суть всего алгоритма меняет)


  1. amberovsky
    28.12.2023 09:57

    Как насчёт использования оптимизированной функции в if-ах

    bool optimised_is_even(int number) {
      bool res = true;
      for (int i = 0; i < number; i++)
        res = !res;
    
      return res;
    }


  1. wasd_0
    28.12.2023 09:57

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

    Жду реализацию программы "FizzBuzz". Планируете ли выпускать книгу по вашим работам?