Pull to refresh

Comments 25

UFO just landed and posted this here
Добрый день. На самом деле — нет. Дело в том, что в C# на операциях создания-удаления куча банально быстрее (не в последнюю очередь из-за компактификации), к тому же, у нас копирование указателей — это вызов, а не одна операция копирования числа. Возможно, на каком-то коде ситуация будет иной, но наши C#-программисты оптимизируют свой код именно для .Net с GC, так что при переходе на плюсы чуда не происходит. Наши усилия обычно направлены на то, чтобы, наоборот, избежать проседания производительности.
Иронично, что раньше за проседания производительности ругали как раз системы со сборщиком мусора.
Это да. Тут ИМХО всё зависит от реализации. Наверняка те же алгоритмы, написанные изначально на C++ и оптмизированные под него же, будут бегать быстрее, чем версии, переведённые с C# и, скорее всего, быстрее, чем реализации на нативном C#. Но нам, увы, приходится транслировать код, а не алгоритмы, т. к. отделить одно от другого не получается.
UFO just landed and posted this here

Что удивительного в том, что хабаровчанам интереснее и актуальнее тема релокейта, чем плюсы? При всем уважении к последним, эпоха сейчас мягко намекает...

Проктостоматология актуальна не для всех

Не стоит сравнивать теплое с мягким.

Работа сделана большая.
Но, мне кажется в конце концов все равно вас не спасут разные типы ссылок, и вам прийдется создават сборщик мусора.
Вопрос — глядя на все эти проблемы, и кучу доп софта для решения этих проблем, потратив столько же ресурсов и времени смогли бы написать сборщик мусора?
ЗЫ: У меня был проект на дельфи orm. Мне очень понятно с чем вы столкнулись. После первых проблем сразу написали сборщик мусора и в дальнейшем это показало что это было самое правильное решение.
Спасибо за комментарии. У нас была обратная ситуация как раз с Bohem GC, было больше проблем, чем с умными указателями, и разработку в конце концов свернули. К сожалению, я этим направлением не занимался, не смогу подробнее рассказать, в чём там был затык.

Одна из принципиальных проблем с использованием сборщиков мусора в нашем случае — в том, что мы поставляем библиотеки, а не приложения. Произвольный клиентский код будет не слишком рад, если его вдруг начнут собирать.

Разве не получается при таком подходе, что из всего C# вам можно использовать подмножество C# 2 (недавно вышел C# 9)? Лямбды, LINQ, Span и так далее за бортом. Причём штуки вроде Span могли бы серьезно ускорить вашу библиотеку за счёт снижения давления на GC.


Как вы проверяете, что C++ версия не течёт? Задача выглядит нерешаемой… В C# есть гарантии со стороны рантайма и системы типов, после перевода в C++ все они превращаются в тыкву. Надеяться, что программисты на C# правильно расставят аннотации и что они будут «правильно» пользоваться объектами — разве это работает на практике?


Я не понимаю почему mono AOT для вас не работает. Но если есть какие-то причины, то можно поместить объектную модель в неуправляемую кучу, так чтобы C++ мог легко с ней работать. Модифицировать эту модель через методы реализованные на C#. Тогда вы не платите за маршалинг данных между C++/C#

Поддерживаемая версия C# у нас ограничена синтаксическим анализатором, который мы используем. Сейчас у нас там NRefactory (это 5-ая версия), после перехода на Roslyn (работа идёт) будет актуальная версия.

Лямбды транслируются в лямбды же, продление времени жизни переменных мы сейчас прикручиваем на этапе кодогенерации. LINQ транслируется, получается функционально эквивалентный код. Span в наших продуктах не использовался, но, если будет, я не вижу с первого взгляда особых проблем с тем, чтобы эмулировать его через aligned_storage. Из того, что мы пока не прожевали, на ум приходят yeild (хотя на уровне кодогенерации это можно сделать, просто не было настолько нужно) и виртуальные обобщённые функции (т. к. дженерики мы транслируем в шаблоны, виртуальность под запретом).

Проверка утечек на C++ делается циклическим запуском тестов через --gtest_repeat, API, не покрытого тестами, у нас нет, хотя могут оставаться отдельные ветки. Аннотации против утечек расставляют программисты C++ при подготвоке очередного релиза, они же проверяют потребление памяти.

