?

Log in

No account? Create an account
   Journal    Friends    Archive    Profile    Memories
 

Программирование, очерк об утилитах и их разработке, в историческом разрезе - morfizm


Oct. 21st, 2017 05:57 am Программирование, очерк об утилитах и их разработке, в историческом разрезе

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

С опытом я понял, что писать сотни тысяч строк утилит, игр, олимпиадных задач и прочего хлама - это компульсивное расстройство, сопровождающее не всех хороших программистов, а только некоторых. Бывают и такие, которые этого не делают, и вполне себе удачно вдаривают на работе, создавая полезный production code. Кроме того, на протяжении всей моей профессиональной карьеры я практически не занимался такими утилитами (разве что какие-то тулы, фреймфорки, прототипы и тесты на работе, но это не совсем то), но при этом очень по ним скучал. Всё надеялся, что когда-нибудь доберусь. Вот, руки дойдут, разберусь какие там новые технологии, чтобы было всё пучком: чтобы и быстро писалось, и быстро работало, чтобы и нативно выглядело, но при этом было портабельным, чтобы удобно куда-то выкладывать и делиться полезными апликухами. В общем, чтобы всё.

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

1. Всё-таки взрослый человек, нет времени. Есть работа, есть всякие хобби, есть жена, дети, мама и всякие ответственности. Годно? С натяжкой. Времени-то оно не так много отнимает.

2. Технологии рванули вперёд, а по работе я углубился в бэкэнд. Сначала в бэкэнд для юзеров, потом в бэкэнд для других разработчиков. Сейчас вот работаю в группе, которая делает движки/фреймворки для разработчиков бэкэнда, пользователями которого являются рабоработчики бэкэнда application layer'а, и где-то через несколько этажей от этого всего находятся пользователи, которые нормальные люди. В общем, я забурился максимально далеко от того, чтобы делать софт для людей. Утилиты это всё-таки софт для людей. Даже если это чисто для себя, там нужен какой-то гуй, или хотя бы удобный CLI, там нужно думать о юс-кейсах. Думать о том, чтобы такое написать, чтобы оно не протухло со временем, но при этом было полезным, и всё такое прочее.

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

4. Надвигаются мобильные девайсы, а это совсем другой технологический стек. Лениво и нет времени его учить. Есть одна лазейка: это web. Но web-приложения сильно ограничены, особенно их интеграция с desktop'ом.

5. Надвигаются cloud-сервисы и cloud-storage. Проблемы, скажем, с организацией файлов, как бы те же, но сражаться с ними нужно уже на другом поле битвы.

6. Ну и технологии... в старые добрые времена многие вещи можно было писать под DOS, даже если они для Винды. Под DOS это Borland Pascal, замечательный своей простотой и скоростью разработки. Встроенных библиотек настолько мало, что всё, что нужно, можно знать наизусть. Можно херачить код без help'а. Открываешь и пишешь. IDE запускается за секунду со всеми наворотами, окнами, отладчиком и всем чем хочешь. Компилируется всё за десятки миллисекунд, лишь самые сложные вещи требуют одной или даже пары секунд. Впрочем, модули можно откомпилировать отдельно и использовать TPU, тогда всё опять со свистом. За менее, чем секунду, можно сделать полностью самодостаточный standalone EXE-шник. Все эти удобства и скорости - это огромная стимуляция творчества, потому что мозг может быть 99% времени занят задачей, а не настройками build environment, поиском багов в dependencies, ожиданием билда и прочим геморроем. На смену Borland Pascal, пришёл Delphi и Visual Studio, которые хоть и затрудняли разработку (теперь проект это не один-два файла, а директория с поддиректориями, с кучей всякого перманентного и промежуточного говна), но всё-таки после изначальной настройки работали быстро. Инкрементальные улучшения в софт можно было вносить за секунды и всё перебилживать.

А вот дальше - всё это умерло. Заниматься софтом для людей стало неинтересно. Дотнет был немеренной тормознёй, кроме того, требовавшей установки рантайма. Невозможно было скопировать куда-то тулу, нужно было делать к ней инсталлер или давать инструкции, вот, установи фреймворк такой-то версии. Нет, вот эту оставь тоже. А вот эту снеси. Ну не ужас ли? Visual Studio как бы разделился на два направления - .NET-овое пошло по стопам Дельфей, где "всё в одном" и визуальный редактор форм, но при этом без 3rd-party копмонент, т.е. всё глубоко завязано на M$-овские технологии. C++-е направление VS не развилось вообще никуда, потому что слил позиции под напором библиотек. Зачем делать что-то своё, если есть QT, который лучше прямого GDI программинга?

