Привет! Меня зовут Александр Каленюк, и я крепко подсел на C++. Пишу на C++ 18 лет кряду, и все эти годы отчаянно пытаюсь избавиться от этой разрушительной зависимости.
Всё началось в конце 2005 года, когда мне довелось писать движок для симуляции 3D-пространства. В этом движке было буквально всё, чем язык C++ мог похвастаться в 2005 году. Трёхзвёздочные указатели, восьмиуровневые зависимости, C-подобные макросы повсюду. Кое-где – вкрапления ассемблера. Итераторы в стиле Степанова и мета-код в стиле Александреску. В общем, всё. Кроме ответа на самый важный вопрос: зачем?
Некоторое время спустя нашёлся ответ и на этот вопрос, но скорее не «для чего?», а «как же так?». Оказалось, этот движок на протяжении примерно 8 лет писали 5 разных команд. Каждая из этих команд привносила в проект свою любимую блажь, упаковывая старый код в стильные обёртки, но при этом не привнося в сам движок почти никакой ценности.
В первое время я честно пытался вникнуть во все детали, вплоть до мельчайших. Это было дело совершенно неблагодарное, и в какой-то момент я сдался. Продолжал закрывать задачи и фиксить баги. Не могу сказать, что работал крайне продуктивно, скорее минимально продуктивно, чтобы меня не уволили. Но как-то раз начальник меня спрашивает: «хочешь в этом шейдере переписать часть кода с ассемблера на GLSL»? Я понятия не имел, что это такое — GLSL, но счёл, что он едва ли окажется хуже, чем C++ — и согласился. Действительно, он оказался не хуже.
Так сложился своеобразный паттерн. Я по-прежнему писал в основном на C++, но то и дело ко мне обращались с вопросом: «А хочешь вот эту задачу, она не на C++?» И я отвечал: «Давай!» А затем брался за задачу, какова бы она ни была. Я писал на C89, MASM32, C#, PHP, Delphi, ActionScript, JavaScript, Erlang, Python, Haskell, D, Rust, а однажды даже ввязался в авантюру с одним из рук вон плохим скриптовым языком, он называется InstallShield. Я писал на VisualBasic, на bash, на нескольких проприетарных языках, которые даже упоминать будет противозаконно. Как-то раз я даже случайно написал собственный язык. Это был простой интерпретатор в стиле Lisp, призванный помочь разработчикам игр автоматизировать загрузку ресурсов. После этого я ушёл в отпуск. Когда вернулся, оказалось, что они пишут на этом интерпретаторе целые игровые сцены, поэтому пришлось некоторое время его поддерживать.
Итак, в течение последних 17 лет я честно пытался завязать с C++, но всякий раз, попробовав какую-нибудь блестящую новинку, брался за старое. Тем не менее, считаю, что писать на C++ — плохая привычка. Этот язык небезопасен, не так эффективен, как считается, при работе с ним программисту приходится жутко ломать голову над вещами, которые никак не связаны с созданием ПО. А вы знаете, что в MSVC uint16_t(50000) * uin16_t(50000) == -1794967296
? А знаете, почему? Да, вот такие мысли меня занимали.
Думаю, моральный долг программиста, который долгое время работал с C++ — отговаривать молодёжь от идеи строить профессию на основе этого языка, подобно тому, как непросыхающий алкоголик морально обязан предупреждать молодых об опасности зелёного змия.
Но почему нельзя просто взять и бросить? В чём же дело? Дело в том, что ни один язык, в особенности из числа так называемых «убийц C++» в современном мире ни в чём реально не превосходит C++. Фишка большинства этих языков — просто в стреноживании программиста, якобы, для его же блага. Это нормально, только вот проблема «как плохому программисту написать хороший код» коренится в XX веке, когда плотность транзисторов на кристалле удваивалась каждые полтора года, а абсолютное количество программистов удваивалось каждые 5 лет.
Мы же теперь живём в XXI веке. В мире настолько много опытных программистов, как никогда ранее в истории. И ещё нам сейчас более чем когда-либо ранее необходим эффективный софт.
В XX веке всё было проще. У вас есть идея, вы обёртываете её в некий пользовательский интерфейс и продаёте как продукт для ПК. Программа тормозит? Какие проблемы? Всего через полтора года компьютеры станут вдвое быстрее. Главное войти на рынок, начать продажу фич, предпочтительно — чтобы они работали без багов. Естественно, в таких условиях хорошо, если сам компилятор помогает программистам избегать некоторых багов. Ведь баги не приносят прибыли, а вам всё равно приходится оплачивать труд программистов, независимо от того, получаются у них фичи или баги.
Теперь ситуация изменилась. У вас есть идея, вы укладываете её в контейнер Docker и запускаете программу в облаке. Теперь вы зарабатываете на людях, которые будут пользоваться вашим софтом лишь в том случае, когда он решает стоящие перед ними задачи. Даже если софт делает всего одну вещь, но делает её хорошо, вам заплатят. Вам не приходится нашпиговывать ваш продукт импровизированными фичами, просто чтобы продать его очередную версию. С другой стороны, за неэффективность вашего кода теперь расплачиваетесь именно вы. Даже одна кривоватая процедура отразится в вашем счёте за AWS.
Теперь ситуация изменилась, и требуется меньше фич, а в то же время – более высокая производительность при работе с теми фичами, которые у вас уже есть.
А потом вдруг оказывается, что все эти «убийцы C++», в том числе, те, которых я всем сердцем люблю и уважаю, в частности, Rust, Julia и D, тоже не решают проблем XXI века. Да, на этих языках получается написать больше фич с меньшим количеством багов, но это не очень помогает, если вам приходится выжимать мощность арендованного вами оборудования вплоть до последнего флопса.
В принципе, у них в самом деле есть конкурентное преимущество над C++. Если уж на то пошло — и друг над другом. Большинство из них, взять, к примеру, Rust, Julia и Clang, даже работают с одним и тем же бекэндом. Невозможно определить победителя в автогонке, если все претенденты сидят в одной машине.
Так какие же технологии действительно дают конкурентное преимущество над C++ или, в более общем смысле, над всеми опережающими компиляторами?
Хороший вопрос!
Рад, что вы спросили.
Киллер C++ № 1. SPIRAL
Прежде, чем подробно обсудить, что такое SPIRAL, давайте проверим, не подводит ли нас интуиция. Как вы считаете, что быстрее: стандартная синусная функция C++ или четырёхчастная многочленная модель синуса?
// версия 1
auto y = std::sin(x);
// версия 2
y = -0.000182690409228785*x*x*x*x*x*x*x
+0.00830460224186793*x*x*x*x*x
-0.166651012143690*x*x*x
+x;
Второй вопрос. Как получится быстрее — с использованием логических операций и вычислениями по короткой схеме, либо с преобразованием логического выражения в арифметическое?
// версия 1
if (xs[i] == 1
&& xs[i+1] == 1
&& xs[i+2] == 1
&& xs[i+3] == 1) // xs – это булевы значения, сохранённые как целочисленные
// версия 2
inline int sq(int x) {
return x*x;
}
if(sq(xs[i] - 1)
+ sq(xs[i+1] - 1)
+ sq(xs[i+2] - 1)
+ sq(xs[i+3] - 1) == 0)
И последний вопрос. Как отсортировать тройки быстрее: при помощи пузырьковой сортировки с ветвлением или при помощи индексной сортировки без ветвления?
// версия 1
if(s[0] > s[1])
swap(s[0], s[1]);
if(s[1] > s[2])
swap(s[1], s[2]);
if(s[0] > s[1])
swap(s[0], s[1]);
// версия 2
const auto a = s[0];
const auto b = s[1];
const auto c = s[2];
s[int(a > b) + int(a > c)] = a;
s[int(b >= a) + int(b > c)] = b;
s[int(c >= a) + int(c >= b)] = c;
Если вы без сомнения ответили на все три вопроса, даже не призадумавшись и не погуглив, то интуиция вас подводит. Вы попались. Вне контекста ни на один из этих вопросов чёткого ответа не существует.
1. Многочленная модель в 3 раза быстрее стандартной синусной функции, если собрать её при помощи clang 11 и опциями -O2 -march=native, а затем выполнить на Intel Core i7-9700F. Но, если собрать её при помощи NVCC с опциями --use-fast-math и выполнить на GPU, а именно на GeForce GTX 1050 Ti Mobile, то стандартная синусная функция окажется в 10 раз быстрее многочленной модели.
2. Кроме того, на i7 имеет смысл отказаться от вычислений по короткой схеме в пользу векторной арифметики. Тогда этот листинг выполняется вдвое быстрее. Но на ARMv7 с теми же настройками clang и -O2 стандартная логика получается на 25% быстрее, чем при микро-оптимизациях.
3. Если же сравнивать индексную сортировку с пузырьковой, оказывается, что индексная втрое быстрее на Intel, а пузырьковая втрое быстрее на GeForce.
Поэтому такие миленькие микро-оптимизации, столь любимые всеми, могут с равным успехом и ускорить ваш код втрое, и замедлить его на 90%. Всё зависит от контекста. Как было бы хорошо, если бы компилятор умел выбрать за нас наилучшую альтернативу, например, чтобы индексная сортировка как по волшебству превращалась в пузырьковую, стоит нам только сменить целевую платформу, под которую идёт сборка. Но, пожалуй, компилятору это не по силам.
1. Даже если разрешить компилятору заново реализовать синус как многочленную модель, таким образом пожертвовав точностью ради скорости, компилятор всё равно не знает, какой точности мы добиваемся. В C++ нельзя указать: «разрешено, чтобы эта функция допускала ошибку». Всё, что у нас есть на этот случай — флаги компилятора вроде “--use-fast-math”, причём, только в области действия единицы трансляции.
2. Во втором примере компилятор не знает, что возможны всего два значения — 0 и 1, поэтому не сможет предложить оптимизацию, которую придумали бы мы. Вероятно, мы могли бы ему подсказать, использовав здесь подходящий булев тип, но это была бы уже совершенно другая задача.
3. А в третьем примере примеры кода различаются настолько сильно, что их даже сложно опознать как синонимичные. Мы слишком детализировали код. Будь это просто std::sort, компилятор был бы явно менее стеснён в выборе алгоритма. Но он бы не выбрал ни пузырьковую, ни индексную сортировку, поскольку оба этих алгоритма неэффективны при работе с большими массивами, а std::sort работает с обобщённым контейнером, ориентированным на перебор.
Здесь мы и подходим к SPIRAL. Этот проект совместно разработан университетом Карнеги-Меллон и Высшей технической школой Цюриха. Если коротко: экспертам по обработке сигналов надоело переписывать любимые алгоритмы под каждый новый образец оборудования, и они создали программу, которая делает это за них. Программа принимает высокоуровневое описание алгоритма и подробное описание аппаратной архитектуры, после чего оптимизирует код до тех пор, пока не отыскивает наиболее эффективную реализацию алгоритма для указанного аппаратного обеспечения.
SPIRAL существенно отличается от Fortran и подобных ему языков в том, что действительно решает задачу оптимизации в подлинно математическом смысле. SPIRAL определяет время выполнения как целевую функцию и отыскивает её глобальный оптимум в фактор-пространстве вариантов реализации, заданном рамками аппаратной архитектуры. Ни один компилятор ничего подобного не делает.
Компилятор не добивается подлинной оптимизации. Он оптимизирует код, руководствуясь эвристикой и тем, чему его научили программисты. В сущности, компилятор не работает как машина, ищущая оптимальное решение, он просто пишет как программист на ассемблере. Хороший компилятор — это хороший ассемблер-программист, только и всего.
SPIRAL — это исследовательский проект с ограниченной областью применения и ограниченным бюджетом. Но его результаты уже впечатляют. Так, при быстрых преобразованиях Фурье данное решение однозначно превосходит реализации MKL и FFTW. Код работает примерно вдвое быстрее даже на Intel.
Просто оцените масштаб этого достижения. MKL — это математическая библиотека функций ядра, разработанная самой компании Intel, а уж эти ребята лучше кого бы то ни было знают, как устроено их фирменное аппаратное обеспечение. В свою очередь, FFTW, известная в народе как «Самое быстрое преобразование Фурье на всём Диком Западе» — это высокоспециализированная библиотека, разработанная теми, кто лучше всех знает этот алгоритм. Обе команды – настоящие асы своего дела, и просто ошеломительно, что SPIRAL вдвое уделывает их обоих.
Вот GitHub-страница проекта SPIRAL: https://github.com/spiral-software/spiral-software. Если вышеприведённые цифры вас не убедили, можете всё перемерять самостоятельно.
Когда используемые в SPIRAL технологии оптимизации будут финализированы и доведены до коммерческого использования, не только C++, но и Rust, Julia, и даже Fortran столкнутся с ранее не виданной конкуренцией. Зачем вообще программировать на C++, если достаточно дать высокоуровневое описание алгоритма — и инструмент сделает вам код, в два раза обгоняющий код на C++?
Киллер C++ №2. Numba
Самый лучший язык программирования — тот, который тебе уже хорошо известен. На протяжении нескольких десятилетий пальму первенства удерживал язык C. Он до сих пор остаётся в высшем эшелоне рейтинга TIOBE, и в первой десятке с ним соседствуют другие C-образные языки. Правда, всего около двух лет назад произошло нечто неслыханное. C уступил первое место… кому?
Оказывается — языку Python. Это язык, который в 90-е никто не воспринимал всерьёз, его считали просто ещё одним скриптовым языком, каких уже насчитывалось огромное множество.
Вы можете возразить: «Фу, Python же медленный!», но в таком случае это будет терминологический нонсенс. Язык программирования — как сковородка или гармошка, просто не может работать однозначно «медленно» или «быстро». Скорость игры на гармошке зависит от мастерства гармониста, а «скорость» языка зависит от скорости компилятора.
«Но Python — не компилируемый язык», — могли бы возразить мне далее, и снова бы не попали. Существует множество компиляторов для Python, и самый многообещающий из них, в свою очередь, является Python-скриптом. Позвольте, я поясню.
Был у меня однажды проект. Симулятор 3D-печати был исходно написан на Python, а затем переписан на C++ «ради увеличения производительности», потом портирован на GPU и только после этого достался мне. Я целые месяцы потратил, чтобы портировать эту сборку на Linux и оптимизировать код GPU для Tesla M60, поскольку на тот момент это было самое дешёвое предложение от AWS. Тем временем, я валидировал все изменения в коде C++/CU, следя за тем, чтобы не отступать от оригинального кода на Python. То есть, занимался чем угодно кроме той вещи, на которой специализируюсь, то есть, кроме разработки геометрических алгоритмов.
Когда же я, наконец, всё закончил, мне позвонил один студент из Бремена (совместитель) и сказал: «вижу, вы отлично справляетесь с разнородным материалом. Скажите пожалуйста, не могли бы вы помочь мне запустить один алгоритм на GPU?». Конечно же! Я рассказал ему про CUDA, CMake, сборку Linux, тестирование и оптимизацию; мы, может быть, час проговорили. Он очень вежливо меня выслушал, а в завершение сказал: «Всё это очень интересно, но у меня к вам есть специфический вопрос. Допустим, у меня есть функция, перед её определением я написал @cuda.jit, а Python пишет мне что-то о массивах и не компилирует ядро. Не знаете ли, в чём здесь может быть проблема?»
Я не смог ответить. Но он примерно за день сам во всём разобрался. По всей видимости, Numba не работает с нативными списками Python, а просто принимает данные в форме массивов NumPy. Он это выяснил и запустил свой алгоритм на GPU. На Python. У него не возникло ни одной из тех проблем, над которыми я бился месяцами. Хотите, чтобы код работал на Linux? Не проблема, просто запускайте на Linux. Хотите, чтобы он был согласован с кодом Python? Не проблема, ведь это код на Python. Хотите его оптимизировать под целевую платформу? Опять же, никаких проблем. Numba оптимизирует код именно под ту платформу, на которой вы его выполняете, поскольку он не подвергается опережающей компиляции — нет, он компилируется по требованию, уже будучи развёрнут.
Разве это не чудесно? Ну, нет. Не для меня, по крайней мере. Я месяцами работал с C++, решая проблемы, которые в Numba просто никогда не возникают, а этот совместитель из Бремена справился с теми же задачами всего за несколько дней. А мог бы и за несколько часов, если бы предварительно ему не пришлось впервые разбираться в Numba. Так что же это за Numba? Может быть, тут не обошлось без колдовства?
Никакого колдовства. Декораторы Python за вас превращают любой фрагмент кода в его абстрактное синтаксическое дерево, после чего вы можете делать с ним что угодно. Numba — это библиотека Python, готовая компилировать абстрактные синтаксические деревья под любой серверный интерфейс на любой платформе, которую она поддерживает. Если вы хотите скомпилировать ваш код Python так, чтобы он выполнялся на многих ядрах ЦП в стиле чрезвычайно параллельных вычислений — просто сообщите Numba, что его нужно скомпилировать именно так. Если вы хотите выполнять что-либо на GPU, опять же, просто попросите об этом.
@cuda.jit
def matmul(A, B, C):
"""Перемножить квадратные матрицы C = A * B."""
i, j = cuda.grid(2)
if i < C.shape[0] and j < C.shape[1]:
tmp = 0.
for k in range(A.shape[1]):
tmp += A[i, k] * B[k, j]
C[i, j] = tmp
Numba — один из тех компиляторов Python, из-за которых C++ морально устаревает. Но, теоретически, он ничем не лучше C++, поскольку работает всё с теми же серверными интерфейсами. Использует CUDA для программирования под GPU и LLVM для работы с ЦП. На практике решения Numba легче адаптировать к любому новому аппаратному обеспечению (поскольку они не требуют никакой опережающей пересборки), а также применять все доступные виды оптимизации.
Разумеется, привлекательнее был бы явный выигрыш в производительности, как в случае со SPIRAL. Но SPIRAL — в большей степени исследовательский проект, он, возможно, и вытеснит C++, но только и только в том случае, если ему повезёт. Numba с Python уже сейчас одолевают C++, прямо на наших глазах. Ведь если можно писать на Python и иметь производительность как на C++, то зачем вообще писать на C++?
Киллер C++ №3. ForwardCom
Теперь сыграем в другую игру. Я даю вам ещё три фрагмента кода, а вы угадываете, какой из них (возможно, не один) написан на ассемблере. Вот они:
Первый
invoke RegisterClassEx, addr wc ; зарегистрировать класс окна
invoke CreateWindowEx,NULL,
ADDR ClassName, ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT, CW_USEDEFAULT,\
CW_USEDEFAULT, CW_USEDEFAULT,\
NULL, NULL, hInst, NULL
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; отобразить окно на ПК
invoke UpdateWindow, hwnd ; обновить клиентскую область
.while TRUE ; Входим в цикл сообщений
invoke GetMessage, ADDR msg,NULL,0,0
.break .if (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
Второй
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
get_local $lhs
get_local $rhs
i32.add)
(export "add" (func $add)))
Третий
v0 = my_vector // хотим просуммировать это по горизонтали
int64 r0 = get_len ( v0 )
int64 r0 = round_u2 ( r0 )
float v0 = set_len ( r0 , v0 )
while ( uint64 r0 > 4) {
uint64 r0 >>= 1
float v1 = shift_reduce ( r0 , v0 )
float v0 = v1 + v0
}
Если вы догадались, что все три примера написаны на ассемблере — поздравляю! Ваша интуиция уже гораздо лучше!
В первом примере имеем дело с MASM32. Это макроассемблер с операторами «if» и «while», на нём пишутся нативные приложения под Windows. Да, именно «по сей день», а не «когда-то». Microsoft ревностно отстаивает обратную совместимость Windows с Win32 API, так что все программы, когда-либо написанные на MASM32, до сих пор могут работать на современных ПК.
Ирония судьбы в том, что язык C изобрели, чтобы упростить трансляцию UNIX с PDP-7 на PDP-11. Он проектировался как портативный вариант ассемблера, который мог бы пережить бурные события 70-х в области аппаратных архитектур, сравнимые по масштабу с Кембрийским взрывом. Но в XXI веке аппаратная архитектура развивается настолько медленно, что те программы, которые я писал на MASM32 20 лет назад, и сегодня отлично собираются и выполняются. Но я совершенно не уверен, что то приложение на C++, которое я в прошлом году собирал с CMake 3.21, соберётся и на будущий год с CMake 3.25.
Второй фрагмент написан на WebAssembly. Это даже не макроассемблер, в нём нет операторов «if» и «while». Его правильнее назвать человеко-читаемым вариантом машинного кода, рассчитанным на работу в браузере. Концептуально, в любом браузере.
Код WebAssembly совершенно не зависит от аппаратной архитектуры. Можно сказать, что он обслуживает абстрактную, виртуальную, универсальную машину, называйте её как хотите. Если вы способны прочитать этот листинг, значит, он физически уже присутствует на вашей машине.
Но самый интересный фрагмент кода — третий. Это ForwardCom, вариант ассемблера, предлагаемый Агнером Фогом. Агнер Фог — прославленный автор мануалов по оптимизации C++ и ассемблера. Точно как и WebAssembly, этот вариант охватывает не столько ассемблер, сколько универсальный набор инструкций. Этот набор инструкций предназначен для обеспечения не только обратной, но и прямой совместимости. Отсюда и название. Полностью ForwardCom именуется «открытая прямо совместимая архитектура с соответствующим набором инструкций». Иными словами, это не столько предложение нового варианта ассемблера, сколько инициатива о мире.
Наиболее распространённые семейства архитектур хорошо известны: это x64, ARM и RISC-V. Во всех них — разные наборы инструкций. Но никто не сможет обосновать, почему ситуация должна оставаться именно такой. Все современные процессоры, кроме, возможно, самых простых, выполняют не тот код, который вы им скармливаете, а микрокод, в который они транслируют ваш ввод. Поэтому не только в M1 предусмотрен слой для обеспечения обратной совместимости с Intel; нет, в сущности, в любом процессоре есть такой слой обратной совместимости для взаимодействия с его же более ранними версиями.
Так почему бы проектировщикам архитектур не договориться о создании аналогичного слоя, но для обеспечения прямой совместимости? Если не считать конфликта интересов между корпорациями, прямо конкурирующими друг с другом — ничего. Но, если когда-нибудь производители процессоров станут работать с общим набором инструкций, а не реализовать новый уровень совместимости для взаимодействия с оборудованием каждого из конкурентов, то программирование на ассемблере благодаря ForwardCom вновь станет мейнстримом. Такая прямая совместимость вылечит тяжелейший невроз у любого специалиста по ассемблеру. «Что, если я единственный раз в жизни напишу код для данной конкретной архитектуры, и именно эта архитектура через год устареет»?
Если реализовать слой прямой совместимости, то ни один код в таком случае никогда не устареет. В этом и суть.
Кроме того, программирование на ассемблере не развивается из-за мифа, будто писать на ассемблере сложно и, следовательно, непрактично. Фог и этот момент учитывает в своём предложении. Если кому-то кажется, что на ассемблере писать сложно, а на C — нет, что ж, хорошо, давайте сделаем ассемблер похожим на C. Нет никаких причин, по которым современный ассемблер должен выглядеть так же, как и его дедушка в 1950-е.
Я только что показал вам три варианта ассемблера. Ни один из них не выглядит похожим на «традиционный» ассемблер, и не должен.
Итак, ForwardCom — это ассемблер, на котором можно писать оптимальный код, и этот код никогда не устареет. Более того, для работы с ним вам не придётся изучать «традиционный» ассемблер. С любой практической точки зрения, это C будущего. А не C++.
Когда же C++ окончательно отомрёт?
Мы живём в мире постмодерна. В этом мире больше ничего не умирает (кроме людей). Точно как латынь не вымерла окончательно, так не вымерли и COBOL, Algol 68, Ada. Язык C++ также обречён на такую полужизнь. C++ никогда окончательно не отомрёт, но его вытеснят из мейнстрима новые, более мощные технологии.
Даже не «вытеснят», а «уже вытесняют». На мою нынешнюю работу я пришёл как C++-программист, а сегодня мой рабочий день начинается с Python. Я пишу уравнения, SymPy решает их за меня, а потом транслирует это решение на C++. Затем я вставляю получившийся код в библиотеку C++, даже ничуть его не форматируя, ведь за меня это сделает clang-tidy. Статический анализатор проверит, не перепутал ли я где-нибудь пространства имён, динамический анализатор проверит, нет ли утечек памяти. Конвейер CI/CD обеспечит кроссплатформенную компиляцию. Профилировщик поможет мне понять, как именно работает мой код, а дизассемблер — почему.
Если мне придётся променять C++ на «не C++», то 80% моей работы никак не изменится. Язык C++ уже просто нерелевантен для большинства моих задач. Могу ли я в таком случае утверждать, что для меня C++ уже на 80% мёртв?