Большинство тестов языков программирования — это синтетика. Мы сравниваем скорость сортировки массивов, подсчёт аллокаций и другие упражнения, которые почти не встречаются в реальной жизни.
А вот настоящие приложения — редакторы, игры, GUI-интерфейсы, базы данных — живут совсем в другом мире. Они управляют не сырыми числами, а сложными структурами объектов. И если присмотреться, почти все эти структуры сводятся к одной универсальной модели: DOM-подобному графу.
Всё сводится к DOM-у
Откройте любое современное приложение — и вы увидите знакомые сущности и связи.
Игровой движок: сцена, акторы, компоненты иерархически вложенные и перекрестно связанные.
UI-фреймворк: контролы внутри контейнеров, окон, панелей — иерархия и связи.
Редактор документов: страницы, стили, текстовые блоки, абзацы.
MVC, MVVM и прочие архитектуры: модели, представления, контроллеры разложенные по контейнерам и подписанные друг на друга.
Базы данных: записи и документы, вложенные в таблицы и коллекции и связанные ключами.
Разные области, разные названия, но структура одна и та же.
Универсальная форма данных
Если убрать детали, всё сводится к трём уровням:
Дерево владения
Каждый объект принадлежит своему владельцу.
Актор принадлежит игровой сцене, абзац принадлежит тексту, кнопка — форме.
Как узлы в XML или JSON, это дерево определяет, кто контролирует чье время жизни, кто кого удаляет.

Сеть перекрестных ссылок
Реальный мир живет не только деревом. Кнопка ссылается на другую форму. GUI-окно хранит ссылку на сфокусированный элемент. Паттерн издатель-подписка весь построен на слабых перекрестных ссылках между произвольными компонентами. Все эти связи не должны владеть объектами и обязаны безопасно разрываться при их удалении.

DAG неизменяемых ресурсов
Есть еще то, что используется всеми — строки, стили, текстуры, меши, шрифты. Они неизменяемы и переиспользуются без копирования. Они неизменяемы и потому их расшаривание между иерархиями объектов вообще никак не контролируется.
Менять их можно, но только через явное дублирование — классический copy-on-write.

Вот и всё: дерево, сеть и DAG. Эта тройка лежит в основе практически любой организации данных современного приложения.
Как современные языки справляются с этими структурами?
Универсальное тестовое задание
Чтобы сравнить языки честно, нужно одно общее задание. Берём упрощенное приложение — редактор карточек. Вот таких:

Модель:
Документ содержит несколько карточек.
-
Каждая карточка хранит список элементов:
текстовые блоки — со стилями;
картинки — хранящие и рисующие битмапки;
автоматические коннекторы со ссылками на другие элементы;
кнопки, ведущие на другие карточки;
группы элементов (вложенные контейнеры).
В будущем мы планируем добавлять новые типы элементов карточек.
Вышеприведенные карточки в памяти выглядят так:

Пример возможной структуры классов:

