惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
Vercel News
Vercel News
D
DataBreaches.Net
MongoDB | Blog
MongoDB | Blog
H
Help Net Security
小众软件
小众软件
美团技术团队
T
The Blog of Author Tim Ferriss
爱范儿
爱范儿
D
Docker
Martin Fowler
Martin Fowler
大猫的无限游戏
大猫的无限游戏
博客园 - 聂微东
Blog — PlanetScale
Blog — PlanetScale
H
Hackread – Cybersecurity News, Data Breaches, AI and More
罗磊的独立博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
S
SegmentFault 最新的问题
云风的 BLOG
云风的 BLOG
B
Blog
雷峰网
雷峰网
The Cloudflare Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
ncpp: Как создать самодостаточную экосистему на С++98 в 2...
Vlad441 · 2026-05-10 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение6 мин

Охват и читатели597

Кейс

Современный С++ стал значительно удобнее по сравнению с прошлыми стандартами. STL и стандартная библиотека существенно облегчают жизнь программиста. С каждой новой версией стандарта добавляют что-то новое, что помогает упростить (или усложнить) очередной аспект разработки.

Однако со временем при разработке на С++ начинают появляться вопросики:

  • Везде ли скомпилируется ли моя программа?

    • Поддерживается ли текущая версия компилятора для моей ОС?

    • Поддерживается ли текущий стандарт в моей версии компилятора?

  • Везде ли запуститься моя программа?

    • Какие зависимости тянет моя программа?

      • Есть ли эти зависимости в минимальной комплектации моей ОС?

      • Или может проще все статически намертво слинковать?

  • На сколько эффективны стандартные реализации?

    • На сколько эффективно они линкуются?

    • Какой будет итоговый размер исполняемого файла?

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

Выбор стандарта и окружения

Как же можно запуститься на максимально возможном количестве конфигураций металлолома, сохраняя минимальный комфорт который дает C++?

Самый ранний из доступных стандартов - C++98, под него больше всего поддерживаемых компиляторов (а следовательно и архитектур и ОС для них) было выпущено.

От каких минимальных версий ОС стоит отталкиваться? В ходе изучения доступных системных вызовов на разных версиях ОС, системных вызовов из Win XP в большинстве своем достаточно для комфортной работы и на Win 10+, а некоторые улучшения в новых версиях можно уже добавить в ходе Progressive Enhancement.

Что на счет Linux, версия ядра 2.6 уже содержит большинство вызовов, которые используются сегодня, например epoll(), pthread_* и прочие.

По компиляторам я пришел к выводу, что более-менее возможно поддерживать компиляторы начиная с gcc 3.4 (с натяжкой при Graceful Degradation), а более полноценно поддерживать начиная с gcc ~4.9, собирая тесты на этой версии, а далее уже следовать Progressive Enhancement. Так как это минимальная версия, в которые включены минимально необходимые определения системных вызовов для написания всех модулей проекта.

Примечание: Подразумевается, что вы сможете одновременно и скомпилировать, и запустить на целевой ОС. Я знаю что вы умеете С++23 запускать и на Win95, но собрать C++23 на Win95, если у вас на руках только металлолом без доступа к современной системе, у вас вряд-ли получится. (Или у кого-то таки получилось?)

Проект нацелен на то, что он должен собраться, даже если ваш металлолом это последний в мире ЭВМ.

Отказ от STL

Как я упоминал в прошлой статье, STL - непозволительная роскошь. Чтобы не зависеть от того, как реализован std::string/std::vector/etc в libstdc++, были написаны свои варианты реализаций ncpp::Array/ncpp::String/etc, которые содержат STL подобный интерфейс.

В стандартных контейнерах реализован метод .stack(), чтобы вы могли передать под управление заранее выделенную память и не дергать лишний раз malloc()/free().

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

Unity Build

Для упрощения и ускорения сборки используется Unity Build стиль, где собирается единая единица трансляции (мастер-файл) из .cpp файлов в необходимой последовательности, что может помочь с оптимизациями, однако может потреблять больше памяти в сравнении классической модульной сборкой.

Как я бодался с линковщиком?

Если взять простейший пример Hello World:

#include "src/ncpp.cpp"
using namespace ncpp;
int main(){ print("Hello World\n"); }

И скомпилировать его на gcc 4.9.2 просто с флагами g++ -O2 -std=c++98 -static hello.cpp -o hello.exe -lws2_32 -lpsapi мы получим… 702 КБ. Непорядок. Просчитался, но где?

Что не пошло не так с линковкой?

По умолчанию линковщик рассчитан на модульную структуру, где должен включать только те объектные файлы (.o) в которых находятся необходимые символы (функции) для включения в бинарник. Однако для линковщика .o является минимальной включаемой единицей трансляции, и если из него нужен даже один символ, он включит весь .o целиком.

