Оптимизация в языках программирования
от aNNiMON
Продолжая тему создания своего языка программирования, я решил рассказать об оптимизации.
С последнего 14-го урока в предыдущей серии, язык OwnLang заметно улучшился, поэтому для начала я предлагаю ознакомиться с изменениями.
Все оптимизации делаются после парсинга, в момент, когда у нас уже сформировано дерево AST. Дальше нам поможет Visitor, с его помощью удобно обходить дерево и заменять узлы.
OptimizationVisitor - базовый класс, который может заменить один узел (Statement или Expression) на другой, либо удалить его. Классы всех остальных оптимизаций будут унаследованы от этого OptimizationVisitor, что позволит больше не задумываться о том, как заменить узел. Достаточно лишь возвратить новый объект и базовый класс заменит узел.
Свёртка констант (Constant Folding)
Эта оптимизация заменяет выражения, которые можно посчитать на этапе компиляции. Наприимер, x = 2 + -3 - y можно заменить на x = -1 - y
В момент обхода выражения, мы должны проверить, что оно содержит константу. В нашем случае это ValueExpression. Если, к примеру, в BinaryExpression будут две константы и, допустим, операция сложения, мы сможем посчитать выражение ещё до запуска программы. Реализуется это так:
Таким образом, выражение x = 2 + -3 - y на первом проходе заменит унарную операцию "минус" от числа 3 на константное значение -3, а на втором проходе посчитает сумму 2 + -3.
ConstantFolding.java
Распространение констант (Constant Propagation)
Эта оптимизация подставляет вместо констант их значения.
Поскольку x имеет константное значение 5, мы можем подставить его во второе выражение:
Дальше Constant Folding посчитает выражение и мы получим
Здесь нужно сначала собрать статистику о переменных, чтобы определить, какие из них имеют значения и больше нигде не изменяются. Этим занимается отдельный Visitor VariablesGrabber. После его прохода мы получаем Map<String, VariableInfo>, где String - имя переменной, а в VariableInfo хранится значение, если его удалось посчитать до запуска программы, и количество модификаций.
Теперь, отобрав константы из статистики о переменных:
мы можем заменить VariableExpression на ValueExpression:
ConstantPropagation.java
Удаление мёртвого кода (Dead Code Elimination)
После внедрения констант, может случиться так, что у нас остались неиспользуемые присваивания, либо какие-то другие ненужные выражения, убрав которые, мы не затронем результат программы. Dead code elimination как раз это и делает. Например:
После свёрки и внедрения констант мы получим такую программу:
Здесь можно с уверенностью сказать, что функция run() выполнится, а exit() нет. И присваивание нам тоже больше не понадобится. В результате мы получим лишь:
В случае с IfStatement мы должны посмотреть, является ли его условие константным. Если да и это true, то можно заменить if содержимое его ветки, если false, тогда либо заменяем на содержимое ветки else, либо, если else нет, полностью удаляем if.
Аналогично с тернарными операторами и циклами.
Чтобы удалить присваивание, нужно заменить его на то значение, которое было уже посчитано. Потому что может быть такой случай:
Здесь нужно заменить "z = 10" на "10", тогда x и y правильно проинициализируются:
Для этого снова понадобится VariablesGrabber, чтобы узнать, какие переменные используются один раз — при присваивании.
DeadCodeElimination.java
Упрощение выражений (Expression Simplification)
Эта оптимизация заменяет выражения на аналогичные, но более эффективные.
Например: x = y / -1 можно заменить на x = -y
Теперь не обязательно, чтобы все выражения в рассматриваемом блоке были посчитаны. К примеру, имея выражение "x = sin(x) * 0", нам не важно ни значение x, ни результат вызова функции sin, мы видим, что один из аргументов - ноль, а операция - умножение. Это значит, что результат безусловно будет нулём.
В этом примере я показал лишь замену в таких случаях:
- вычитание выражения от нуля (равносильно отрицанию выражения),
- вычитание нуля от выражения (равносильно самому выражению, ноль не играет роли),
- деление выражения на единицу (равносильно самому выражению),
- вычитание выражения из выражение (равносильно нулю).
В классе ExpressionSimplification.java замен гораздо больше.
Объединение инструкций (Instruction Combining)
Если в программе подряд идут несколько операторов, к примеру print:
Тогда можно их объединить в один:
Здесь нужно просматривать сразу по два Statement, чтобы узнать можно ли их объединить.
Если первый и второй операторы это print:
И их значения уже известны до запуска программы:
Тогда мы можем эти значения объединить (так как это строки, то операцией будет конкатенация) и создать новый PrintStatement, куда положим результат:
Здесь я привёл лишь случай с print, но в исходнике InstructionCombining.java есть полный пример обработки print и println.
Попробовать, как справляется оптимизация можно в ПК версии OwnLang, запустив программу с ключом "-o 9", либо в OwnLang Android Pro
Плейлист с уроками про оптимизацию
Классы, отвечающие за оптимизацию (GitHub)
С последнего 14-го урока в предыдущей серии, язык OwnLang заметно улучшился, поэтому для начала я предлагаю ознакомиться с изменениями.
Все оптимизации делаются после парсинга, в момент, когда у нас уже сформировано дерево AST. Дальше нам поможет Visitor, с его помощью удобно обходить дерево и заменять узлы.
OptimizationVisitor - базовый класс, который может заменить один узел (Statement или Expression) на другой, либо удалить его. Классы всех остальных оптимизаций будут унаследованы от этого OptimizationVisitor, что позволит больше не задумываться о том, как заменить узел. Достаточно лишь возвратить новый объект и базовый класс заменит узел.
Свёртка констант (Constant Folding)
Эта оптимизация заменяет выражения, которые можно посчитать на этапе компиляции. Наприимер, x = 2 + -3 - y можно заменить на x = -1 - y
В момент обхода выражения, мы должны проверить, что оно содержит константу. В нашем случае это ValueExpression. Если, к примеру, в BinaryExpression будут две константы и, допустим, операция сложения, мы сможем посчитать выражение ещё до запуска программы. Реализуется это так:
- @Override
- public Node visit(BinaryExpression s) {
- if ( (s.expr1 instanceof ValueExpression) && (s.expr2 instanceof ValueExpression) ) {
- return new ValueExpression(s.eval());
- }
- return super.visit(s);
- }
- @Override
- public Node visit(UnaryExpression s) {
- if (s.expr1 instanceof ValueExpression) {
- return new ValueExpression(s.eval());
- }
- return super.visit(s);
- }
Таким образом, выражение x = 2 + -3 - y на первом проходе заменит унарную операцию "минус" от числа 3 на константное значение -3, а на втором проходе посчитает сумму 2 + -3.
ConstantFolding.java
Распространение констант (Constant Propagation)
Эта оптимизация подставляет вместо констант их значения.
- x = 5
- y = 2 * x - 8
- x = 5
- y = 2 * 5 - 8
- x = 5
- y = 2
Здесь нужно сначала собрать статистику о переменных, чтобы определить, какие из них имеют значения и больше нигде не изменяются. Этим занимается отдельный Visitor VariablesGrabber. После его прохода мы получаем Map<String, VariableInfo>, где String - имя переменной, а в VariableInfo хранится значение, если его удалось посчитать до запуска программы, и количество модификаций.
Теперь, отобрав константы из статистики о переменных:
- final Map<String, Value> candidates = new HashMap<>();
- for (Map.Entry<String, VariableInfo> e : variables.entrySet()) {
- final VariableInfo info = e.getValue();
- if (info.modifications != 1) continue;
- if (info.value == null) continue;
- switch (info.value.type()) {
- case Types.NUMBER:
- case Types.STRING:
- candidates.put(e.getKey(), info.value);
- break;
- }
- }
- @Override
- public Node visit(VariableExpression s, Map<String, Value> t) {
- if (t.containsKey(s.name)) {
- return new ValueExpression(t.get(s.name));
- }
- return super.visit(s, t);
- }
ConstantPropagation.java
Удаление мёртвого кода (Dead Code Elimination)
После внедрения констант, может случиться так, что у нас остались неиспользуемые присваивания, либо какие-то другие ненужные выражения, убрав которые, мы не затронем результат программы. Dead code elimination как раз это и делает. Например:
- x = 2 + 1
- if (x > 0) {
- run()
- } else {
- exit()
- }
- x = 3
- if (true) {
- run()
- } else {
- exit()
- }
- run()
В случае с IfStatement мы должны посмотреть, является ли его условие константным. Если да и это true, то можно заменить if содержимое его ветки, если false, тогда либо заменяем на содержимое ветки else, либо, если else нет, полностью удаляем if.
- @Override
- public Node visit(IfStatement s, Map<String, VariableInfo> t) {
- if (isValue(s.expression)) {
- ifStatementEliminatedCount++;
- // true statement
- if (s.expression.eval().asInt() != 0) {
- return s.ifStatement;
- }
- // false statement
- if (s.elseStatement != null) {
- return s.elseStatement;
- }
- return new ExprStatement(s.expression);
- }
- return super.visit(s, t);
- }
Аналогично с тернарными операторами и циклами.
Чтобы удалить присваивание, нужно заменить его на то значение, которое было уже посчитано. Потому что может быть такой случай:
- x = y = z = 10
- x = y = 10
- @Override
- public Node visit(AssignmentExpression s, Map<String, VariableInfo> t) {
- if (!isVariable((Node)s.target)) return super.visit(s, t);
- final String variableName = ((VariableExpression) s.target).name;
- if (!t.containsKey(variableName)) return super.visit(s, t);
- final VariableInfo info = t.get(((VariableExpression) s.target).name);
- if (info.modifications != 1 || info.value == null) {
- return super.visit(s, t);
- }
- switch (info.value.type()) {
- case Types.NUMBER:
- case Types.STRING:
- assignmentExpressionEliminatedCount++;
- return new ValueExpression(info.value);
- default:
- return super.visit(s, t);
- }
- }
DeadCodeElimination.java
Упрощение выражений (Expression Simplification)
Эта оптимизация заменяет выражения на аналогичные, но более эффективные.
Например: x = y / -1 можно заменить на x = -y
Теперь не обязательно, чтобы все выражения в рассматриваемом блоке были посчитаны. К примеру, имея выражение "x = sin(x) * 0", нам не важно ни значение x, ни результат вызова функции sin, мы видим, что один из аргументов - ноль, а операция - умножение. Это значит, что результат безусловно будет нулём.
- @Override
- public Node visit(BinaryExpression s, Void t) {
- // operations with 0
- final boolean expr1IsZero = isIntegerValue(s.expr1, 0);
- if (expr1IsZero || isIntegerValue(s.expr2, 0)) {
- switch (s.operation) {
- case SUBTRACT:
- if (expr1IsZero) {
- // 0 - x2 to -x2
- return new UnaryExpression(UnaryExpression.Operator.NEGATE, s.expr2);
- }
- // x1 - 0 to x1
- return s.expr1;
- case MULTIPLY:
- // 0 * x2 to 0, x1 * 0 to 0
- return new ValueExpression(0);
- }
- }
- // x1 / 1 to x1
- if (isIntegerValue(s.expr2, 1) && s.operation == BinaryExpression.Operator.DIVIDE) {
- return s.expr1;
- }
- // x - x to 0
- if (isSameVariables(s.expr1, s.expr2) && s.operation == BinaryExpression.Operator.SUBTRACT) {
- simplificationsCount++;
- return new ValueExpression(0);
- }
- return super.visit(s, t);
- }
- вычитание выражения от нуля (равносильно отрицанию выражения),
- вычитание нуля от выражения (равносильно самому выражению, ноль не играет роли),
- деление выражения на единицу (равносильно самому выражению),
- вычитание выражения из выражение (равносильно нулю).
В классе ExpressionSimplification.java замен гораздо больше.
Объединение инструкций (Instruction Combining)
Если в программе подряд идут несколько операторов, к примеру print:
- print 3
- println "test"
- print "1212"
- print "3test\n1212"
Здесь нужно просматривать сразу по два Statement, чтобы узнать можно ли их объединить.
Если первый и второй операторы это print:
- if ((n1 instanceof PrintStatement) && (n2 instanceof PrintStatement)) {
- }
- final Expression e1 = ((PrintStatement) n1).expression;
- final Expression e2 = ((PrintStatement) n2).expression;
- if (isConstantValue(e1) && isConstantValue(e2)) {
- }
- return new PrintStatement(new ValueExpression(s1 + s2));
Здесь я привёл лишь случай с print, но в исходнике InstructionCombining.java есть полный пример обработки print и println.
Попробовать, как справляется оптимизация можно в ПК версии OwnLang, запустив программу с ключом "-o 9", либо в OwnLang Android Pro
Плейлист с уроками про оптимизацию
Классы, отвечающие за оптимизацию (GitHub)