Какие операции поддерживаем:
Создание, копирование, удаление документов, карточек в документах и элементов в карточках.
Изменение любых полей любых вышеперечисленных объектов.
Для текстовых стилей - копирование, модификация, перевод некоторых текстовых блоков на модифицированный стиль.
Проход по перекрестным ссылкам с проверкой на не-оторванность.
Удаление карточки из метода элемента этой карточки.
Правила:
Документы, карточки и их элементы должны быть изменяемыми.
Стили и битмапы — неизменяемые ресурсы, разделяемые между текстовыми блоками и картинками.
Любое их изменение должно выполняться через copy-on-write.Удаление любых объектов из иерархии не должно вызывать сбоев — даже если где-то в стеке остались ссылки на них.
Перекрестные ссылки из коннекторов к другим элементам и от кнопок к карточкам должны автоматически обрываться при удалении любого из двух объектов (кто ссылается - на кого ссылаются).
Обращение по оборванной перекрестной ссылке должно контролироваться и пресекаться, но без падения программы.
Копирование карточек и любых их элементов обязано сохранять структуру связей. Например, если копируется карточка, то все коннекторы в копии должны ссылаться на копии оригинальных элементов.
Контроль циклов — группу элементов нельзя вставлять в себя, и в свои подгруппы.
Контроль единственности владельца. Карточка не может быть вставлена в несколько документов одновременно и не может быть вставлена в один и тот же документ несколько раз. Аналогично с элементами карточек — они должны иметь строго одного владельца — или карточку или группу.
Почему это лучший тест для языка
Если язык умеет описывать такую модель — он готов к реальной разработке. Если нет — никакая скорость и сборщик мусора не помогут.
DOM-подобные структуры проверяют всё сразу:
Может ли язык безопасно управлять временем жизни объектов?
Поддерживает ли разделяемые ресурсы без утечек?
Работают ли слабые ссылки или показывают на мусор?
Можно ли копировать объекты, сохраняя топологию связей?
Устойчиво ли приложение при произвольных изменениях?
Это не лабораторный эксперимент — это реальная модель поведения живой программы.
Что оцениваем
Критерий |
Вопрос |
Безопасность памяти |
Возможны ли висячие ссылки и краши? |
Предотвращение утечек |
Всё ли освобождается корректно? |
Ясность владения |
Понятно ли, кто чем владеет и когда? |
Копирование |
Сохраняется ли структура связей? |
Слабые ссылки |
Автоматически ли инвалидируются? |
Устойчивость |
Переживает ли система произвольные изменения? |
Выразительность |
Можно ли это описать без боли и избыточности? |
Момент обнаружения ошибок |
Поймаются ли циклы владения, множественное владение, попытка изменения расшаренного ресурса во время компиляции или только при запуске. |
Почему это важно
Обычно мы оцениваем языки по скорости вычислений или потребляемой памяти. Но скорость — это не проблема XXI века.
Настоящая сложность — управление живым состоянием, когда в памяти крутятся тысячи взаимосвязанных объектов, а приложение всё ещё должно оставаться стабильным.
DOM-подобные структуры — не про веб и не про XML. Это универсальная форма данных, через которую можно описать почти любую программу.
DOM бенчмарк заставляет язык показать, как он справляется с этой задачей: умеет ли он сочетать владение, разделение и ссылки без хаоса.
Если умеет — это язык, на котором можно писать реальные системы. Если нет — всё остальное не имеет значения.
Что дальше
В следующей части я протестирую на этом бенчмарке С++ и JavaScript, как примеры языков с подсчетом ссылок+RAII и со сборщиком мусора. Я прошу всех неравнодушных поучаствовать - показать реализацию этого бенчмарка на вашем любимом языке программирования. Особо приглашаются любители Раста - это ваш шанс показать насколько Rust заточен для реальных задач.
Комментарии (16)

knight_of_light_1
10.10.2025 04:22Очередная нейростатья, где афтор поленился даже глупые llm-паттерны убрать, все эти сравнения, метафоры — по-любому это писал живой человек)

kotan-11 Автор
10.10.2025 04:22"Очередная нейростатья...по-любому это писал живой человек". Штоэ?
А можно пример глупого llm-паттерна? Мне просто интересно, какая часть картинок, которые делались вручную, и текстов, которые, я писал из головы, хотя и проверял граматику чат-ботом, какая часть материала вам кажется нейросетевой?

TimurZhoraev
10.10.2025 04:22Технически это выражается в подсчёте ссылок или составления обычного линейного массива указателей на объекты с детектированием не высвобожденных сущностей, путём введения туда флажков (ценой увеличения памяти). Включая флажки синхронизации и детектирования гонки данных. Другое дело что это поддерживается самим языком из коробки. Но таких языков где явно можно управлять виртуальными функциями, скрытыми атрибутами классов, исключения по доступу вне границ специально выделенной кучи, вроде как нет. Железобетонно и в явном виде это может делать только подобный С.

Dhwtj
10.10.2025 04:22Поскольку, требований к сложности нет, а упор в основном на скорость и безопасность, то Rust лидер.
GC языки будут тормозить, особенно в 95 перцентиле. С++ требует высокой дисциплины, не страхует от ошибок
Особо приглашаются любители Раста - это ваш шанс показать насколько Rust заточен для реальных задач.
Привет

