Brainfuck — язык программирования, созданный с одной целью: написать для него интерпретатор. Их было написано так много, что даже не буду давать на них ссылки. В этой статье на пальцах объясняется простой, но эффективный способ его оптимизации.
Для оптимизации нужно выполнить 3 правила:
-
Обратные инструкции (+ и -, > и <) взаимоуничтожаются, когда идут одна за другой. Код >++>><<- превращается в >+. Это, скорее, защита от дурака, чем оптимизация, т.к. такой код бессмысленен.
-
Повторяющиеся инструкции заменяются на команды, в аргументах которых сказано сколько раз конкретную инструкцию выполнить. Например, код +++ заменяется на команду ADD с аргументом 3, а код <<< на MOVE:-3.
- Добавляется новая команда, у которой в bf нет соответствующий инструкции. Команда присвоения значения. Код [+] и [-] обнуляет текущую ячейку, а команда ADD за этим кодом присваивает текущей ячейке значение своего аргументы. Код --[-]+++ заменяется на команду ASSIGN:3.
Список команд с описанием
Каждая команда имеет свой аргумент (далее просто arg). Значение аргумента ограничено только у команды ADD, т.к. размер одной ячейки 256.
Имя команды | Инструкции | Описание |
---|---|---|
ADD | + и - | Изменяет значение текущей ячейки на arg |
MOVE | > и < | Изменяет индекс текущей ячейки на arg |
READ | , | Пропускает из потока ввода arg символов и читает следующий за ними символ |
. | Печатает символ соответствующий значению текущей ячейки arg раз | |
GOTO | [ и ] | Переходит к выполнению arg по счёту команды относительно текущей |
ASSIGN | [+] и [-] | Присваивает текущей ячейке arg |
Пример кода интерпретатора
Интерпретация разделена на 3 этапа. Чтение исходного кода, его преобразование в оптимизированные команды и выполнение этих команд. Это всё происходит в функциях main и parse.
Первый и последний этап происходят сразу в функции main. Её код с комментариями:
int main(int argc, char* argv[]){
/* имя файла с исходниками передаётся первым аргументом */
if(argc == 1){
fprintf(stderr, "%s: Wrong argument count\n", argv[0]);
return 1;
};
/* файл открывается для чтения */
FILE* f = fopen(argv[1], "r");
if(f == NULL){
fprintf(stderr, "%s: Can't open %s\n", argv[0], argv[1]);
return 2;
};
/* исходный код читается из файла */
int n = fread(str, sizeof(char), SIZE - 1, f);
if(n == 0){
fprintf(stderr, "%s: Can't read data from %s\n", argv[0], argv[1]);
return 3;
};
str[n] = '\0';
/* проверяется правильность расстановки скобок */
fclose(f);
if(brackets()){
fprintf(stderr, "%s: Wrong brackets sequence\n", argv[0]);
return 4;
};
/* парсинг исходного кода */
parse();
/* выполнение команд */
ptr = mem;
int (*exe[])(int) = {exe_a, exe_b, exe_c, exe_d, exe_e, exe_f};
struct code* p = src + 1;
for(; p->cmd; ++p){
p += (*exe[p->cmd - 1])(p->arg);
};
return 0;
};
Оптимизация с помощью преобразования инструкций bf в команды для интерпретатора происходит в функции parse. Её код с комментариями:
void parse(){
/* инициализируется массив команд */
struct code *p = src;
p->cmd = 0;
p->arg = 0;
/* указатель на текущую инструкцию */
char* ch = str;
/* указатель на вершину стека необходимый для обработки скобок */
top = stk;
/* массив из указателей на функции обрабатывающих 8 возможных команд и комментарии */
int (*prs[])(struct code*) = {prs_0, prs_1, prs_2, prs_3, prs_4, prs_5, prs_6, prs_7, nothing};
/* парсинг */
for(; *ch != '\0'; ++ch){
p += (*prs[find(*ch)])(p);
};
/* нуль-терминальная команда в конце массива */
(p + 1)->cmd = 0;
};
Проверка эффективности
Для сравнения взял интерпретатор из официального ubuntu репозитория и несколько тестов из этого репозитория. В таблице записано среднее время работы теста в секундах.
Имя теста | Официальный | Из статьи |
---|---|---|
Long | 34 | 20 |
Mandelbrot | 21 | 26 |
EasyOpt | 24 | 27 |
Counter | 34 | 37 |
На всех тестах, кроме первого, официальный интерпретатор работает примерно на 12 процентов быстрее интерпретатора из статьи. В первом тесте, в отличии от остальных, в большинстве циклов количество инструкций > не совпадает с количеством инструкций <. Это делает циклы несбалансированными и не поддающимися оптимизации (другому виду оптимизации, не описанному в статье). Похоже, официальный интерпретатор оптимизирует сбалансированные циклы и получает от этого 12-процентный прирост к скорости, в то время как интерпретатор из статьи этого не делает и побеждает только на первом тесте. Хотя это неплохой результат для такого простого алгоритма оптимизации.
Исходники на Github
Комментарии (0)