Вот, кстати, про библиотеки... это ещё один интересный момент истории. Программерское сообщество заболело вирусом "DRY". Есть такой принцип, don't repeat yourself. Это очень важный принцип, без которого можно с лёгкостью писать несопровождаемый говнокод. Но религия преклонения этому принципу перетянула в другую сторону: повторяться вот настолько прям нельзя, что каждая маленькая хрень это отдельная библиотека, и теперь внутри отдельно взятой библиотеки царит красота и порядок, а любой проект состоит из говнокучи хитросплетений зависимостей между сотнями этих библиотек. Мне временами кажется, что программистам теперь платят не за способность написать хороший код, а за способность не утонуть в этом болоте из мини-модулей, каждый из которых живёт какой-то своей жизнью. С каждым апдейтом он ломает сотню соседей, и главное, что любая система, собирающее все эти сопли воедино, обязательно требует, чтобы в конечном соплепродукте каждая сопля входила ровно по одному разу, одной своей версией (ну, DRY же!). Сопли ломаются не зависимо друг от друга, continuous deployment'ы и прочие продвинутые дев-процессы ускоряют скорость выкатывания новых багов, баги требуют апгрейда соплей, апгрейд одной сопли требует заапрейдить два десятка других (новая сопля уже не совместима с прежними версиями, вы чо, уже ж целый месяц прошёл! innovation, ёпть!), когда апгрейдишь эти другие сопли, соответственно, подтаскиваешь их свежие баги... в конце концов, понимаешь, что все эти идеи о компонентности яйца выеденного не стоят, копируешь сырцы библиотеки в поддиректорию своего проекта, чинишь там ровно те баги, про которые ты знаешь, и, наконец, можешь пописать свой код, пока вокруг ничего снова не сломалось. Кстати, самые ярые приверженцы принципа DRY это, как правило, fresh grad'ы. Они ещё не понимают, что такое инкапсуляция и компонентизация, но уже понимают, что нельзя повторяться. Поэтому там где есть хоть какое-то повторение, у них 10 уровней абстракции, чтобы не дай Б. ни строчка не повторилась, а там где повторения нет, у них функции на 300 строк по вертикали и 150 позиций по горизонтали. Можно же разбить на логические задачи, но "разделение обязанностей" это видимо, более продвинутый design pattern, чем DRY, до него не все доходят головой.

К сожалению, DRY-мания победила, и к сегодняшнему дню полсотни библиотек для того, чтобы напечатать Hello, World! это уже в порядке вещей. Единственное, что радует, это то, что скорость интернета и объёмы дисков растут немножко быстрее, чем способность community выносить полезные повторяющиеся куски кода в отдельные сопле-пакеты, и сегодня с этим уже можно жить.

Тут недавно был хороший вброс в useless_faq на тему почему современный софт такой тормозной и жирный, когда делает, в принципе, то же самое:
https://useless-faq.livejournal.com/15477816.html
Полезные библиотеки это оно.

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

Одновременно с этим, разные технологии, которые я лично считал тормознёй, перестали ею быть. Отчасти потому что железо стало побыстрее, а отчасти потому что их оптимизировали разработчики в процессе эволюции продукта. Для веба - JavaScript. Он стал очень достойным по скорости. Для не-веба - Java. Опять же, во многих задачах, и если умело пользоваться, порядок скоростей может быть такой же, как у C++. А разрабатывать на порядок быстрее и на полтора-два порядка дешевле. C++ видоизменился таким образом, что совмещает идею "язык способен максимально полно использовать возможности процессора" с примитивами функционального программирования и другими прогрессивными штуками. Сегодня уже никто не будет писать свой смартпоинтер, а 10-15 лет назад это было модно, потому что язык был не достаточно мощным.

Ладно, к чему же это я...
Я, вот, решил попробовать Java, или даже нет... Scala (!) в качестве подручного языка для утилит, посмотреть, могу ли я вернуться к этому благому делу, в плане которого у меня накопилось уже много интересных идей. Причём попробовать настроить всё под виндой, но, понятно, с заделом на портабельность.