rsashka
10.10.2025 04:22Ваш хваленый Rust не решает проблему циклический зависимостей и множественного владения.
Пока.

Dhwtj
10.10.2025 04:22Множественное владение это не проблема а ошибка дизайна.
Циклические зависимости не появляются, если использовать слабые ссылки, которые не мешают удалить элемент
Классический пример: дерево. Родитель владеет детьми (Rc<Node>), а дети лишь ссылаются на родителя (Weak<Node>).
Живите теперь с этим

rsashka
10.10.2025 04:22Множественное владение может быть требованием задачи (как в текущем случае), и если язык не может это реализовать, то это проблема дизайна языка программирования.
Живите теперь с этим :--)

shai_hulud
10.10.2025 04:22Реализация не может быть требованием. Я даже удивлен, что это надо писать буквами в комментарий.

rsashka
10.10.2025 04:22Я даже удивлен, что это надо писать буквами в комментарий.
Та и не писал бы. Все равно не понятно, к чему относится ваш комментарий, либо вы отвечаете не тому человеку.

Dhwtj
10.10.2025 04:22Я даже знаю такие языки где на уровне языка это решается. Целых джва. Go, PHP.
Появились дедлоки или утечка памяти - прихлопнуть процесс. Делов то. Бггг
PHP: max_execution_time истёк — убили скрипт, пользователь пусть обновит страницу.
Go: дедлок детектор в рантайме паникует и роняет всю программу / микросервис а супервайзер рестартит. "Лучше упасть, чем зависнуть".
Это как "решение" проблемы с памятью через перезагрузку сервера каждую ночь. Работает? Да. Решение? Ну такое.

kotan-11 Автор
10.10.2025 04:22Rust лидер
Вполне может быть. Однако, чтобы доказать это,все равно придется показать реализацию этого бенчмарка на Rust и проанализировать его на соответствие критериям.

Dhwtj
10.10.2025 04:22Минимальный набор тестов (описания)
1. Удаление карточки обрывает перекрёстные ссылки
Сценарий: Создать документ с двумя карточками. В первой карточке разместить кнопку, ссылающуюся на вторую карточку. Удалить вторую карточку.
Ожидание: Обращение к цели кнопки возвращает «ссылка недоступна» (например,
nullилиNone). Программа не падает.
2. Глубокое копирование сохраняет внутренние связи
Сценарий: Создать карточку, содержащую текстовый блок и коннектор, ссылающийся на этот блок. Сделать глубокую копию карточки.
Ожидание: В копии коннектор указывает на копию текстового блока (а не на оригинал). Изменение текста в оригинале не влияет на копию.
3. Copy-on-write для стилей
Сценарий: Создать два текстовых блока с одним и тем же стилем. Изменить стиль одного из них (например, размер шрифта).
Ожидание: Стиль второго блока остаётся неизменным. Это подтверждает, что ресурсы разделяются до момента изменения.
4. Запрет циклической вложенности
Сценарий: Создать группу элементов. Попытаться вставить эту группу в саму себя (напрямую или через промежуточную подгруппу).
Ожидание: Операция отклоняется (ошибка, исключение или возврат
false). Структура остаётся целостной.
5. Безопасное удаление изнутри элемента
Сценарий: Создать карточку с элементом, у которого есть метод (например, «обработчик клика»), вызывающий удаление своей собственной карточки из документа. Вызвать этот метод.
Ожидание: Карточка успешно удаляется. Программа не падает, нет use-after-free или dangling pointers.
6. Единственность владельца
Сценарий: Создать карточку и попытаться добавить её в два разных документа (или дважды в один).
Ожидание: Вторая попытка добавления отклоняется. Аналогично для элементов: нельзя вставить один и тот же элемент в две разные карточки или группы.
Почему этого достаточно?
Эти 6 тестов покрывают все три слоя модели из ТЗ:
Дерево владения тесты 4, 6
Сеть перекрёстных ссылок тесты 1, 2, 5
DAG (ориентированный ациклический граф) неизменяемых ресурсов тест 3
Так? Это соответствует табличке

