Компилируемые в реальном
времени DSL для С++
Юрий Ефимочев
О себе
Архитектор в LogicNow
Специализация: высоконагруженные
отказоустойчивые системы на C++
Бэкап-решение
Что такое DSL?
Domain-specific language - это язык программирования с
ограниченными возможностями, ориентированный на конкретную
предметную область.
Достоинства и недостатки DSL
Достоинства:
• управление сложностью
• скорость разработки
• комуникация с экспертами
• альтернативные парадигмы
• динамическое выполнение
Недостатки:
• порог вхождения
• эволюция в язык общего назначения
• разработка и поддержка
Классификация DSL
• Внутренние
• Внешние
• Визуальные
• Интерпретируемые
• Компилируемые
Пример: тестирование
Given(
Node("a")
(Node("b")
(Node("c"))
(Node("d")))
(Node("e")
(Node("f"))
(Node("g"))));
When().Filter("%f%");
Expect(
Node("a")
(Node("e")
(Node("f"))));
a
b
c d
e
f g
a
e
f
Архитектура DSL
Программа
на
DSL
Семантическая
модель
Целевой
код
Опционально
Постановка задачи
Дано:
~ 100 000 объектов
~ 500 параметров у объекта
Требуется:
получение актуальной статистики по произвольной
выборке объектов
Примеры запросов
Count(true)
Count(LastSession.Timestamp < 2.days().ago())
Count(Version like ‘15.12.%’)
Count(LastSession.Status == SessionStatus::Failed)
Average(LastSession.Duration)
Sum(UsedStorage)
Архитектура решения
Expression AST Result
Data
Синтаксический анализ
Инструментарий:
• flex & bison
• antlr
• boost::spirit
Синтаксический анализ
%start Start;
AtomicExpression:
IntegerNumber { $$.Value = m_builder.CreateInteger($1.Name); } |
Identifier { $$.Value = m_builder.CreateVariable($1.Name) };
AddSubExpression:
AtomicExpression |
AddSubExpression Plus AddSubExpression { m_builder.CreateAddNode(...);
} |
AddSubExpression Minus AddSubExpression { m_builder.CreateSubNode(...);
};
Start:
AddSubExpression { result.AstTree.reset($1.Value); };
AST
Count(time > 2.days().ago()) Count
time ago
days
2
>
2.days().ago() = now - 2.days()
2.days() = 2 * 86400
AST
Count(time > 2.days().ago()) Count
time -
*
86400
>
now
2.days().ago() = now - 2.days()
2.days() = 2 * 86400
2
Реализация объектов AST
class IntegerNode : public IAstNode
{
public:
IntegerNode(int const value) :
m_value(value)
{
}
private:
virtual Variant Compile(IDataProvider& /*dataProvider*/) const
{
return Variant(m_value);
}
int const m_value;
};
Реализация объектов AST
class VariableNode : public IAstNode
{
public:
VariableNode(std::string const& name) :
m_name(name)
{
}
private:
virtual Variant Compile(IDataProvider& dataProvider) const
{
return Variant(dataProvider.GetVariableValue(m_name));
}
private:
std::string const m_name;
};
Реализация объектов AST
class AddNode : public IAstNode
{
public:
AddNode(IAstNodePtr left, IAstNodePtr right) :
m_left(std::move(left)),
m_right(std::move(right))
{
}
private:
virtual Variant Compile(IDataProvider& dataProvider) const
{
return m_left->Compile(dataProvider) + m_right->Compile(dataProvider);
}
private:
IAstNodePtr m_left;
IAstNodePtr m_right;
};
AST
Count(time > 2.days().ago()) Count
time -
*
86400
>
now
2.days().ago() = now - 2.days()
2.days() = 2 * 86400
2
Архитектура LLVM
Что такое LLVM IR?
Особенности:
• обобщенная система команд
• функции
• типы данных
• простые типы
• указатели
• массивы
• статическая типизация
• SSA(static single assignment) нотация
LLVM IR
bool MoveNext();
int GetValue();
int main()
{
int sum = 0;
while (MoveNext())
{
sum += GetValue();
}
return sum;
}
1 declare i1 @move_next()
2 declare i64 @get_value()
3
4 define i64 @main()
5 {
6 entry:
7 br label %loop_condition
8
9 loop_condition:
10 %1 = phi i64 [ 0, %entry ], [ %4, %loop_body ]
11 %2 = call i1 @move_next()
12 br i1 %2, label %loop_body, label %loop_exit
13
14 loop_body:
15 %3 = call i64 @get_value()
16 %4 = add i64 %3, %1
17 br label %loop_condition
18
19 loop_exit:
20 ret i64 %1
21 }
Архитектура решения
Expression AST
LLVM
IR AST
Native
code
Data
descriptor
Data
provider
Result
Data
Реализация AST
class IntegerNode : public IAstNode
{
public:
IntegerNode(int const value) :
m_value(value)
{
}
private:
virtual CompiledNode Compile(IDataProvider& context) const
{
return CompiledNode(DataType::Integer, context.GetBuilder().getInt64(m_value));
}
int const m_value;
};
Реализация AST
class VariableNode : public IAstNode
{
public:
VariableNode(std::string const& name) :
m_name(name)
{
}
private:
virtual CompiledNode Compile(ICompilationContext& context) const
{
return context.GetVariable(m_name);
}
private:
std::string const m_name;
};
Реализация AST
class AddNode : public IAstNode
{
public:
AddNode(IAstNodePtr left, IAstNodePtr right) :
m_left(std::move(left)),
m_right(std::move(right))
{
}
private:
virtual CompiledNode Compile(ICompilationContext& context) const
{
CompiledNode left = m_left->Compile(context);
CompiledNode right = m_right->Compile(context);
DataType::Enum const effectiveType = GetEffectiveType(left, right);
return CompiledNode(effectiveType, context.GetBuilder().CreateAdd(left, right));
}
private:
IAstNodePtr m_left;
IAstNodePtr m_right;
};
Производительность
Interpreted 1734 мс
LLVM 31 мс
LLVM(precompiled) 28 мс
Native 21 мс
Count(x < 50 && x > 22 || x == 77), size = 1000000
Кодогенерация
Инструментарий:
• LLVM
• Lua
• Chrome V8
Производительность
Interpreted 1734 мс
Lua 309 мс
Lua(precompiled) 276 мс
LLVM 31 мс
LLVM(precompiled) 28 мс
Native 21 мс
Count(x < 50 && x > 22 || x == 77), size = 1000000
Итоги
Плюсы:
• простота
• скорость разработки
• Производительность
Минусы:
• размер бинарного файла (~ 15 Mb)
• реализация местами сложна
• нативный код
?

