Компилируемые в реальном
времени DSL для С++
Юрий Ефимочев
О себе
Архитектор в LogicNow
Специализация: высоконагруженные
отказоустойчивые системы на C++
Бэкап-решение
Что такое DSL?
Domain-specific language - это язык программирования с
ограниченными возможностями, ориентированный на конкретную
предметную область.
Плюсы DSL
➢ управление сложностью
➢ скорость разработки
➢ комуникация с экспертами
➢ альтернативные парадигмы
➢ динамическое выполнение
Минусы DSL
➢ порог вхождения
➢ эволюция в язык общего
назначения
➢ разработка и поддержка
Классификация 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
Программа
на
DSL
Семантическая
модель
Целевой
код
Опционально
Архитектура бэкап-решения
Cloud
storage
Support
Integration
Customers
Development
Sales
Finances
Backup
client
Backup
client
Backup
client
Backup
client
~ 100 000 клиентов
~ 500 параметров у каждого
Примеры запросов
Count(true)
Sum(UsedStorage)
Count(LastSession.Timestamp < 2.days().ago())
Count(LastSession.Status == SessionStatus::Failed)
Average(LastSession.Duration)
Архитектура решения
Expression AST Result
Data
Синтаксический анализ
Flex & Bison
Antlr
boost::Spirit
Bison: грамматика
%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
>
x.ago() = now - x
AST
Count(time > 2.days().ago()) Count
time -
days
2
>
x.ago() = now - x
x.days() = x * 86400
now
AST
Count(time > 2.days().ago()) Count
time -
*
86400
>
x.ago() = now - x
x.days() = x * 86400
now
2
Реализация объектов AST
1 class IntegerNode : public IAstNode
2 {
3 public:
4 IntegerNode(int const value) :
5 m_value(value)
6 {
7 }
8
9 private:
10 virtual Variant Evaluate(IDataProvider& /*dataProvider*/) const
11 {
12 return Variant(m_value);
13 }
14
15 int const m_value;
16 };
Реализация объектов AST
1 class VariableNode : public IAstNode
2 {
3 public:
4 VariableNode(std::string const& name) :
5 m_name(name)
6 {
7 }
8
9 private:
10 virtual Variant Evaluate(IDataProvider& dataProvider) const
11 {
12 return Variant(dataProvider.GetVariableValue(m_name));
13 }
14
15 private:
16 std::string const m_name;
17 };
Реализация объектов AST
1 class AddNode : public IAstNode
2 {
3 public:
4 AddNode(IAstNodePtr left, IAstNodePtr right) :
5 m_left(std::move(left)),
6 m_right(std::move(right))
7 {
8 }
9
10 private:
11 virtual Variant Evaluate(IDataProvider& dataProvider) const
12 {
13 return m_left->Compile(dataProvider) + m_right->Compile(dataProvider);
14 }
15
16 private:
17 IAstNodePtr m_left;
18 IAstNodePtr m_right;
19 };
Итоги: первая версия
Плюсы:
➢ простота использования
➢ скорость разработки
➢ простота реализации
Минусы:
➢ производительность
AST
Count(time > 2.days().ago()) Count
time -
*
86400
>
now
2
Архитектура LLVM
Что такое LLVM IR?
LLVM IR(Intermediate representation) - апаратно независимый
низкоуровневый язык прогрммирования.
LLVM IR
i1, … ,i32, … , i1942652 Целочисленные типы
half, float, double Типы с плавающей точкой
[40 x i32] Массивы
<4 x i32> Векторы (SIMD)
{ i32, i32, i32 } Структуры
i32*, [4 x i32]* Указатили
➢ функции
➢ типы данных(статическая строгая типизация)
➢ платформонезависимый
➢ SSA(static single assignment) нотация
LLVM IR
1 bool MoveNext();
2 int GetValue();
3
4 int main()
5 {
6 int sum = 0;
7
8 while (MoveNext())
9 {
10 sum += GetValue();
11 }
12
13 return sum;
14 }
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
1 class Expression
2 {
3 public:
4 Expression(std::string const& expression);
5
6 std::int64_t Evaluate(IDataProvider& dataProvider);
7
8 private:
9 typedef std::int64_t(*CompiledExpression)();
10
11 LLVMContext m_llvmContext;
12 ExecutionEnginePtr m_executionEngine;
13 CompiledExpression m_expression;
14 ExecutionContext m_executionContext;
15 };
Реализация
1 std::int64_t Expression::Evaluate(IDataProvider& dataProvider)
2 {
3 m_executionContext.Reset(dataProvider);
4
5 std::int64_t const result = m_expression();
6
7 m_executionContext.ThrowIfError();
8
9 return result;
10 }
Реализация
1 Expression::Expression(std::string const& expression)
2 {
3 IAstNodePtr astTree = BuildAst(expression);
4
5 IRBuilder builder(m_llvmContext);
6 Module* module = new Module("module", m_llvmContext);
7
8 m_executionEngine.reset(ExecutionEngine::createJIT(module));
9
10 FunctionType* mainType = FunctionType::get(builder.getInt64Ty(), Arguments(), false);
11 Function* main = Function::Create(mainType, Function::ExternalLinkage, "main", module);
12 BasicBlock* mainEntry = BasicBlock::Create(m_llvmContext, "entry", main);
13
14 builder.SetInsertPoint(mainEntry);
15
16 CompilationContext compilationContext(builder, *m_executionEngine, m_executionContext, module);
17 Value* returnValue = astTree->Compile(compilationContext);
18 builder.CreateRet(returnValue);
19
20 m_expression = reinterpret_cast<CompiledExpression>(m_executionEngine->getPointerToFunction(main));
21 }
Реализация
Реализация: константы
1 class IntegerNode : public IAstNode
2 {
3 public:
4 IntegerNode(int const value) :
5 m_value(value)
6 {
7 }
8
9 private:
10 virtual Value* Compile(IDataProvider& context) const
11 {
12 return context.GetBuilder().getInt64(m_value);
13 }
14
15 int const m_value;
16 };
Реализация: сложение
1 class AddNode : public IAstNode
2 {
3 public:
4 AddNode(IAstNodePtr left, IAstNodePtr right) :
5 m_left(std::move(left)),
6 m_right(std::move(right))
7 {
8 }
9
10 private:
11 virtual Value* Compile(ICompilationContext& context) const
12 {
13 Value* left = m_left->Compile(context);
14 Value* right = m_right->Compile(context);
15 return context.GetBuilder().CreateAdd(left, right);
16 }
17
18 private:
19 IAstNodePtr m_left;
20 IAstNodePtr m_right;
21 };
Реализация: переменные
1 class VariableNode : public IAstNode
2 {
3 public:
4 VariableNode(std::string const& name) :
5 m_name(name)
6 {
7 }
8
9 private:
10 virtual Value* Compile(ICompilationContext& context) const
11 {
12 return context.GetHelper().GetVariable(m_name);
13 }
14
15 private:
16 std::string const m_name;
17 };
Реализация: переменные
1 extern "C" std::int64_t GetVariableValue(std::int64_t const context, char const* const variableName)
2 {
3 ExecutionContext& executionContext = *reinterpret_cast<ExecutionContext*>(context);
4 std::int64_t const result = executionContext.GetVariableValue(variableName);
5 return result;
6 }
7
8 Value* LlvmHelper::GetVariable(std::string const& name)
9 {
10 Value*& variable = m_variables[name];
11
12 if (variable == nullptr)
13 {
14 Function* llvmFunction = Export("get_value", &GetVariableValue);
15 variable = Call(llvmFunction, &m_executionContext, name);
16 }
17
18 return variable;
19 }
Реализация: переменные
1 template<typename ReturnType, typename... ArgumentTypes>
2 Function* LlvmHelper::Export(std::string const& name, ReturnType (*nativeFunction)(ArgumentTypes...))
3 {
4 std::vector<Type*> llvmArgumentTypes = { (GetLlvmType<ArgumentTypes>())... };
5 Type* llvmReturnType = GetLlvmType<ReturnType>();
6
7 Function* function = Function::Create(functionType, Function::ExternalLinkage, name, m_module);
8 function->setCallingConv(llvm::CallingConv::C);
9
10 m_executionEngine.addGlobalMapping(function, (void*)nativeFunction);
11
12 return function;
13 }
14
15 template<typename... ArgumentTypes>
16 Value* LlvmHelper::Call(Function* llvmFunction, Arguments... arguments)
17 {
18 std::vector<Value*> llvmArguments = { (ToLlvmValue(arguments))... };
19 return m_builder.CreateCall(llvmFunction, llvmArguments);
20 }
Реализация: отрицание
1 Value* NotNode::Compile(ICompilationContext& context) const
2 {
3 CompiledNode child = m_value->Compile(context);
4
5 IRBuilder& builder = context.GetBuilder();
6
7 Value* one = builder.getInt64(1);
8 Value* zero = builder.getInt64(0);
9 Value* notEqualZero = builder.CreateICmpNE(zero, child.Value);
10 notEqualZero = builder.CreateZExt(notEqualZero, builder.getInt64Ty());
11
12 Value* value = builder.CreateXor(one, nonEqualZero);
13 value = builder.CreateZExt(value, builder.getInt64Ty());
14
15 return value;
16 }
not x == 1 xor (x != 0)
Производительность
Count(x < 50 && x > 22 || x == 77), size = 1000000
Interpreted 1734 мс
LLVM 31 мс
Native 21 мс
Кодогенерация
LLVM
Google V8
Lua
Производительность
Count(x < 50 && x > 22 || x == 77), size = 1000000
Interpreted 1734 мс
V8 402 мс
Lua 309 мс
LLVM 31 мс
Native 21 мс
Итоги: вторая версия
Плюсы:
➢ простота использования
➢ скорость разработки
➢ производительность
Итоги: вторая версия
Минусы:
➢ размер бинарного файла (~ 15 Mb)
➢ сложность реализации
➢ нативный код
?
efimyury@gmail.com
yury.efimochev@logicnow.com

Юрий Ефимочев, Компилируемые в реальном времени DSL для С++