Del*_*ani 32 c optimization interpreter brainfuck
作为一个帮助我学习口译和优化的练习,我都不知道,我用C编写了一个脑筋解释器.到目前为止它看起来完美无缺,尽管与其他快速相比,它在执行速度方面表现不佳口译.
有哪些方法可以改变这个解释器以提高性能(或其他方面)?
我的解释器的一个有趣的方面(虽然大多数其他人也可能这样做)是我运行一个循环,读取源输入并将每个指令转换为
struct { long instruction; long loop; }
Run Code Online (Sandbox Code Playgroud)
该loop值是匹配的索引]指令,如果该指令是[,和匹配的索引[指令,如果该指令是],允许快速跳跃.我想这个'解析'过程(不需要很长时间)可以改善执行时间,而不是每次需要时进行冗余重新分析以找到匹配的方括号.
这个程序是一个有趣的测试brainfuck解释器的速度:
++++++++[->-[->-[->-[-]<]<]<]>++++++++[<++++++++++>-]<[>+>+<<-]>-.>-----.>
Run Code Online (Sandbox Code Playgroud)
instructionstruct instruction成为操作函数的直接指针来消除运行时循环中的巨型开关- 这比前一版本运行得慢(函数调用开销?)Nem*_*emo 19
嗯,这不是C.而且它不是一个插件.所以,是的,这个问题几乎完全不合适.
但它是什么,是一个使用C++ 0x可变参数模板的完全可移植的brainfuck编译器.您必须以#define PROGRAM逗号分隔的C语法字符序列,因为我无法在编译时从字符串中提取它们.但除此之外它是合法的.我认为.
用g ++ 4.5.2测试,使用g++ -std=c++0x -O2 -Wall.
#include <cstdio>
#include <vector>
#define PROGRAM '+', '+', '+', '+', '+', '+', '+', '+', '[', '-', '>', \
'-', '[', '-', '>', '-', '[', '-', '>', '-', '[', '-', ']', '<', \
']', '<', ']', '<', ']', '>', '+', '+', '+', '+', '+', '+', '+', \
'+', '[', '<', '+', '+', '+', '+', '+', '+', '+', '+', '+', '+', \
'>', '-', ']', '<', '[', '>', '+', '>', '+', '<', '<', '-', ']', \
'>', '-', '.', '>', '-', '-', '-', '-', '-', '.', '>'
template<char... all>
struct C;
template<char... rest>
struct C<'>', rest...> {
typedef C<rest...> rest_t;
typedef typename rest_t::remainder remainder;
static char *body(char *p) {
return rest_t::body(p+1);
}
};
template<char... rest>
struct C<'<', rest...> {
typedef C<rest...> rest_t;
typedef typename rest_t::remainder remainder;
static char *body(char *p) {
return rest_t::body(p-1);
}
};
template<char... rest>
struct C<'+', rest...> {
typedef C<rest...> rest_t;
typedef typename rest_t::remainder remainder;
static char *body(char *p) {
++*p;
return rest_t::body(p);
}
};
template<char... rest>
struct C<'-', rest...> {
typedef C<rest...> rest_t;
typedef typename rest_t::remainder remainder;
static char *body(char *p) {
--*p;
return rest_t::body(p);
}
};
template<char... rest>
struct C<'.', rest...> {
typedef C<rest...> rest_t;
typedef typename rest_t::remainder remainder;
static char *body(char *p) {
putchar(*p);
return rest_t::body(p);
}
};
template<char... rest>
struct C<',', rest...> {
typedef C<rest...> rest_t;
typedef typename rest_t::remainder remainder;
static char *body(char *p) {
*p = getchar();
return rest_t::body(p);
}
};
template<char... rest>
struct C<'[', rest...> {
typedef C<rest...> rest_t;
typedef typename rest_t::remainder::remainder remainder;
static char *body(char *p) {
while (*p) {
p = rest_t::body(p);
}
return rest_t::remainder::body(p);
}
};
template<char... rest>
struct C<']', rest...> {
typedef C<rest...> rest_t;
struct remainder_hack {
typedef typename rest_t::remainder remainder;
static char *body(char *p) {
return rest_t::body(p);
}
};
typedef remainder_hack remainder;
static char *body(char *p) {
return p;
}
};
template<>
struct C<> {
static char *body(char *p) {
return p;
}
struct remainder {
static char *body(char *p) {
return p;
}
};
};
int
main(int argc, char *argv[])
{
std::vector<char> v(30000, 0);
C<PROGRAM> thing;
thing.body(&v[0]);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
Jer*_*fin 18
我可以看到几种可能性.我认为我的方法是将其转变为生成直接线程代码的编译器.也就是说,当您读取输入时,而不是将大多数"指令"或多或少地复制到内存中,而是编写代码以将每条指令实现为函数,并将指向每个函数的指针复制到内存中.然后执行代码将包括按顺序调用这些函数.我可能会让该函数返回下一条要执行的指令的索引(或可能是地址),所以你最终得到的结果如下:
typedef int return_type;
typedef return_type (*f)(void);
f *im = malloc(sizeof(f) * ia);
ci = (*(im[ci]))();
Run Code Online (Sandbox Code Playgroud)
我还为每条指令分别设置了三个函数,每个函数对应一个BF_END_*模式,因此您只需在"编译"阶段处理该函数.执行代码时,您将直接指向正确的函数.
编辑:
我一直在玩代码.我已经将循环地址分成一个单独的数组,并将大部分解析合并在一起,所以它看起来像这样:
for (ii = 0; (i = getc(fp)) != EOF; ++ii) {
if (++in > ia) {
ia *= 2;
im = realloc(im, sizeof(*im) * ia);
loops = realloc(loops, sizeof(*loops) * ia);
}
im[in-1] = i;
switch (i) {
case BF_OP_LSTART:
if (ln >= la)
ls = realloc(ls, sizeof(*ls) * (la *= 2));
ls[ln++] = ii;
break;
case BF_OP_LEND:
loops[in-1] = ls[--ln];
loops[ls[ln]] = ii;
break;
}
}
Run Code Online (Sandbox Code Playgroud)
这对速度没有任何实际影响,但确实使代码更短,并且(至少在我看来)更容易理解.
EDIT2:
好吧,我有机会更多地玩这个,发现一个(相当奇怪的)优化似乎确实有点帮助.编译器通常会为具有密集大小写值的switch语句生成稍微好一些的代码,因此我尝试转换为该代码,并获得了大约9-10%的改进(取决于编译器).
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define BF_END_ERROR 'e'
#define BF_END_IGNORE 'i'
#define BF_END_WRAP 'w'
#define BF_OP_VINC '+'
#define BF_OP_VDEC '-'
#define BF_OP_PINC '>'
#define BF_OP_PDEC '<'
#define BF_OP_LSTART '['
#define BF_OP_LEND ']'
#define BF_OP_IN ','
#define BF_OP_OUT '.'
enum {
C_OP_VINC,
C_OP_VDEC,
C_OP_PINC,
C_OP_PDEC,
C_OP_LSTART,
C_OP_LEND,
C_OP_IN,
C_OP_OUT
};
typedef struct {
long instruction; /* instruction type */
long loop; /* 'other' instruction index in a loop */
} instruction;
void die(const char *s, ...) {
va_list a;
va_start(a, s);
fprintf(stderr, "brief: error: ");
vfprintf(stderr, s, a);
putchar(10);
va_end(a);
exit(1);
}
int main(int argc, char **argv) {
unsigned instruction_count = 0;
long
ci = 0, /* current cell index */
cn = 4096, /* number of cells to allocate */
cw = BF_END_WRAP, /* cell wrap behaviour */
ia = 4096, /* number of allocated instructions */
ii = 0, /* current instruction index */
in = 0, /* number of used instructions */
la = 4096, /* loop stack allocation */
ln = 0, /* loop stack used */
va = 0, /* minimum value */
vb = 255, /* maximum value */
vw = BF_END_WRAP /* value wrap behaviour */
;
instruction *im = malloc(sizeof(instruction) * ia); /* instruction memory */
long *cm = NULL; /* cell memory */
long *ls = malloc(sizeof(long) * la); /* loop stack */
FILE *fp = NULL;
int i;
while ((i = getopt(argc, argv, "a:b:c:f:hv:w:")) != -1) {
switch (i) {
case 'a': va = atol(optarg); break;
case 'b': vb = atol(optarg); break;
case 'c': cn = atol(optarg); break;
case 'f':
fp = fopen(optarg, "r");
if (!fp)
die("%s: %s", optarg, strerror(errno));
break;
case 'h':
fputs(
"brief: a flexible brainfuck interpreter\n"
"usage: brief [options]\n\n"
"options:\n"
" -a set minimum cell value (default 0)\n"
" -b set maximum cell value (default 255)\n"
" -c set cells to allocate (default 4096)\n"
" -f source file name (required)\n"
" -h this help output\n"
" -v value over/underflow behaviour\n"
" -w cell pointer over/underflow behaviour\n\n"
, stderr);
fputs(
"cells are 'long int' values, so do not use -a with a "
"value less than -2^31 or -2^63, and do not use -b with a "
"value more than 2^31-1 or 2^63-1, depending on your "
"architecture's 'long int' size.\n\n"
"over/underflow behaviours can be one of:\n"
" e throw an error and quit upon over/underflow\n"
" i do nothing when attempting to over/underflow\n"
" w wrap-around to other end upon over/underflow\n"
, stderr);
exit(1);
break;
case 'v': vw = optarg[0]; break;
case 'w': cw = optarg[0]; break;
default: break;
}
}
if (!fp)
die("no source file specified; use -f");
for (ii = 0; (i = getc(fp)) != EOF; ++ii) {
if (++in > ia) {
ia *= 2;
im = realloc(im, sizeof(*im) * ia);
}
switch (i) {
case BF_OP_LSTART:
if (ln >= la)
ls = realloc(ls, sizeof(*ls) * (la *= 2));
ls[ln++] = ii;
im[in-1].instruction = C_OP_LSTART;
break;
case BF_OP_LEND:
im[in-1].loop = ls[--ln];
im[ls[ln]].loop = ii;
im[in-1].instruction = C_OP_LEND;
break;
case BF_OP_VINC:
im[in-1].instruction = C_OP_VINC;
break;
case BF_OP_VDEC:
im[in-1].instruction = C_OP_VDEC;
break;
case BF_OP_PINC:
im[in-1].instruction = C_OP_PINC;
break;
case BF_OP_PDEC:
im[in-1].instruction = C_OP_PDEC;
break;
case BF_OP_IN:
im[in-1].instruction = C_OP_IN;
break;
case BF_OP_OUT:
im[in-1].instruction = C_OP_OUT;
break;
}
}
cm = memset(malloc(cn * sizeof(long)), 0, cn * sizeof(long));
for (ii = 0; ii < in; ii++) {
++instruction_count;
switch (im[ii].instruction) {
case C_OP_VINC:
if (cm[ci] == vb)
switch (vw) {
case BF_END_ERROR:
die("value overflow");
break;
case BF_END_IGNORE: break;
case BF_END_WRAP: cm[ci] = 0; break;
}
else ++cm[ci];
break;
case C_OP_VDEC:
if (cm[ci] == 0)
switch (vw) {
case BF_END_ERROR:
die("value underflow");
break;
case BF_END_IGNORE: break;
case BF_END_WRAP: cm[ci] = vb; break;
}
else --cm[ci];
break;
case C_OP_PINC:
if (ci == cn - 1)
switch (cw) {
case BF_END_ERROR:
die("cell index overflow");
break;
case BF_END_IGNORE: break;
case BF_END_WRAP: ci = 0; break;
}
else ++ci;
break;
case C_OP_PDEC:
if (ci == 0)
switch (cw) {
case BF_END_ERROR:
die("cell index underflow");
break;
case BF_END_IGNORE: break;
case BF_END_WRAP: ci = cn - 1; break;
}
else --ci;
break;
case C_OP_IN:
cm[ci] = getchar();
break;
case C_OP_OUT:
putchar(cm[ci]);
break;
case C_OP_LSTART:
if (!cm[ci])
ii = im[ii].loop;
break;
case C_OP_LEND:
if (cm[ci])
ii = im[ii].loop;
break;
default: break;
}
}
fprintf(stderr, "Executed %d instructions\n", instruction_count);
free(cm);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
由于该项目的整个过程都是学习,因此使用工具或替换解决方案显然无法回答这个问题.
首先,免责声明:我不是x86程序员 - 我在嵌入式环境和现在(使用手机)ARM芯片方面做了大量工作.关于好东西......
使解释器更快的两种基本方法是:使其优化BF代码本身,并使解释器本身进行优化.我将在下面的一步中推荐两者.
据我所知,x86在提供相对令人印象深刻的快速分支特性方面花费了大量染料空间.可能由于这个原因,我看到几个编译器(包括gcc)生成嵌套分支,支持x86的实际跳转表.跳跃表在理论上听起来很有吸引力,但是真正的x86优化了传统技术,传统的大O思想在大多数实践中都不适用.这就是为什么长期的x86开发人员会告诉你是否要计算代码的速度,然后你需要编写它,运行它并计算时间.
尽管分支在x86上的速度很快,但仍然可能值得在不分支上花费一些开销.由于BF指令无论如何都是如此简单,它可以采取"一次完成大部分指令的形式,因为它比另一个分支更快".其中一些甚至可以由处理器并行完成,其中分支不能.(x86在单个内核中有足够的解码和执行单元,这可能是可行的)
另一件会破坏你性能的是错误检查(例如换行).将其保留在那里会导致性能问题(现在不是主要问题),但更重要的是阻止您进行优化.
此外,您的程序非常通用.它允许您使用任何最大值进行包装.如果它是2的幂,你只需执行一个按位AND(非常快,因为它几乎在所有现代平台上都是一个CPU周期)相当于包装而不是比较和分支.魔鬼在编写真正快速的解释器的细节 - 你添加的每一点都使它变得更加复杂.
为此,我建议简化你的BF解释器所做的事情(例如,使用值为2的值进行包装).单独的按位AND技巧和强制换行是选项(在大多数现代编程语言中溢出/下溢的情况)已经删除了至少两个分支.
一旦这些得到处理,这将实现一个有趣的优化:丢弃BF指令,而是让您的机器执行更适合解释器的不同指令.
考虑Java:Java没有解释.它JIT成一种完全不同的语言.
我建议应用相同的逻辑.您已经通过向指令提供与其相关的值来完成此操作.
我建议使用以下信息创建指令:
解释器循环然后更改为逻辑如下:如果数据指针处的值与我们的比较值集匹配,则将指令中的数据值添加到数据指针处的值将指令中的数据地址值添加到数据指针本身如果比较值是特殊值(例如,您可以选择0x80000000),则指向新指令指针的指令指针继续(到下一个解释器循环)处理输入/输出填充将指令地址增加一
现在,初步解释变得更加棘手:指令现在可以将+, - ,<,>,有时甚至[,通常]组合到同一条指令中.循环中的第一个分支可以在没有分支的情况下表示,如果你对它很聪明(甚至更高效,一些程序集卡在或一些编译器内在函数).你可能想告诉编译器第二个分支不太可能命中(在这种情况下输入/输出是瓶颈,而不是解释的速度,所以即使你做了很多输入/输出,一个小的未经优化的分支不会有所作为).
必须注意运行结束的情况.解释的BF程序中的最后一条指令现在应该始终使指令指针为NULL,以便循环退出.
解释发生的顺序很重要,因为 - /+在I> O之前完成之前完成<>之前完成.因此,> +是两个解释指令,而+>是一个.
除了设计像这样的快速紧密循环解释器之外,您还在研究更复杂的代码分析,在这种情况下,您将进入编译器设计并且远离直接解释器.这不是我每天都做的事情,但Louden的"编译器构建"一书对我来说是一本非常好的读物,但它不会以这种方式成为一个小项目.除非你认真对待这个事情,并且最终可能编译代码,否则我将远离抛出那些大而强硬的优化.
我希望我已经给你一个尝试和测试的想法,引导你进行更多的优化步骤.我自己没有尝试过这些,所以根据过去的经验,它仍然是猜测.但是,即使它最终不会更快,您也可以获得一些将BF代码重写为与vanilla BF解释器相对不同的架构的经验.
PS 好问题!
Brainfuck应该很容易编译成C代码,然后编译并执行.这可能是一个非常快的BF"翻译".
从根本上说,你所要做的就是在程序中从左到右为每个brainfuck操作员生成非常简单的代码.人们可以很容易地优化+和 - 的序列; 类似地,可以通过缓存最近遇到的每个的计数来优化<和>的序列.这是一种窥视孔优化.
这是一个草稿编译器,在命令行上接受BF代码并将编译好的程序打印到控制台:
int increments; // holds pending increment operations
void flush_increments(){
if (increments==0) return;
printf(" *ptr+=%d;\n",increments);
increments=0;
}
int steps; // holds pending pointer steps
void flush_steps(){
if (steps==0) return;
printf(" ptr+=%d;\n",steps);
steps=0;
}
int main(int argc, char **argv){
// Brainfuck compiler
if( !(argc > 1) )
return 1;
unsigned char *code = argv[1];
int nesting=0;
printf("int main(){\n");
printf(" #define CELLSPACE 1000\n");
printf(" unsigned char *ptr = malloc(sizeof(char)*CELLSPACE);\n");
printf(" if(ptr == NULL) return 1;\n")
printf(" for(int i=0;i<CELLSPACED;i++) ptr[i]=0; // reset cell space to zeros");
increments=0;
steps=0;
for(;;) {
switch(*code++) {
case '+':
flush_steps();
++increments;
break;
case '-':
flush_steps();
--increments;
break;
case '>':
flush_increments();
++steps;
break;
case '<':
flush_increments();
--steps;
break;
case '[':
flush_increments();
flush_steps();
printf("while(*ptr){");
++nesting;
break;
case ']':
flush_increments();
flush_steps();
if (--nesting<0)
{ printf("Unmatched ']'\n");
return 1;
}
printf("}\n";);
break;
case '.':
flush_increments();
flush_steps();
printf(" putc(*ptr, stdout);\n");
break;
case ',':
increments=0;
flush_steps();
printf("*ptr = getc(stdin);");
break;
case '\0':
printf("}");
if (nesting>0)
{ printf("Unmatched '['\n");
return 1;
}
return 0;
}
}
}
Run Code Online (Sandbox Code Playgroud)
在马修布兰查德的代码(感谢马修!)的启发下,这是我的头脑中的关键,但没有经过测试.我会把它留给其他灵魂; 如果发现问题,请随时修改代码.如果将代码写入文件,显然会得到改进: - }
[我使用http://en.wikipedia.org/wiki/Brainfuck文章作为生成代码的明显灵感].
OP的BF计划:
++++++++ [ - > - [ - > - [ - > - [ - ] <] <] <]> ++++++++ [<++++++++++ > - ] <[> +> + << - ]> - > ----->
应编译为(缩进添加):
int main(){
#define CELLSPACE 1000
unsigned char *ptr = malloc(sizeof(char)*CELLSPACE);
if(ptr == NULL) return 1;
for(int i=0;i<CELLSPACED;i++) ptr[i]=0; // reset cell space to zeros
*ptr+=8;
while(*ptr) {
*ptr+=-1;
ptr+=1;
*ptr+=-1;
while(*ptr) {
*ptr+=-1;
ptr+=1;
*ptr+=-1;
while(*ptr) {
*ptr+=-1;
ptr+=1;
*ptr+=-1;
while(*ptr) {
*ptr+=-1;
}
ptr+=-1;
}
ptr+=-1;
}
ptr+=1;
*ptr+=8;
while (*ptr) {
ptr+=-1;
*ptr+=10;
ptr+=1;
*ptr+=-1;
}
ptr+=-1;
while (*ptr) {
ptr+=1;
*ptr+=1;
ptr+=1;
*ptr+=1;
ptr+=-2;
*ptr+=-1;
}
ptr+=1;
*ptr+=-1;
putc(*ptr,stdout);
ptr+=1;
*ptr+=-5;
putc(*ptr,stdout);
ptr+=1;
}
Run Code Online (Sandbox Code Playgroud)
这可能非常接近每个BF操作的一个机器指令.
真正雄心勃勃的人会在程序的每个点计算ptr的可能值; 我猜在很多情况下它指的是一个恒定的细胞.然后可以避免间接访问.
如果你真的想要坚持下去,你可以弄清楚BF命令在第一个输入请求之前做了什么; 这必须是一个"常量"初始内存配置,并使用该常量生成一个CELLSPACE初始化器,并按照我所示的方式为程序的其余部分生成代码.如果你这样做,OP的示例程序将消失为单个CELLSPACE初始化器和几个putc调用.