- Don’t Fear the Reaper
- Life in the Fast Lane
- Go Your Own Way. Часть первая. Стек
- Go Your Own Way. Часть вторая. Куча
В первой из серии статей о GC я представил сборщик мусора в языке D и возможности языка, которые его используют. Два ключевых момента, которые я пытался донести:
GC запускается только тогда, когда вы запрашиваете выделение памяти. Вопреки расхожему заблуждению, GC языка D не может просто взять и поставить на паузу ваш клон Майнкрафта посреди игрового цикла. Он запускается только когда вы запрашиваете память через него и только тогда, когда это необходимо.
Простые стратегии выделения памяти в стиле C и C++ позволяют уменьшить нагрузку на GC. Не выделяйте память внутри циклов — вместо этого как можно больше ресурсов выделяйте заранее или используйте стек. Сведите к минимуму общее число выделений памяти через GC. Эти стратегии работают благодаря пункту № 1. Разработчик может диктовать, когда допустимо запустить сборку мусора, грамотно используя выделение памяти из кучи, управляемой GC.
Стратегии из пункта № 2 подходят для кода, который программист пишет сам, но они не особенно помогут, когда речь идёт о сторонних библиотеках. В этих случаях при помощи механизмов языка D и его рантайма можно гарантировать, что в критических местах кода никакого выделения памяти не произойдёт. Также есть параметры командной строки, которые помогают убедиться, что GC не встанет на вашем пути.
Давайте представим, что вы пишете программу на D и по тем или иным причинам решили полностью исключить сборку мусора. У вас есть два очевидных решения.
Таблетка от жадности
Первое решение — вызвать GC.disable
при запуске программы. Выделение памяти через GC всё ещё будет работать, но сборка мусора остановится. Вся сборка мусора, включая ту, что могла произойти в других потоках.
void main() {
import core.memory;
import std.stdio;
GC.disable;
writeln("Goodbye, GC!");
}
Вывод:
Goodbye, GC!
Преимущество этого способа в том, что все возможности языка, использующие GC, продолжат работать, как и ожидалось. Но если учесть, что будет происходить выделение памяти без всякой очистки, то по здравому размышлению вы поймёте, что такое решение может выйти вам боком. Если всё время бесконтрольно выделять память, то рано или поздно какое-то из звеньев цепи сдаст. Из документации:
Сборка мусора всё может произойти, если это необходимо для дальнейшей корректной работы программы, например в случае нехватки свободной памяти.
Насколько это плохо, зависит от конкретного случая. Если для вас такое ограничение приемлемо, то есть ещё кое-какие инструменты, которые помогут держать всё под контролем. Вы можете по необходимости вызывать GC.enable
и GC.collect
. Такая стратегия позволяет контролировать циклы освобождения ресурсов лучше, чем простые техники из C и C++.
Антимусоросборочная стена
Когда запуск сборщика мусора абсолютно недопустим, вы можете обратиться к атрибуту @nogc
. Повесь его на main
, и минует тебя сборка мусора.
@nogc
void main() { ... }
Это окончательное решение вопроса GC. Атрибут @nogc
, применённый к main
, гарантирует, что сборщик мусора не запустится никогда и нигде на всём протяжении стека вызовов. Больше никаких подводных камней «если это необходимо для дальнейшей корректной работы программы».
Не первый взгляд такое решение кажется гораздо лучшим, чем GC.disable
. Давайте попробуем.
@nogc
void main() {
import std.stdio;
writeln("GC be gone!");
}
На этот раз мы не продвинемся дальше компиляции:
Error: @nogc function 'D main' cannot call non-@nogc function 'std.stdio.writeln!string.writeln'
(Ошибка: @nogc-функция 'D main' не может вызвать не-@nogc–функцию 'std.stdio.writeln!string.writeln')
Сила атрибута @nogc
в том, что компилятор не позволяет его обойти. Он работает очень прямолинейно. Если функция обозначена как @nogc
, то любая функция, которую вы вызываете внутри неё, также должна быть обозначена как @nogc
. Очевидно, что writeln
это требование не выполняет.
И это ещё не всё:
@nogc
void main() {
auto ints = new int[](100);
}
Компилятор не спустит вам с рук и этого:
Error: cannot use 'new' in @nogc function 'D main'
(Ошибка: нельзя использовать 'new' в @nogc-функции 'D main')
Также внутри @nogc
-функции нельзя использовать любые возможности языка, которые выделяют память через GC (их мы рассмотрели в предыдущей статье серии). Мир без сборщика мусора. Большое преимущество такого подхода в том, что он гарантирует, что даже сторонний код не может использовать эти возможности и выделять память через GC за вашей спиной. Недостаток же в том, что сторонние библиотеки, разработанные без @nogc
, становятся для вас недоступны.
При таком подходе вам придётся прибегнуть к различным обходным путям, чтобы обойтись без несовместимых с @nogc
возможностей языка и библиотечных функций, включая некоторые функции из стандартной библиотеки. Некоторые из них тривиальны, другие сложнее, а что-то и вовсе нельзя обойти (мы подробно всё это рассмотрим в будущих статьях). Неочевидный пример одной из таких вещей — исключения. Идиоматический способ породить исключение такой:
throw new Exception("Blah");
Из-за того, что здесь есть new
, в @nogc
-функции так написать нельзя. Чтобы обойти это ограничение, требуется заранее выделить место под все исключения, которые могут быть выброшены, а для этого требуется потом как-то освобождать эту память, из чего проистекают идеи об использовании для механизма исключений подсчёта ссылок или о выделении памяти на стеке… Короче говоря, это большой клубок проблем. Сейчас появилось предложение по улучшению D Уолтера Брайта, которое призвано распутать этот клубок и сделать так, чтобы throw new Exception
работало без GC, когда это необходимо.
К сожалению, проблема использования исключений в @nogc
-коде не решена до сих пор. (прим. пер.)
Справиться с ограничениями @nogc main
— вполне выполнимая задача, она просто потребует немного мотивации и дисциплины.
Ещё одна вещь, которую стоит отметить: даже @nogc main
не исключает GC из программы полностью. D поддерживает статические конструкторы и деструкторы. Первые срабатывают перед входом в main
, а последние — после выхода из неё. Если они есть в коде и не обозначены как @nogc
, то, технически, выделение памяти через GC и сборка мусора могут происходить даже в @nogc
-программе. Тем не менее, атрибут @nogc
, применённый к main
, означает, что на протяжение работы main
сборка мусора запускаться не будет, так что по сути это то же самое, что не иметь никакого GC.
Добиваемся хорошего результата
Здесь я выскажу мнение. Существует широких спектр программ, которые можно написать на D, не отключая GC на время и не отказываясь от него полностью. Очень много можно добиться, минимизируя выделение памяти через GC и исключая его из горячих точек кода — и именно так и следует делать. Я не устаю это повторять, потому что часто неправильно понимают, как происходит сборка мусора в языке D: она может запуститься только когда программист выделяет память через GC и только когда это нужно. Используйте это знание с пользой, выделяя мало, редко и за пределами внутренних циклов.
В тех программах, где действительно нужен полный контроль, возможно, нет необходимости полностью отказываться от GC. Рассудительное использование @nogc
и/или API core.memory.GC
зачастую позволяет избежать любых проблем с производительностью. Не вешайте атрибут @nogc
на main
, повесьте его на функции, где точно нужно запретить выделение памяти через GC. Не вызывайте GC.disable
в начале программы. Вызывайте её перед критическим местом, а после него вызывайте GC.enable
. Сделайте так, чтобы GC собирал мусор в стратегических точках (например, между уровнями игры), при помощи GC.collect
.
Как и всегда в оптимизации производительности при разработке ПО, чем полнее вы понимаете, что происходит под капотом, тем лучше. Необдуманное использование API core.memory.GC
может заставить GC выполнять лишнюю работу или не оказать никакого эффекта. Для лучшего понимания внутренних процессов вы можете использовать тулчейн D.
В скомпилированную программу (не компилятор!) можно передать параметр рантайма D --DRT-gcopt=profile:1
, который поможет вам в тонкой настройке. Вы получите полезную информацию от профилировщика GC, такую как суммарное количество сборок мусора и суммарное время, затраченное на них.
В качестве примера: gcstat.d добавляет двадцать значений в динамический массив целых чисел.
void main() {
import std.stdio;
int[] ints;
foreach(i; 0 .. 20) {
ints ~= i;
}
writeln(ints);
}
Компиляция и запуск с параметром профилировщика GC:
dmd gcstat.d
gcstat --DRT-gcopt=profile:1
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
Number of collections: 1
Total GC prep time: 0 milliseconds
Total mark time: 0 milliseconds
Total sweep time: 0 milliseconds
Total page recovery time: 0 milliseconds
Max Pause Time: 0 milliseconds
Grand total GC time: 0 milliseconds
GC summary: 1 MB, 1 GC 0 ms, Pauses 0 ms < 0 ms
Отчёт сообщает об одной сборке мусора, которая, по всей вероятности, произошла во время выхода из программы. Рантайм D завершает работу GC на выходе, что (в текущей реализации) обычно вызовет сборку мусора. Это делается главным образом для того, чтобы запустить деструкторы собранных объектов, хотя D и не требует, чтобы деструкторы объектов под контролем GC когда-либо вызывались (это тема для одной из следующих статей).
DMD поддерживает параметр командной строки -vgc
, который отобразит каждое выделение памяти через GC в вашей программе — включая те, что спрятаны за возможностями языка, такими как оператор присоединения ~=
.
В качестве примера: взгляните на inner.d.
void printInts(int[] delegate() dg)
{
import std.stdio;
foreach(i; dg()) writeln(i);
}
void main() {
int[] ints;
auto makeInts() {
foreach(i; 0 .. 20) {
ints ~= i;
}
return ints;
}
printInts(&makeInts);
}
Здесь makeInts
— внутренняя функция. Указатель на нестатическую внутреннюю функцию является не указателем на функцию, а делегатом, то есть парным указателем на функцию/контекст (если внутренняя функция обозначена как static
, то вместо типа delegate
вы получаете тип function
). В этом конкретном случае делегат обращается к переменной в родительской области видимости.
Вот вывод компилятора с опцией -vgc
:
dmd -vgc inner.d
inner.d(11): vgc: operator ~= may cause GC allocation
inner.d(7): vgc: using closure causes GC allocation
(inner.d(11): vgc: оператор ~= может вызвать выделение памяти через GC)
(inner.d(7): vgc: использование замыкания может вызвать выделение памяти через GC)
Здесь мы видим, что нужно выделить память, чтобы делегат мог нести в себе состояние ints
, что делает его замыканием (которое не является отдельным типом — тип по-прежнему delegate
). Переместите объявление ints
внутрь области видимости makeInts
и скомпилируйте снова. Вы увидите, что выделение памяти из-за замыкания пропало. Ещё лучше изменить объявление printInts
таким образом:
void printInts(scope int[] delegate() dg)
Добавление scope
к параметру функции гарантирует, что никакие ссылки в этом параметре не могут выйти за пределы функции. Иначе говоря, становится невозможным присвоить делегат dg
глобальной переменной или вернуть его из функции. В результате отпадает необходимость создавать замыкание, так что никакого выделения памяти не будет. См. в документации об указателях на функции, делегатах и замыканиях, а также о классах хранения параметров функций.
Резюме
Учитывая, что GC языка D сильно отличается от того, что есть в языках вроде Java и C#, его производительность будет иметь другую характеристику. Кроме того, программы на D как правило производят гораздо меньше мусора, чем программы на языках вроде Java, где почти все типы имеет ссылочную семантику. Полезно это понимать, приступая к своему первому проекту на D. Стратегии, которые применяют опытные программисты на Java, чтобы уменьшить влияние GC на производительность, здесь вряд ли применимы.
Хотя определённо существует программы, где паузы на сборку мусора абсолютно неприемлемы, их, пожалуй, меньшинство. В большинстве проектов на D можно и нужно начинать с простых приёмов из пункта № 2 в начале статьи, а затем адаптировать код к использованию @nogc
и core.memory.GC
там, где требуется производительность. Параметры командной строки, представленные в этой статье, помогут найти места, где это может быть необходимо.
Чем больше времени проходит, тем проще становится управлять сборщиком мусора в программах на D. Идёт организованная работа над тем, чтобы сделать Phobos — стандартную библиотеку D — как можно более совместимой с @nogc
. Улучшения языка, такие как предложение Уолтера о выделении памяти под исключения, должны значительно ускорить этот процесс.
В будущих статьях мы рассмотрим, как выделять память, не прибегая к GC, и использовать её параллельно с памятью из GC, чем заменить недоступные в @nogc
-коде возможности языка и многое другое.
Спасибо Владимиру Пантелееву, Гильяму Пьола (Guillaume Piolat) и Стивену Швайхофферу (Steven Schveighoffer) за ценные отзывы о черновике этой статьи.
SpiderEkb
Выскажу крамольное мнение.
На мой взгляд, старик Оккам в гробу вертится сейчас.
Сначала мы выдумываем всякие smart pointers и вообще делаем все, чтобы отделить разработчика от контроля за выделением и освобождением памяти.
Потом мы придумываем различные стратегии сборки мусора, потому что разработчик уже сам освобождение используемой памяти не контролирует.
Потом мы боремся со сборщиками мусора чтобы они не тормозили процесс (ну хотя бы не тормозили его там, где это слишком сильно бросается в глаза и раздражает пользователя).
Не кажется все это странным? Ради чего вот это все вот?
bfDeveloper
Я писал на D физический симулятор реального времени для применения в играх. В нём не понадобилось отключать или как-либо особо задумываться над GC, всё просто работало. Предыдущая версия на C++ содержала свои коллекции и аллокаторы, была заметно сложнее в этом аспекте, но работала медленнее. Удобство написания кода позволяет больше времени уделять алгоритмам.
Это я к тому, что GC удобен, а ситуаций, где с ним надо что-то делать не так много. Обычный программист на C++ не пишет свои аллокаторы (разве что использует готовые простые решения), так же и обычно на D не надо вспоминать про особенности сборки мусора.
Весь цикл статей как раз про то, что не надо бояться GC, потому что вам его хватит, а если нет, то даже за его пределами есть жизнь.
SpiderEkb
А я занимался разработкой систем с гарантированным временем отклика. И часть кода сознательно писалась на чистом С (без всех этих коллекций и аллокаторов) потому что так все работало быстрее. А речь там шла о том, что нужно было некоторые вычисления уложить в очень короткие таймауты. И цена ошибки была не лаг в игре, а полный вылет протокола обмена с удаленным устройством. Т.е. это сразу рассматривалась как аварийная ситуация — нет ответа в течении таймаута — перепосылка. Несколько неудачных перепосылок — переинициализация соединения с посылкой аварийного сообщения «нет связи».
При всем при этом мы не могли себе позволить выставлять требования к железу как в современных играх — «террабайт оперативки и 256 ядер процессора». Все должно было работать на весьма скромном офисном компе (все это стабильно работало начиная с селеронов паруядерных и парой гигабайт оперативки).
Я к тому, что все проблемы разработчика сейчас решаются за счет потребителя. Разработчик не хочет уделять внимания эффективности кода, а потребитель платит за это бесконечным апгрейдом железа.
Бела в том, что разработчику все меньше и меньше приходится «включать мозг». Все основные алгоритмы тащатся в фреймворки. Работа с памятью прячется подальше. ЧТо остается на долю разработчика? Да ничего практически.
Сейчас вот работаю с мейнфреймами. Высоконагруженные системы. Так вот, проще взять человека без опыта и научит всему с нуля, чем «с опытом» новомодных языков — переучивать такого сложнее, а код, что они выдают… Там в подкорке привычка что кто-то как-то за тобой и мусор подберет и алгоритм подходящий уже где-то реализован…
bfDeveloper
Спору нет, hard realtime это точно не про GC. Но согласитесь, это очень маленькая ниша и там себя неплохо чувствует C. Там есть средства и время на разработку.
А вот с тем, что GC из D может приводить к требованию 256 ядер процессора для простой игры, я не соглашусь. Люди пишут говнокод независимо от языка и фреймвёрка. Если бы не было js или C# с Unity, в такой же среде даже на чистом C написали бы то же самое, такое же медленное, но дороже. Более того, я уверен, что при равных трудозатратах код на C будет медленнее кода на D и, возможно, C#. C быстр тогда, когда на него тратят силы, а когда тяп-ляп и в прод, будет ад.
И нет, это не разработчики обленились и не хотят «включать мозг». Это запрос рынка на тяп-ляп, js, electron… ой, у нас анимация мигания курсора проц съела. Если качество кода не ценится, то язык не поможет. Административные проблемы техническими средствами не решаются.
SpiderEkb
Ну… У нас не было жесткого реалтайма. Было гарантированное время отклика, это немножко другое.
Просто система была распределенной и были определенные протоколы обмена. Там очень много ограничений было связано с тем, что на другой стороне стоит однокристаллка с крайне ограниченными ресурсами. И надо было по факту приема от нее сообщения быстро его провалидировать и отправить квиток чтобы она его могла удалить, освободив память для новых событий. Не успел в таймаут — оно считает что сообщение не получено и перепосылает его снова.
Писать говонкод на С сложнее — он сразу дает по мозгам и падает. Можно написать коряво, но без ошибок. Ошибок С не прощает от слова совсем.
В целом тенденция идет к утяжелению софта. Всего. Когда-то я на 4-м пне под Win2000 с гигабайтом в голове себя чувствовал очень комфортно. Сейчас пот Win10 в голове 16 гиг и не скажу чтобы этого было много. А что толку? Ничего принципиально нового. А сейчас еще и сборка и отладка не на рабочей машине идет, а на сервере. Т.е. рабочая машина фактически терминалом работает. Куда память уходит? Да на всякие финтифлюшки которые по факту жизнь не сильно упрощают.
Кому-то было лен написать нормальную IDE — сваяли эклипс на жабе. Тяжеленная хреновнина, а еще периодически лагать начинает (привет сборщику мусора).
И альтернативы нет т.к. некоторые специфические для AS/400 (платформа под которую пишу) есть только в RDi (IDE на базе Eclipce для разработки под эту платфому). Ну что-то можно на VSC делать, но не все…
И не соглашусь что это чисто административные проблемы. Отчасти да. Но отчасти и потому что половина вчерашних школьников, кое-как освоивших таблицу умножения, ломится в «разработчики» потому что там хорошо платят. И всем находится место, увы…
richman5
дуракамне таким талантливым людям.SpiderEkb
Ну вот в целом все это не нравится мне. Во многих отношениях.
Скорее выкинуть на рынок сырой продукт чтобы застолбить место
Создавать монструальные фреймворки на все случаи жизни (а универсальность это всегда оверхед) с попыткой как можно больше скрыть от разработчика и оставить ему только «кубики» из которых лепится некая конструкция…
Все просчеты эффективности свалить на пользователя — «у вас комп слишком слабый».
Я не скажу что категорически против сборщиков мусора, но с моей точки зрения это некая крайняя мера, предохранитель в предаварийной ситуации. И сам факт что он сработал говорит об ошибках разработчика. Но за выделением и освобождением памяти разработчик должен следить сам.
bfDeveloper
Вот это выкидывание на рынок мне, да и не только мне, тоже не нравится. А точнее разработка под такой рынок. Но боюсь, это примерно того же порядка, что и «мне не нравится вода, потому что она сырая». Это же не заговор злых менеджеров по продажам, это объективные рыночные условия, так что приходится расслабляться и получать удовольствие.
А вот должен ли разработчик сам следить за памятью — это уже интересный технический вопрос. Тот же rust отлично показывает, что даже под жёстким контролем и гарантиями языка, человек не может точно управлять временем жизни объектов. Иначе там бы не появился счётчик ссылок. То есть есть ситуации, когда время жизни не очевидно и зависит от разных условий. И тогда это становится выбором между счётчиком и сборщиком. И оба не идеальны. Так что я бы не называл это ошибкой, скорее рекомендацией обойтись без автоматического управления, когда хватает ручного. В терминах C++ это использование uniq_ptr вместо shared_ptr, в D — @nogc, в C# struct, где это возможно.
Особенно ярко проблема заметна не на памяти, а файловых дескрипторах и прочих ресурсах, которые сборщик не трогает. Они должны управляться предельно просто и детерменировано. Но там и циклических ссылок нет.
richman5
AnthonyMikh
Или не сразу, а предварительно пройдя по памяти и попортив её.
0xd34df00d
C здесь является совершенно не обязательным.
Я тоже занимался чем-то похожим, и где нужно было не просто уложиться в таймауты (очень короткий таймаут у вас — это сколько, кстати?), а сделать быстрее, чем все остальные: ускорил систему на несколько наносекунд — получил выигрыш в деньгах. HFT — он такой, да.
И мы там спокойно использовали плюсы. Естественно, тоже без аллокаций в hot path, но вполне себе с коллекциями и темплейтами. Более того, темплейты позволяли достичь того, что на С вообще нереализуемо, кроме как внешними кодогенераторами (а зачем тогда писать на С?). Например, собираемый и проверяемый в компилтайме граф обработчиков данных.
SpiderEkb
Ну у нас попроще. Микросекунда на валидацию входящего сообщения и отправку квитка отправителю и еще пара-тройка микросекунд на обработку и раздачу конечным получателям. Но у нас было очень скромное железо (двухядерный селерон 1.7ГГц + 2Гб оперативки было нормой).
Темплейты в нашем случае не нужны — в основе протокола лежали датаграммы. Там по коду в заголовке в таблице выбирался обработчик и тело датаграммы уже передавалось ему, а он уже знает что с ней делать.
Коллекциями особо не пользовался — большинство алгоритмов писали сами под конкретную задачу с учетом ее специфики. И это работало быстрее чем любая коллекция общего назначения.
Что касается темплейтов… В 90% случаев прекрасно без них обходился. И сейчас пользуюсь очень редко.
Ogi Автор
Это не лень каких-то разработчиков, а требования рынка. Время, которое вы тратите на ручное управление памятью — это время, которое вы могли потратить на разработку новых фич. Поэтому и имеем то, что имеем.
С другой стороны, так ли это плохо? Мы можем сколько угодно смеяться над программами на Электроне, но я боюсь, что мире, в котором был бы только C — это мир, в котором бы просто не было многих программ, которыми мы каждый день пользуемся.
И потом, если для вас превыше всего производительность, то для кого-то на первом месте безопасность и корректность кода. И для них C — корень всех зол, а языки, выполняющиеся в виртуальной машине, — послание с небес.
0xd34df00d
И хотя я согласен с общим посылом, но вот здесь не соглашусь:
Факт наличия ВМ не имеет никакого отношения к корректности, да и безопасность там очень ограниченная. Тут нужны формально доказуемые языки.
Разрабатывать на них, впрочем, ещё дольше и тяжелее, чем на С.
SpiderEkb
Что есть «безопасность и корректность кода»? И как это соотносится с языком?
Есть языки, которые позволяют писать «безопасный и корректный код» левой ногой, не задумываясь, а на других нужно прикладывать некоторые мысленные усилия? Вы это имеете ввиду?
То, что в некоторых современных языках можно динамически создать объект и потом наплевать на него — сам как-нибудь уберется сборщиком мусора — это считаете нормальным? По мне так это раздолбайский подход к разработке. Из разряда «динамической типизации» — берем букву и используем, а там уж пусть сам компилятор догадается что это у нас переменная такая и придумает для нее наиболее подходящий тип. Зато разработчик сможет больше «о высоком» думать (например, где бы сегодня вечером пивка попить).
Все-таки, процесс разработки требует дисциплины внутренней. И если вы создаете объект, то должны сразу понимать где вы его будете использовать и когда он вам станет не нужен.
0xd34df00d
Ну, например, язык может позволять (или не позволять) выражать те или иные инварианты в типах, включая требования к логике функций.
Соответственно, если язык это позволяет, то написать доказуемо корректный (то есть, следующий спеке) код вы можете, а иначе — нет.
SpiderEkb
А в каком из языков есть принципиальные на то ограничения?
Ну вот как думаете, на каком языке написано микроядро QNX (да и все остальное, что вокруг этого микроядра крутится)? Которая, кстати, сертифицирована и по безопасности и по уровню недокументированных свойств и еще черт знает по чему и используется от автомобильных бортовых систем и до военных беспилотников и управлению АЭС.
Что там внутри? Java? Rust? Go?
И что на «безопасных языках» написано сопоставимое по уровню требований к безопасности и устойчивости?
Я к тому что разработка безопасного кода это в любом случае сложный и трудоемкий процесс. И начинается он с разработки архитектуры (и там не проходит модный нынче Agile — слишком многое надо продумывать заранее и потом следовать намеченному плану чтобы все не пошло вразбежку и не рухнуло под тяжестью придуманных по дороге фич). А язык… Это даже не вторичный вопрос.
Ну мне так кажется исходя из моего опыта. Могу ошибаться.
0xd34df00d
Проще сказать, в каких языках их нет. Идрис, агда, кок, вот это всё.
Тут надо заметить, что я в силу некоторых причин отдаю предпочтение internal verification. Есть тулзы для external verification, которые могут взять, например, подмножество кода на С и что-то для него вывести, но это на самом деле всего лишь подмножество.
Сертификация ? формальная верификация.
seL4, например. Там комбинация из Isabelle/HOL и хаскеля для верификации. Ну и, опять же, подмножество сишки для всяких совсем платформенных вещей, с доказательством корректности трансляции из/в С.
Как другой пример, я знаю места, где делают формально верифицированные распределённые вычисления на идрисе.