Dsl for c++

Editor's Notes

  • #4 ограниченные возможности - простота ограниченный язык полезен только если ориентирован на конкретную предметную область, иначе смысла нет неполнота по тьюрингу примеры: regexp, sql, cmake, make области применения: тесты, конфигурация, администрирование, финансы
  • #5 сложность - основная проблема разработки ПО(новые языки, фреймворки, ide) плюсы управление сложностью - инкапсуляция, проблема + реализация скорость разработки - проще понять, меньше ошибок, легче поддерживать комуникация с экспертами - не писать, так читать изменение в процессе выполнения - динамически меняющиеся области(финансы) альтернативные парадигмы - императивные, декларативные, функциональные без необходимости использования по всему проекту, интерприт минусы
  • #6 внутренний - специфический способ использования языка общего назначения(подмножество возможностей, определенный стиль, работа с небольшим аспектом системы) отличае от API связь методов(метод имеет смысл в контексте другого) совместимость - сгенерить код на С и заюзать в контроллере(на этапе компиляции) компилируемые - генерация, доп шаг при компиляции, совместимость
  • #8 первый шаг - синтаксический анализ второй шаг - генерация(опционален) синтаксис - описывает корректные выражения, которые могут встерчаться в программе семантика - это смысл программы строго говоря семантическая модель не обязательна, но очень желательна семантическая модель отделяет синтаксически анализ от результирующей семантики менять синтаксис независимо от логики, синтаксический сахар, заменить внутренний dsl на внешний dsl - тонкий слой над семантической моделью роль dsl это инициализация семантической модели с семантической моделью можно работать и без dsl семантическая модель не обязательно AST
  • #9 коллекция иерархическая 150К это максимум
  • #10 2 части: аггрегирующая функция и выражение выражение применяется к каждому объекту
  • #11 LLVM IR AST - оптимимизация
  • #12 flex&bison граматика LARL проверена временем 80-ые, но до сих пор развивается работает быстро, парсера маленький по размеру сложно дебажить, не всегда понятные ошибки код который генерится не идеален(сырые указатели, union) antlr граматика LL antlrworks среда для разработки(дебаг, автрозрешение конфликтов, аст) ограниченная поддержка C++(4-ая версия не поддерживает, 3-яя поддерживает но ограниченно(безполезно)) парсер более требователен к ресурсам(больше памяти) spirit прикольная идея, построен на с++ шаблонах header-only либа, не требует доп шагов при компиляции адские ошибки для реальных грамматик огромное время компилляции
  • #14 yзлы - операторы листья - операнды отстутствуют узлы и ребра тех синтаксических правил, которые не влияют на семантику программы(например скобки, группировка операндов задается струтурой дерева) синтаксис описывает корректные выражения семантика - смысл
  • #15 yзлы - операторы листья - операнды отстутствуют узлы и ребра тех синтаксических правил, которые не влияют на семантику программы(например скобки, группировка операндов задается струтурой дерева) синтаксис описывает корректные выражения семантика - смысл
  • #19 yзлы - операторы листья - операнды отстутствуют узлы и ребра тех синтаксических правил, которые не влияют на семантику программы(например скобки, группировка операндов задается струтурой дерева) синтаксис описывает корректные выражения семантика - смысл
  • #21 разные языки, разные модели памяти, разные архитекруры, разные наборы инструкций ssa - функциональный стиль описания данных, императивный стиль описания операций нет переменных
  • #23 LLVM IR AST - оптимимизация
  • #30 нативный код - возможны крэши, мэмори ликик, ограниченные платформы и тп