Для начала я решил взять тривиальную утилиту из очень старых, которая у меня была и всё ещё работала, это CRC.exe. Даёшь ей имя одного файла, она его всасывает и выплёвывает стандартный CRC32 хэш. Полезная утилитка для сравнения больших файлов, проверки integrity, а также как составная часть более сложных задач: например, задача найти дубликаты файлов на диске может состоять из составления списка файлов, их размеров и хэшей, а потом уже по этим трём параметрам можно группировать и что-то делать с группами.

CRC.exe занимает 22 килобайта. Работает 75 миллисекунд на 14-MB файле (из которых 20 мс - overhead на запуск EXE-шника, а 55 мс - сама работа) и 9.5 секунд на 2.25-GB файле. Я помню, что он был написан на Дельфях, без ассемблерных вставок, но с хорошо оптимизированным кодом, я там вычислял полезную look-up таблицу. Я сравнивал с имеющимися на тот момент другими библиотеками и утилитами, и моя работала без заметного замедления. В чистом виде именно на посчитать CRC у меня ушло тогда пару дней трудов, но я помню, что игрался с этим неделю, потому что я решал родственные задачи. Например:
- какие 4 байта прописать в указанное место в файле, чтобы его CRC стал равен заданному,
- какие 4 байта прописать в указанное место в файле, чтобы его CRC стал равен этим 4 байтам,
- решил общую задачу - заданы позиции отдельных битов, которых не менее 32, определить, можно ли подкрутить их так, чтобы получить заданный CRC, и если да, то подкрутить. Если биты идут подряд, то они всегда линейно независимы, а если не подряд, то не всегда. Частный случай этой задачи - это подкрутить 8 байт таким образом, чтобы они образовывали ASCII hex-репрезентацию от CRC файла, но если честно, я не помню, был ли там всегда успешный результат... кажется, был,
- ещё помню задачу, как из CRC отдельных последовательных кусков быстро составить CRC всего куска. Там можно за O(log(n)) посчитать CRC от массива нулей такой же длины как первый кусок, и потом выполнить несколько O(1) операций, чтобы совместить. Эта задача даёт возможность, скажем, рекурсивно зачитать директорию, посчитать CRC отдельных файлов, не важно, в каком порядке, потом отсортировать, и быстро посчитать CRC от массива, как будто все эти файлы были склеены. Это позволяет быстро определить, идентичны ли две директории.
Но это лирическое отступление, просто чтобы продемонстрировать, что даже такие забитые задачи как подсчёт CRC могут быть весёлым развлечением на неделю1. Впрочем, даже если скомпилировать все эти навороты, вряд ли они бы сильно увеличили размер EXE-шника. Может, было бы не 22 килобайта, а 24.

CRC.exe, сделанный на Scala, занял 11 мегабайт. Работал 800 миллисекунд на 14-MB файле (из которых 785 мс - overhead на запуск EXE-шника, а 15 мс - сама работа) и 2.2 секунды на 2.25-GB файле (соответственно, 785 мс - overhead на запуск, и 1.4 секунды сама работа, в 6.7 раз быстрее!). Вся работа заняла 7 часов. Из них:

*) Один час - установка и настройка IntelliJ IDEA + Scala + JDK, чтобы оно вообще могло откомпилировать пустой проект и "Hello, World!".

*) *Одна минута* - решение задачи с CRC "в лоб" (засасываем весь файл в Array[Byte] и используем java.util.zip.CRC32), порядка 5 строк.

*) *Две минуты* - улучшенное решение (читаем файл блоками по 64K, обновляя бегущий хэш походу дела), порядка 10 строк. Но нужно было добавить параметры командной строки. Для этого нужен парсер. Для этого нужна библиотека. Я быстро понял, что нужен scopt, но как его заполучить?

*) Три часа - выбор способов, как закачивать библиотеки в IntelliJ, выбор между SBT и Maven в пользу SBT (выбирал, что проще настроить), много секса с SBT, чтобы заработало хоть что-нибудь. Ребята прикололись и в документации SBT написали, что, типа, если ты очень-очень спешишь, то прочти хотя бы вот эти три главы, типа, гайд для идиотов, базовые концепты. Но мы не гарантируем, что тебе этого хватит. Я попробовал прочесть базовые концепты для идиотов, понял, что я идиот утону и за сегодня не закончу, а читать весь гайд мне вообще влом, поэтому разбирался сам, как полагается, об коленку и с моими верными друзьями по имени StackOverflow и StackExchange.