Если же импортируемые статические библиотеки неэффективно собраны, то линковщик может тянуть значительно больше мертвого кода. Для решения этой проблемы придумали LTO (Link Time Optimization).

Однако LTO в основном рассчитан именно на модульную сборку, где на этапе линковки он по идее должен более агрессивно выбирать только нужные символы, видя весь проект “целиком”.

У GCC есть свой специальный флаг -fwhole-program который лучше приспособлен для этой задачи: Он указывает, что текущая единица трансляция и есть вся программа, что позволяет оптимизировать еще агрессивнее. Сравнительная таблица применения флагов:

702 КБ -(без доп. флагов)
415 КБ -s
370 КБ -s -m32
302 КБ -fno-exceptions -fno-rtti -s -m32

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

g++ -O2 -std=c++98 -static hello.cpp -o hello.exe -lws2_32 -lpsapi -fno-exceptions -fno-rtti -s -m32

302 КБ -fvisibility=hidden
303 КБ -ffunction-sections -fdata-sections -Wl,–gc-sections
138 КБ -flto
138 КБ -flto=thin
138 КБ -flto=thin -fvisibility=hidden -ffunction-sections -fdata-sections -Wl,–gc-sections
138 КБ -flto -fvisibility=hidden -Dexternally_visible=‘visibility(“default”)’
16 КБ -fwhole-program

Как мы видим, не все флаги оптимизации могут помогать в конкретных случаях. Что заметно, флаг -fwhole-program лидирует и может даже немного ускорить компиляцию. Однако в Clang посчитали что он не нужен и LTO справится лучше.

Как мы видим правильные флаги линковки помогают добиться 16 КБ размера исполняемого файла. Однако даже тут оптимизации не смогли вырезать все. Чтобы помочь линковщику, для сравнения я ввел в проект макрос NCPP_BASE_ONLY, который еще на уровне препроцессинга выпиливает большую часть лишнего.

Сравнительная таблица применения флагов с макросом NCPP_BASE_ONLY начиная с g++ -O2 -std=c++98 -static hello.cpp -o hello.exe:

405 КБ -(без доп. флагов)
215 КБ -s
153 КБ -s -m32
129 КБ -s -m32 -flto
129 КБ -s -m32 -flto=thin
16 КБ -s -fwhole-program
12 КБ -s -m32 -fwhole-program

В итоге для кода:

#define NCPP_BASE_ONLY
#include "src/ncpp.cpp"
using namespace ncpp;
int main(){ print("Hello World\n"); }

При использовании таких флагов:

g++ -O2 -std=c++98 -fno-exceptions -fno-rtti -fwhole-program -static -s -m32 hello.cpp -o hello.exe

Удалось добиться размера в 12 кб - размер, сравнимый использованию системного вызова WriteFile. В итоге именно такой вариант параметров я и вшил по умолчанию в сборочный скрипт make.bat

Системщики скажут что это много - и вы будете правы. По оптимизации еще есть над чем работать.

Что с линковкой на Linux?

Тут ситуация усложняется, так как мы наталкиваемся на существенное препятствие: glibc. Его монолитная структура не позволяет линковщику выдернуть только нужное, и он тащится целиком.

  • При использовании iostream которая тянет libstdc++ на gcc 4.8.5 - вы получите 1.3 Mb.

  • И даже при использовании примера выше (без -m32) вы сможете ужаться лишь до 769 Kb.

Как же это можно решить? Отказаться от libc и написать свои syscalls на ASM? Это неоправданно, если только вы не хотите познать ад.

Наиболее компромиссное на текущий момент решение - статическая линковка с musl заместо glibc. В таком случае при использовании:

musl-gcc -O2 -std=c++98 -fno-exceptions -fno-rtti -fwhole-program -static -s hello.cpp -o hello

Вы сможете получить размер около 14 Kb. Этот вариант сборки также можно вызвать из сборочного скрипта make.sh MUSL

Что по зависимостям?

В идеале конечно цель добиться полного Zero Deps, и большая часть компонентов не требуют внешних зависимостей.

Однако с текущей архитектурой современных ОС, некоторые вещи просто невозможно написать без динамической линковки. Например GUI или работу с графикой OpenGL/Vulkan. Нельзя просто взять и вшить ту тонкую прослойку GLX/EGL/libGLdispatch.so чтобы ваш бинарник мог независимо обращаться к видеокарте и дергать из нее API. Ядро-то диспатчить не умеет, а нужных либ в системе может попросту не быть.

Хотя на Windows это работает хитрее. Даже при флаге -static на самом деле у вас все еще остается динамическая зависимость от opengl32.dll, которая содержит реализации OpenGL 1.1 и содержит необходимый wglGetProcAddress() чтобы подгрузить недостающее.

Для любителей ASM

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

Однако ASM - совершенно непереносим, да и мы тут пытаемся писать именно на С++, пытаясь выжать все соки из доступного компилятора на доступной платформе.

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