Dhwtj
10.10.2025 04:22use std::rc::{Rc, Weak}; use std::cell::RefCell; // === Ошибки === #[derive(Debug, PartialEq)] pub enum DomError { AlreadyHasOwner, WouldCreateCycle, InvalidOperation, // для прочих недопустимых действий } // === Style и Bitmap неизменяемы и безопасны) === #[derive(Clone, Debug, PartialEq)] pub struct Style { /* ... */ } impl Style { pub fn new(font_size: i32, color: String) -> Self; pub fn with_font_size(&self, new_size: i32) -> Self; pub fn with_color(&self, new_color: String) -> Self; } #[derive(Clone, Debug)] pub struct Bitmap { /* ... */ } impl Bitmap { pub fn new(width: u32, height: u32, data_hash: u64) -> Self; } // === Element === pub type ElementRef = Rc<RefCell<Element>>; #[derive(Debug)] pub enum Element { Text { content: String, style: Rc<Style> }, Image { bitmap: Rc<Bitmap> }, Connector { target: Weak<RefCell<Element>> }, Button { target_card: Weak<RefCell<Card>> }, Group { children: Vec<ElementRef> }, } impl Element { pub fn text(content: String, style: Rc<Style>) -> Self; pub fn image(bitmap: Rc<Bitmap>) -> Self; pub fn connector() -> Self; pub fn button() -> Self; pub fn group() -> Self; pub fn deep_clone(&self) -> ElementRef; // Работа со ссылками — всегда безопасна, ошибок нет pub fn set_target(&mut self, target: &ElementRef); pub fn get_target(&self) -> Option<ElementRef>; pub fn set_target_card(&mut self, card: &Rc<RefCell<Card>>); pub fn get_target_card(&self) -> Option<Rc<RefCell<Card>>>; // Добавление в группу — может завершиться ошибкой pub fn add_child(&mut self, child: ElementRef) -> Result<(), DomError>; } // === Card === #[derive(Debug)] pub struct Card { pub elements: Vec<ElementRef>, has_owner: bool, } impl Card { pub fn new() -> Self; pub fn add_element(&mut self, element: ElementRef) -> Result<(), DomError>; pub fn deep_clone(&self) -> Rc<RefCell<Card>>; } // === Document === #[derive(Debug)] pub struct Document { pub cards: Vec<Rc<RefCell<Card>>>, } impl Document { pub fn new() -> Self; pub fn add_card(&mut self) -> Rc<RefCell<Card>>; // всегда успешно pub fn remove_card(&mut self, card: &Rc<RefCell<Card>>); // всегда успешно }Тесты
#[cfg(test)] mod tests { use super::*; #[test] fn test_button_target_becomes_none_after_card_deletion() { let mut doc = Document::new(); let card1 = doc.add_card(); let card2 = doc.add_card(); let button = Rc::new(RefCell::new(Element::button())); card1.borrow_mut().add_element(button.clone()).unwrap(); // Устанавливаем ссылку на card2 button.borrow_mut().set_target_card(&card2); // Удаляем card2 doc.remove_card(&card2); // Проверяем, что ссылка стала недоступна assert!(button.borrow().get_target_card().is_none()); } #[test] fn test_deep_clone_preserves_internal_references() { let mut doc = Document::new(); let card = doc.add_card(); let text = Rc::new(RefCell::new(Element::text( "Hello".to_string(), Rc::new(Style::new(12, "#000000".to_string())), ))); let connector = Rc::new(RefCell::new(Element::connector())); card.borrow_mut().add_element(text.clone()).unwrap(); card.borrow_mut().add_element(connector.clone()).unwrap(); connector.borrow_mut().set_target(&text); // Копируем карточку let cloned_card = card.borrow().deep_clone(); // Извлекаем копии let cloned_elements = &cloned_card.borrow().elements; let cloned_text = cloned_elements[0].clone(); let cloned_connector = cloned_elements[1].clone(); // Проверяем: коннектор ссылается на копию текста let target = cloned_connector.borrow().get_target().unwrap(); assert_eq!( target.borrow().as_text().unwrap().content, "Hello" ); // Проверяем, что это разные объекты assert_ne!( Rc::as_ptr(&text), Rc::as_ptr(&cloned_text) ); } #[test] fn test_style_copy_on_write() { let base_style = Rc::new(Style::new(12, "#000000".to_string())); let text1 = Rc::new(RefCell::new(Element::text("A".to_string(), base_style.clone()))); let text2 = Rc::new(RefCell::new(Element::text("B".to_string(), base_style.clone()))); // Меняем стиль у text1 let new_style = base_style.with_font_size(16); text1.borrow_mut().as_text_mut().unwrap().style = Rc::new(new_style); // text2 должен остаться с оригинальным стилем assert_eq!(text1.borrow().as_text().unwrap().style.font_size, 16); assert_eq!(text2.borrow().as_text().unwrap().style.font_size, 12); } #[test] fn test_group_cannot_be_inserted_into_itself() { let group = Rc::new(RefCell::new(Element::group())); let result = group.borrow_mut().add_child(group.clone()); assert_eq!(result, Err(DomError::WouldCreateCycle)); } #[test] fn test_safe_deletion_from_within_element_callback() { let mut doc = Document::new(); let card = doc.add_card(); // Имитируем "обработчик", который удаляет карточку let doc_ref = Rc::new(RefCell::new(doc)); let card_ref = card.clone(); let doc_weak = Rc::downgrade(&doc_ref); // Создаём элемент с замыканием (в реальности — через поле, здесь упрощённо) let _element = Rc::new(RefCell::new(Element::text( "Click me".to_string(), Rc::new(Style::new(12, "#000000".to_string())), ))); // Выполняем удаление "изнутри" { let mut doc_strong = doc_weak.upgrade().unwrap(); doc_strong.borrow_mut().remove_card(&card_ref); } // После удаления карточка больше не в документе // (проверка через подсчёт — в реальной реализации можно добавить is_removed флаг) // Главное: не упало } #[test] fn test_element_cannot_be_added_to_multiple_groups() { let elem = Rc::new(RefCell::new(Element::text( "Shared".to_string(), Rc::new(Style::new(12, "#000000".to_string())), ))); let group1 = Rc::new(RefCell::new(Element::group())); let group2 = Rc::new(RefCell::new(Element::group())); // Первое добавление — успешно assert!(group1.borrow_mut().add_child(elem.clone()).is_ok()); // Второе — ошибка assert_eq!( group2.borrow_mut().add_child(elem.clone()), Err(DomError::AlreadyHasOwner) ); } }Ну, а реализация как нибудь на неделе )