*) Час - поиск библиотеки для logging-а, выбор между log4j и slf4j в пользу log4j (опять же, на халяву выбрал), много секса, чтобы всё заработало. Интернет замусорен советами для первой версии log4j, которая существенно отличается от второй (log4j2).

*) Полтора часа - поиск возможности сбилдить Jar, а также, самое главное - Jar Jar'ов. Т.е. упаковать все dependencies в одном Jar'е, чтобы получить самодостаточный пакет, который можно копировать на другие компьютеры. Если на компьютере должен быть Java runtime (JRE8), это ещё куда ни шло, но я-то уже использую кучу библиотек! И одна строчка логгинга (время засечь), и парсинг аргументов командной строки (имя файла и параметр -v чтобы печатать время). Это ж огого! :) В результате обильного секса, мне удалось настроить плагин sbt-assembly.

*) Полчаса - выбор инструмента для создания EXE-шника. Перебрал и опробовал три варианта (exe4j, nsis, launch4j), больше всего подошёл launch4j. Размер это почти не увеличило, что JAR 11MB, что EXE 11MB, но зато удобство, что не нужно отдельно иметь JAR, а отдельно CMD-файл для его запуска, теперь всё в одном. Время запуска одинаковое, что напрямую java.exe -jar путь-к-JAR, что через EXE wrapper.

Отдельно отмечу, что SBT скачал 200 MB вспомогательных библиотек в репозиторий (user-profile/.ivy2/cache/...), там 37 пакетов, всё это dependencies тех 4 пакетов, которые мне были нужны. Кстати, 5-й пришлось добавить, чтобы разрулить конфликт версий (разным соплям нужны разные версии неких одноимённых соплей, так что... выбираем соплю поновее и молимся, что она обратно совместима с соплёй на 3 минорных версии старше неё).
Кстати, а что внутри этого 11-мегабайтного JAR-а? Внутри него:
- 20 KB - откомпилированная версия тех 3 KB исходников, которые я написал,
- 165 KB - модуль scopt,
- 4466 KB - org.apache.logging.log4j - чтобы вывести одну строчку в лог, как видите, нужно тащить 5 MB говна,
- 22 MB - scala. Да, конечно, scala не входит в JRE, с точки зрения Java это просто ещё одна библиотека. Но если делать standalone утилиты, то в каждую из них придётся запихивать всю скАлу целиком. А вы что думали? Теперь понятно, почему софт стал таким большим и так неповоротливо запускается? На самом деле, JRE занимает ещё 180 MB, и часть его тоже подгружается при запуске любой Java программы, отсюда и эти 800 мс на запуск...

Мне кажется, этот пример как бы резюмирует, в какую сторону оно всё повернулось:
- Скорость разработки таки увеличилась. Было два дня (когда-то давно), стал один (сегодня). Библиотеки рулят.
- Скорость работы алгоритма тоже заметно увеличилась, не смотря на то, что язык интерпретируемый, а не компилируемый. Библиотеки дважды рулят!
- Скорость начального запуска уменьшилась до borderline-приемлемого. Впрочем, это улучшение, раньше то же самое заняло бы много секунд.
- Время на саму "соль" программы несоизмеримо (на два порядка!) меньше, чем время на геморрой с соплепакетами и с системой, которая призвана приструнить растекание соплей в адский кисель.

Попробую потом с этим поиграться ещё. Мне кажется, что есть шанс, что overhead будет постепенно уменьшаться, по мере накопления опыта и уже скаченных библиотек. Отдельно интересно, можно ли снизить время первого старта (800 мс) без ущерба удобства, что всё в одном файле? Подозреваю, что если писать на чистой джаве, без скалы, может быть лучше. С другой стороны, это ж я только две сторонние библиотеки подключил. А что будет, когда я их подключу два десятка?..

_____
1 кстати, если решить те же задачи для md5, то можно стать очень богатым или очень известным человеком.

57 comments - Leave a commentPrevious Entry Share Next Entry

Comments:

From:li111
Date:October 21st, 2017 01:16 pm (UTC)
(Link)
Сейчас ,как раз, и зарылась в библиотеках :(
From:morfizm
Date:October 21st, 2017 01:20 pm (UTC)
(Link)
Ну, по крайней мере, этот пост поможет тебе относиться к этому по-философски: на это, к сожалению, все тратят кучу времени. Легко будет, когда в проекте достаточно наработок так, что все нужные библиотеки уже используются в коде, который ты недавно писал, и, соответственно, тебе легко их привинтить к новому коду.
From:natpy
Date:October 21st, 2017 03:46 pm (UTC)
(Link)
Лучше было мавен выбрать имхо. Если ничего кроме подгрузки библиотек не нужно то очень быстро всё там
From:morfizm
Date:October 21st, 2017 10:19 pm (UTC)
(Link)
Спасибо, буду знать. В моём случае сейчас-то, наверное, уже всё равно? SBT, будучи настроенным, быстр и удобен. Добавление dependency это добавление 1 строки, после которой он полминуты колдует над выискиванием и скачиванием либы и её зависимостей, и она автоматом добавляется в проект.
From:rezkiy
Date:October 21st, 2017 07:22 pm (UTC)
(Link)
твоему кастомеру все депенденси засосутся пекидж менеджером, тогда и только тогда когда тебе надо. Так что не парься.
From:morfizm
Date:October 21st, 2017 10:21 pm (UTC)
(Link)
Будем ждать появления волшебных пекидж менеджеров, которые будут делать всё сами и незаметно. Пока это из разряда фантастики. Как минимум потому, что если я завишу от A и B, а они оба зависят от разных версий C, то мне нужно вручную разрулить dependency conflict.
From:andreyvo
Date:October 21st, 2017 08:59 pm (UTC)
(Link)
Proguard для уменьшения бинарника. Коцает лишние классы, методы, переменные, мертвый код, уцелевшее инлайнит, мерджит классы, обфускирует в односимвольные айдишники.
From:morfizm
Date:October 21st, 2017 10:23 pm (UTC)
(Link)
Спасибо, попробую его. Впрочем, я полазил по форумам, и там говорят, что серьёзное улучшение startup time (в контексте мелких утилит) возможно только если реюзать JVM. Это неплохая идея. Типа клиент-серверный дизайн, клиент это легковесная хрень на C++, которая стартует java процесс и подключается к нему, отправляя аргументы и зачитывая output. Надо только с security как-то решить вопрос.
From:_m_e_
Date:October 21st, 2017 09:41 pm (UTC)
(Link)
Как-то очень медленно секунда на гигабайт на больших файлах для CRC32. Более менее современные процессоры должны с такой скоростью считать MD5, а те что побыстрее и SHA256.

64kb буфер, это из времён MS DOS, сейчас по умолчанию должно быть мегабайт 8 👍
From:morfizm
Date:October 21st, 2017 10:29 pm (UTC)
(Link)
Ну, меньше секунды. Без startup-overhead'а там 0.6 сек выходило. У меня процессор 2 поколения core i7, память DDR3. Не знаю, насколько это "современно", но интуитивно. Я могу попробовать просто просуммировать все байты и посмотреть, сколько это займёт. Подозреваю, что полсекунды это overhead на перекачку данных из файлового кэша OS, и быстрее будет никак.

Насчёт буфера ты не прав. Позапускай бенчмарки и охуей от того, что 64kb даёт best perf на современном железе. Я и сейчас в этом убедился, и недавно на работе к такому же выводу пришёл. Подозреваю, что когда working set порядка 64kb, overhead от memory аллокаций не выходит за пределы L1/L2 кэша. Но это всего лишь догадка. Суть в том, что 8 мегабайт - плохой буфер, будет медленнее!
From:archaicos
Date:October 21st, 2017 09:54 pm (UTC)
(Link)
А я две недели с перерывами йопся с настройками терминала на маке.
2017-й год. Чтобы настроить софт (примитивный редактор кода) для комфортной работы нужно изучить говно мамонта из 1970-х.
From:morfizm
Date:October 21st, 2017 10:30 pm (UTC)
(Link)
Всего две недели, ты молодец! :)
Я с маком намучился тоже будь здоров.
From:metaller
Date:October 22nd, 2017 02:23 am (UTC)
(Link)
22 MB scala в 11 MB JAR ?
From:morfizm
Date:October 22nd, 2017 02:32 am (UTC)
(Link)
Yep. Jar делает компрессию.
From:metaller
Date:October 22nd, 2017 02:25 am (UTC)
(Link)
Непонятно зачем для решения этой задачи было привлекать Scala ? На обычной Java написал бы - и меньше выходной файл, меньше библиотек и меньше траха с настройкой development environment. А кроме того я заметил что даже hello world на Scala компилится раз в 10 дольше, чем на Java.
From:morfizm
Date:October 22nd, 2017 02:34 am (UTC)
(Link)
Эстетика. Scala красивее.