По поводу использования mono — в прошлой статье я писал о том, что C++-версия делалась, в основном, по анналогии с Java-версией, для которой данный подход сработал примерно в то время, когда mono ещё был достаточно сырым фреймворком. Кроме того, непосредственный проброс API не избавит от необходимости решать задачи согласования с плюсовым кодом (обёртки над System.IO.Stream для использования с iostream, итераторы для IEnumerable, синтаксический сахар для работы со строками, и т. д.). Хотя, разумеется, в чём-то это сократило бы объём работы, но зато сделало бы использование продукта более сложным для клиентов.
Лямбды транслируются в лямбды же, продление времени жизни переменных мы сейчас прикручиваем на этапе кодогенерации.

То есть вы анализируете текст лямбды чтобы понять какие переменные она захватывает? Кажется тут могут возникать неоднозначные ситуации связанные с владением. А атрибут вам поставить некуда.

Атрибут можно поставить на метод, указав имя переменной/параметра/лямбды/номер строки/и т. д. в качестве аргумента атрибута. Хотя, конечно, не так красиво. А вообще, насколько я помню, задачу удалось решить в полностью автоматическом режиме, вынося все неоднозначные переменные на кучу, как это делает компилятор C#.
UFO just landed and posted this here
Если речь идет о C# и исключительно managed объектах, вы перестраховываетесь. GC корректно работает с циклическими ссылками. Пруф

Если вы имеете дело с unsafe в C#, взаимодействуете с нативным кодом через Interop или вообще пишете нативные приложения на том же C++, то да, там могут возникнуть проблемы с циклическими ссылками.
UFO just landed and posted this here

Возможно, я что-то упустил, но в названии статьи вставлено такое красивое слово "Модель памяти C++", но не вижу ничего про портирование синхронизации работы потоков, а ведь это большой кусок собственно модели памяти C++.
P.S. Про модель работы синхронизации в C# знаю мало, поэтому интересно посмотреть что там получается

Нет, в том, что касается синхронизации потоков, у нас особой работы проделано не было. Дело в том, что наши библиотеки по большей части однопоточные, т. к. предоставляют, в сновном, сервис по работе с файловыми форматами. Потребности отдельных потоков, которые занимаются обновлением кэшей или т. п., не выходят за рамки типовых реализаций Thread или ThreadPool. Тот же паттерн async-await мы пока не транслировали (т. к. он не поддерживается синтаксическим анализатором).

Не до конца понятно чем не подошел std::make_shared. Он как раз и предназначен для локализации памяти control block с памятью объекта. Ну и проблемы его тоже известны — пока хоть одна слабая ссылка будет удерживаться, вся память под объект и control block не вернется в систему.

У стандартного shared_ptr в нашем случае сразу несколько проблем. Во-первых, даже при использовании enable_shared_from_this нельзя дёргать shared_from_this на этапе работы конструктора (что для нас критично), в отличие от intrusive_ptr. Во-вторых, если бы было даже можно, то нельзя было бы манипулировать значением shared_count, чтобы избегать удаления конструируемого объекта (как описано выше в статье). В-третьих, проблемы с конверсией this в shared_ptr — при наследовании Object от enable_shared_from_this придётся каждый раз делать dynamic_pointer_cast во всех дочерних классах, что существенно замедляет работу. Привожу ниже код, который демонстрирует, как выглядел бы пример из раздела «Вид умных указателей» в семантике make_shared — можно легко убедиться, что он не работает.

#include <iostream>
#include <memory>
using namespace std;
class Object : public enable_shared_from_this<Object>
{
public:
    Object()
    {}
    virtual ~Object()
    {}
};
class Node;
class Document : virtual public Object
{
    shared_ptr<Node> m_root;
public:
    virtual ~Document()
    {}
    Document()
    {
        m_root = make_shared<Node>(dynamic_pointer_cast<Document>(shared_from_this()));
    }
};
class Node : virtual public Object
{
    weak_ptr<Document> m_document;
public:
    virtual ~Node()
    {}
    Node(shared_ptr<Document> document)
    {
        m_document = document;
    }
};
int main(int, char*[])
{
    auto doc = make_shared<Document>();
    return 0;
}

В данном конкретном случае все решается ленивыми свойствами.


class Document : virtual public Object
{
    shared_ptr<Node> m_root;
public:
    virtual ~Document()
    {}
    Document()
    {
        m_root = make_shared<Node>(dynamic_pointer_cast<Document>(shared_from_this()));
    }
    shared_ptr<Node> GetRoot()
    {
        if (!m_root)
        {
            m_root = make_shared<Node>(dynamic_pointer_cast<Document>(shared_from_this()));
        }
        return m_root;
    }
};

Но, понятно, что это не панацея.

А главное — не очень понятно, кто и по какому принципу должен решать, когда то, что в шарповом коде было обычным полем, вдруг станет ленивым свойством. Сложность анализа, навскидку, зашкалит.
Sign up to leave a comment.

Articles