kotan-11 Автор
10.10.2025 04:22Спасибо. Пекрасное переложение текста в тесты и прекрасный Раст-код (декларации). Очень профессионально и четко.
Не то, чтобы я не согласен вот с этим решением, но у меня есть несколько вопросов:
pub enum Element { Text { content: String, style: Rc<Style> }, Image { bitmap: Rc<Bitmap> }, Connector { target: Weak<RefCell<Element>> }, Button { target_card: Weak<RefCell<Card>> }, Group { children: Vec<ElementRef> }, }Мне кажется, что этот подход:
Усложнит в будущем архитектеру, потоу что
В будущем мы планируем добавлять новые типы элементов карточек. А при использованииenum-а, нам придется вписывать куски кода для их поддержки в десятки мест, вместо добавления нового типа данных в отдельном модуле, если использовать полиморфные типы данных.Может ударить по памяти, если один из элементов
enum-а будет иметь слишком много прожорливых по памяти атрибутов.Не позволяет адекватно моделировать ограничения предметной области с помощью системы типов, например
Element::set_target_cardприменим только к кнопкам, но может быть вызван у любогоElement-а, нарушение этого правила не поймается на этапе компиляции.
Может быть стоит сделать полиморфизм через
dyn CardItem?В любом случае, каким бы ни было ваше решение,
спасибо за очень профессональный код и подход.PS. Это не должно потеряться в виде 100-500-го комментария. Сделайте это отдельной статьёй.
Xexa
Т.е к аллокации(выделение и распределение памяти) и сортировке(найти/вставить) вернулись с этим "домом"