C++26 — встреча ISO в Хагенберге

В этот раз прорабатывались следующие большие темы:
- std::hive
- Constexpr, ещё больше constexpr
- Безопасность, контракты, hardening, профили, UB и std::launder
- Relocate
- #embed
Искусство создания компьютерных программ
[Прим. пер.: на Хабре уже был перевод этой статьи, но незавершённый примерно на четверть.]
Неправда.
Калькулятор должен показывать результат введённого математического выражения. А это намно-о-ого сложнее, чем кажется.
В этом посте я расскажу величайшую историю о разработке приложения-калькулятора.
На изображении выше показан калькулятор из iOS.
Заметили что-нибудь?
Он посчитал неправильно.
(10100) + 1 − (10100) равно 1, а не 0.
Android считает правильно. А причина, по которой он это делает, абсолютно безумна.
Меня заинтриговал комментарий GuB-42 на Hacker News:
При помощи последовательностей ZWJ (Zero Width Joiner) теоретически можно закодировать в один эмодзи неограниченный объём данных.
Действительно ли можно закодировать в один эмодзи произвольные данные?
tl;dr: да, однако я нашёл решение и без ZWJ. На самом деле, можно закодировать данные в любой символ Unicode. Например, в этом предложении есть скрытое послание: This sentence has a hidden message󠅟󠅘󠄐󠅝󠅩󠄜󠄐󠅩󠅟󠅥󠄐󠅖󠅟󠅥󠅞󠅔󠄐󠅤󠅘󠅕󠄐󠅘󠅙󠅔󠅔󠅕󠅞󠄐󠅝󠅕󠅣󠅣󠅑󠅗󠅕󠄐󠅙󠅞󠄐󠅤󠅘󠅕󠄐󠅤󠅕󠅨󠅤󠄑. (Попробуйте вставить его в декодер.)
Наверное, каждый второй разработчик на ПЛИС в начале своего пути пытался визуализировать работу своих схем. Кто-то подключал TFT-дисплей, кто-то — VGA монитор. А у меня под рукой оказался только телевизор с композитным входом. Ну что ж, работаем с тем, что есть!
Переезд большого сервиса с Perl на Golang едва ли кому-то покажется простой задачей. А теперь представьте, что это главная страница Яндекса, на которую ежедневно заходят миллионы пользователей. И что продукт постоянно дорабатывается, а значит, нельзя взять и остановить разработку на пару лет переезда. Представили? Сложно? А вот, оказывается, всё возможно.
Привет, Хабр! Меня зовут Вячеслав Круглов. Я руковожу одной из команд разработки бэкенда главной страницы Яндекса. Расскажу, как мы переписывали бэкенд с Perl на Go, поделюсь интересными подробностями переезда, а также сравню компоненты и продуктовые блоки.
Привет, Хабр. Меня зовут Рогатнев Сергей. Я работаю в Контуре ведущим разработчиком уже более 7 лет. За это время я поработал как минимум над десятью разными проектами в разных командах. Это были и проекты с историей на 10 лет, и стартапы, делающие свои первые шаги. Где-то я был всего 2–3 месяца, а где-то задерживался на пару лет. Такой формат работы позволил мне увидеть совершенно разные подходы к работе и написанию кода. За это время я адаптировался к переходам и смене команд, но мой собственный code style практически исчез, потому что нет двух команд с одинаковым стилем.
В этой статье я хочу показать вам примеры таких холиваров, которые я встретил работая над разными C#-проектами.
// Смещение для корректировки кода ASCII каждого символа в строке кода ISO страны для определения соответствующего флага.
const EMOJI_CHARACTER_OFFSET = 127397;
const getEmojiForCountryCode = (countryCode: string) =>
String.fromCodePoint(
...countryCode
.toUpperCase()
.split('')
.map((char) => char.charCodeAt(0) + EMOJI_CHARACTER_OFFSET),
);
// "en-US"
const currentLanguageCode = navigator.language;
// "US"
const currentCountryCode = currentLanguageCode.split("-")[1];
// "🇺🇸"
getEmojiForCountryCode(currentCountryCode);
// "🇫🇷"
getEmojiForCountryCode("FR");
// "🇸🇪"
getEmojiForCountryCode("SE");
Некоторое время назад я написал несколько статей о различных трюках, применявшихся в операционной системе DOS, чтобы вписаться в те жёсткие лимиты памяти, которые действовали в реальном режиме на архитектуре x86. Постоянно возникал и оставался без ответа один вопрос: а каковы были различные «модели», которые предлагались компиляторами тех времён? Взгляните, как выглядело меню для генерации кода в Borland Turbo C++.
Tiny (крошечный), small (маленький), medium (средний), compact (компактный), large (большой), huge (огромный)… Что означают эти опции? Каковы их эффекты? Ещё важнее… а так ли важен весь этот антиквариат сегодня, в мире 64-разрядных машин и гигабайтных ОЗУ? Чтобы ответить на этот вопрос, сделаем небольшой обзор архитектуры 8086 и тех двоичных форматов, которые поддерживались в DOS.
В СМИ много говорят о том, что разработчики ПО скоро потеряют работу из-за ИИ. Я в это не верю.
Это не конец программирования. Это конец программирования в том виде, в котором мы его знаем сегодня.
Собеседование — это ключевой этап, определяющий, насколько кандидат подходит компании. Важно создать процесс, который не только выявит технические знания, но и покажет, насколько человек соответствует корпоративной культуре, стрессоустойчив ли он и способен ли работать в условиях реальной нагрузки.
Тестовое задание
Перед собеседованием можно добавить этап выполнения тестового задания. Хорошее тестовое задание должно быть максимально приближено к реальным задачам. Чтобы оценить навыки, можно предложить что-то объёмное, например, разработку небольшого, но полнофункционального сервиса. Важно, чтобы кандидат сделал всё самостоятельно и в кратчайшие сроки — это покажет, насколько он заинтересован в позиции. Если человек отказывается от тестового задания, это говорит о недостаточной вовлечённости.
Собеседование
Сколько человек должно проводить собеседование? Оптимально 3-5. Один интервьюер может что-то упустить, а вот группа сможет задать вопросы с разных точек зрения.
Эта программа представляет собой свободную от зависимостей реализацию GPT-2. Она загружает матрицу весов и файл BPE из оригинальных файлов TensorFlow, токенизирует вывод при помощи простого энкодера, работающего по принципу частотного кодирования, реализует базовый пакет для линейной алгебры, в котором заключены математические операции над матрицами, определяет архитектуру трансформера, выполняет инференс трансформера, а затем очищает вывод от токенов при помощи BPE-декодера. Всё это — примерно в 3000 байт на C.
Код достаточно эффективно оптимизирован — настолько, что малый GPT-2 на любой современной машине выдаёт отклик всего за несколько секунд. Чтобы этого добиться, я реализовал KV-кэширование и применил эффективный алгоритм перемножения матриц, а также добавил опциональный OMP-параллелизм.
Взяв это за основу, можно создать некий аналог Chat GPT — при условии, что вас не слишком волнует качество вывода (объективно говоря, вывод получается просто ужасный… но решение работает). Здесь есть некоторые глюки (особенно с обработкой символов в кодировке UTF-8), а для эксплуатации модели размером XL с широким контекстным окном может потребоваться ~100 ГБ оперативной памяти. Но, если вы просто набираете текст в кодировке ASCII при помощи малого GPT2, то такая модель должна нормально работать примерно везде.
Я выложил весь код на GitHub, поэтому можете свободно брать его там и экспериментировать с ним.
ML‑модели применяются в сервисах Яндекса уже много лет, мы накопили большой опыт в их обучении. Статьи об этом коллеги регулярно публикуют, в том числе на Хабре. Но сегодня хочу обсудить другую не менее важную задачу — ускорение инференса (процесса работы на конечном устройстве) моделей. Скорость зависит от разных условий, главным образом от архитектуры и железа, но есть множество интересных способов повлиять на неё. Особенно актуальна проблема тяжёлого инференса при использовании больших языковых моделей (LLM) — на то они и large!
Для команды YandexGPT, в которой я и тружусь вместе со своими коллегами, тема инференса LLM находится в разряде вечных вопросов. С предыдущей статьи прошёл уже почти год, опыта у нас стало больше — получилось протестировать новые подходы, которыми и хочется поделиться сегодня.
Друзья! Многие из вас, возможно, как и я, интересовались изучением и использованием в работе очень эффективного и востребованного языка программирования Rust но, как и я, оставляли свои попытки из-за сложности, запутанности и многослойности доступного материала и книг по этой теме.
Лично я делал не меньше 5 попыток на протяжении последних 10 лет, прорабатывая, большей частью в свободное и личное время, литературу, некоторые книги по несколько раз, в поисках ответов на простые человеческие вопросы - как свободно писать на Rust и решать, как орешки, ежедневные задачи, не страдая от головной боли и хорошо понимая, что происходит и почему простая программа не компилируется.
В результате, сейчас, наконец-то, стало понятно все в деталях, код пишется быстро, задачи решаются легко, результаты применения языка поражают своей эффективностью и точностью и возникло желание восполнить пробел и поделиться с вами накопленным опытом, но, главное, провести и привести вас к совершенному пониманию простоты и лаконичности этого удивительно эффективного языка наиболеее коротким и приятным путем. Приготовьтесь к увлекательной и познавательной прогулке и подъему по ступеням вверх, к мастерству написания полезного кода на Rust.
C и C++ не имеют встроенной сборки мусора, поэтому разработчик сам решает, как и когда выделять и освобождать память. Мы, конечно, можем покивать в сторону STL, сокрытия аллокаций в контейнерах, но от этого они никуда не денутся. Просто если раньше приходилось думать про выделенный кусок памяти, понимать, как он скажется на времени фрейма, помнить, что его надо удалить (а может, не надо и стоит оставить на следующий фрейм), то теперь всё заворачивается в сахарные контейнеры и разработку в стиле STL-blin-vse-sterpit
. STL-то может и стерпит, и даже как-то будет ворочаться, однако не стоит полагаться исключительно на системный аллокатор, бездумно вызывая new
или malloc
для каждого запроса памяти. Вы ведь понимаете, что std::vector
посреди цикла или горячей функции — это плохая идея?
Кроме того, такая практика приводит к ожидаемым проблемам с производительностью даже в обычных приложениях, чего уж говорить про высоконагруженные системы или игры, которые претендуют на что-то быстрее 20 фреймов в секунду.
Пытаться оптимизировать код, который использует системные аллокаторы, — всё равно что сгребать листья в кучу ветреным днём: куча, конечно, сгребается, но постоянно приходится махать грабельками, чтобы она оставалась на одном месте. Даже если выделения памяти происходят последовательно, друг за другом, вот прям без всяких перерывов, нет гарантии, что эти участки будут расположены хотя бы близко друг к другу. В результате при обработке таких данных процессору приходится прыгать по разным участкам памяти, теряя такты просто на поиск данных вместо того, чтобы работать с ними.
Я отнюдь не призываю вас встать на путь ручного управления памятью, ибо он будет усеян ловушками, граблями и чреват утечками. Но разработчик в итоге оказывается перед выбором: либо довериться системному аллокатору и столкнуться с проблемами вроде размазанного перфа, когда вроде и код написан правильно, модно и молодежно, но отчего-то работает небыстро, либо взять всё в свои руки, создавая собственные механизмы выделения и освобождения ресурсов.
Ребята из HFT, Database, Automotive и Embedded-систем наверняка могут рассказать немало интересных историй про оптимизацию new
/delete
. Давайте я расскажу немного про разные аллокаторы в играх?