看板Programming
标 题Re: [问题] 如何学写COMPILER? [纯抛砖引玉]
发信站政大狂狷年少 (Fri Apr 27 21:15:47 2007)
转信站ptt!ctu-reader!ctu-gate!news.nctu!newsfeed.nthu!news.cs.nthu!WHSHS
※ 引述《[email protected] ( )》之铭言:
> ※ 引述《[email protected] (汀)》之铭言:
> > 你好像已经忘记我更前面说「context-free LALR(1) parser」了:
> BNF is context-free only
不好意思,
这个的确是我记错了,
BNF 有规定 ::= 左边只有一个 symbol,
KNF (Kuroda Normal Form) 才可以那样玩,
我误会你了。
> 如果所用的 yacc 可靠, 你也不该追进去看
> 只应该看人写的部份的 C, 好的 debugger 是支援 yacc 的
debugger 其实不是直接支援 yacc,
而是跟着那个 #line 对应的行号跑;
而 yacc 本身当然是可靠的,
但是有一个缺点在於,
programmer 本身写的文法不见得可靠,
或许你会说 spirit 也是有这种现象,
但是你换个角度想想,
现代的程式重视的是能「共同合作」和「容易交接」,
如果「读程式码」和「debug 的人」都不是「原作者」,
会是什麽样的情景呢?
debugger 在 yacc 的状况是,
会混着 .y 跟 .c 在跳,
这是因为 yacc 在 .c file 里面插入了一堆 #line 123 "xxx.y" 这种东西,
可是这时你就会有一个麻烦,
相信你也知道 .y 常常是这样写的:
rule1:
xxxx yyyy zzzz
{
/* 超大一片 C code */
}
| aaa bbb
{
/* 又是超大一片 C code */
}
| ...
yacc 会把 #line 插在每个 { 的下面,
所以当 debugger 跑进去看的时候固然能看到原本作者写的 .y file 内容,
但是...
通常 debug 的人跳进去会看到的东西是长这样:
/* 上面一大片 code 的末几行 */
}
| aaa bbb
{
/* 下面一大片 code 的前几行 */
就算 debugger 有办法让你直接输入那些 $$ $1 $2 帮你展开好了,
你看到的东西仍然是一个 grammar 的片段,
但是 spirit 就不会有这种现象,
你很容易看到完整的 grammar,
大概会是长这个样子:
rule1 =
(xxxx >> yyyy >> zzzz)[&action1]
| (aaa >> bbb)[&action2]
| ...
;
虽然在更复杂的情形下 spirit 的版本会看起来乱一点,
但是 programmer 用 debugger 跳进去的时候大都可以看到完整的 rule,
而且他们也有办法选择要不要 step into 到里面去,
debug 起来自然容易,
不是作者的人看起来也比较简明易懂。
> spirit 是 boost 之中特别容易让 compiler 挂点的东西之一,
这年头会因为这样就挂掉的 compiler 不多了,
除非你还在用 VC6 跟 gcc 2.95.x。
> 光 boost 要编起来就很麻烦, 遇到 compile error 比 yacc 更头大
嗯,关於这一点,
*nix 上通常有一种东西叫做套件管理系统,
譬如:
pkg_add boost-1.33.1.tbz
apt-get install boost
emerge boost
cd /usr/ports/devel/boost; make install clean
然後去吃个饭就好了,
当然偶尔会碰到一些地雷,
但是这些套件的 maintainer 会负责搞定它,
这个大可放心。
> 只因为是 C++ 就一定好 debug? error message 的数量就可说明
当然不可能是这样,
理由在上面就有描述了一部份罗。
> > 今天用 C++ 的 boost::spirit library 写出的 parser,
> > 显然就是比 yacc generator 出来的 parser 容易 debug,
> > 因为 boost::spirit 用的技术比较先进 (无关它是否较晚被做出来),
^^^^^^^^^^^^^^^^
> 看起来跟 yacc 很像, 却是 C++, 比较先进是说用 C++ 就先进?
上面已经不是说了,
是用的「技术」比较先进,
让该在一起的东西在一起,
不该混在一起的就分开来 (grammar 跟 action),
进而获得不想 step into 就可以 next 掉的 debugging 优势。
> xxx = a|b;
> yyy = xxx|c;
> 因 C++ 语法限制所以还不能像 yacc 用 LR, spirit 是 LL only
> 从来没有比 yacc 先进的说法
不对,
这是因为 spirit 故意选择了 recursive descent parsing,
为什麽?
因为合乎人类的直觉,
所以也因此更容易 debug (容易 debug 的理由又多了一个)。
GCC 4.x 也选择了「纯手工」打造的 LL(1) parser,
boost::spirit 则选择了「半手工」打造的途径,
并不能说 LL 在课本上比 LR 早出现,
而 LR 可以比 LL 少改一些文法,
就说 LL 比较落後,
LL 跟 LR 各有优缺点,
而它们的优劣差异可以大到让人们分成两大派系,
所以拿 LL 跟 LR 来区分先进与落後是不恰当的。
> PPC, MIPS, x86 assembly 不够旧?
> 为了 binary 相容一个指另集最少活 20 年
重点不是一个指令集活了多少年,
而是新的指令集有没有持续出现。
> CPU 效能看法也一直在变, 古代 ram 少, cisc>>risc, risc 不划算
> 民初 ram 多, risc>>cisc, 工作站用 RISC 成王道
> 近代 cache 少, ram 慢, cisc >> risc, x86 干掉 PPC,SPARC,MIPS
> 现代多核心 vliw>>cisc>>risc, 只是 compiler 难搞
^^^^^^^^^^^^^^^^^^
真的吗?
事实上连写 assembly code 的人都很难搞,
他们必须重新学习一套设计哲学,
使得自己写出来的 asm code 可以「正确」而且「比 compiler 编出来的快」,
VLIW 可以扩增到 5-way issue、8-way issue 甚至更多,
你一个 cycle 最多可以平行 issue 那麽多指令出去,
不但要想办法解决 dependency 的问题,
还要想办法尽可能的把指令排在最少量的 bundle 里,
VLIW 跟 superscalar 的差异我想你应该知道,
它是不会帮你解决 hazards 的,
大部分为了节省成本连侦测都不侦测,
而且 programmer 甚至可以利用不侦测的特性,
做到一些以往在 RISC 和 CISC 都做不到的最佳化,
譬如因为 MEM 指令的结果要在 2 个 cycles 後能给 ALU 指令用,
这时我们可以将使用旧值的 ALU 指令排在 MEM 指令跟 ALU 指令之间,
会自动侦测和 stall 的 architecture 办不到这一点,
因为你一插到中间去,
hardware 就会帮你 stall 起来保证拿到的是新值;
很多观念都越来越不一样了。
加上每个 ISA 都会有各自的特异功能,
有时还会为了一些奇奇怪怪的理由让指令有诸多限制,
asm code 的设计哲学也会因此大幅转变,
重点是 asm language 本来就是绑 ISA 的,
asm syntax 也会因为 ISA 的特殊性而有所改变,
它们时常都在变化。
> 何种 CPU instruction 叫先进?
所以说你搞错重点了,
重点是 asm 一直都在变,
而我已经说过长期不变的东西往往是落後,
这没有什麽奇怪,
技术这种东西本来就是不进则退,
技术跟工具这两种东西与理论不同,
它们本来就是非常物质面的东西,
很难像理论一样永远能以不变应万变。
> microchip c18, m$ visual c 都是 bsd yacc
> 只要在档案中有很大的非人写成的 table, 都是 code gen 产生的
> 不一定是 yacc, 但是通常 compiler/interpreter 走两个极端:
> 1. 4GL, 最快速完成
> 2. 用 3GL 硬干, 执行最快速, 如 turbo pascal, 大多 interpreter
> visual c 有 "grammar.y", c18 有 "yacc stack overflow"
> 在 compiler 执行档找 yacc 的收获会不少
世上仍存在使用 yacc 写成的 compiler 是正常的,
就如同我前篇所说的,
久未更新的教科书留下的遗毒其实很恐怖。
4GL 确实不是坏东西,
而我一开始也只有说 yacc/bison 一直不变所以很不好。
> 照这样说, 那 yacc 跟 spirit 比 C++ 和 java 之间差更多
> 要互相比较是更困难罗, 为啥不能比较 C++ & java
答案很简单,
因为 C++ 和 Java 一直还有在改进,
但是 yacc 停住了,
spirit 不但使用了新技术且确实达到了使用该技术的目的 (好写好 debug),
而且它仍然在不断改进当中 (要注意是技术面的改进,可以比较看看它的历史纪录)。
> C++ 的 template 不算 tricks 的话, 比 template 更简单的 C 语法何来 tricks
> C 的语法能作的变化更简单
C 语法能作的变化太简单,
就达不到 C++ 完全不用变化就能做到的功能,
所以 C 语法在要达成跟 C++ 相同功能的状况下 (享受相同/相似的好处),
语法会相当不简单。
不少人曾经用纯 C 来模拟 OO,
使用了大量且特异的 data structures,
以及千奇百怪的 macros,
相信你应该有见识过才对。
语言的 tricks 最终可能变成标准,
但是如果语言机制实在太少,
那麽这些 tricks 就会变成另一个语言的标准,
在 C99 制订以前这种东西也叫 trick:
typedef struct t {
int x;
char y[1];
} T;
void foo()
{
T *data = malloc(sizeof(T) + sizeof(char) * N - 1);
...
}
至於你问 template 是不是 trick,
答案当然是 NO,
template 是 C++ 原有的机制,
它不是什麽 trick,
因 template 而生的 generic programming 也是本来就存在的;
如果你要说的是 template metaprogramming,
很遗憾,
它跟上述的 trick 一样「曾经是 trick」,
因为 2005 年 C++ TR1 的出现,
已经不再是 trick,
参考参考吧:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1745.pdf
遗憾的是 C 的 syntax 就是没办法直接支援 OO,
不能直接支援的不单是 OO 而已,
template metaprogramming 的 turning-complete 性质也是,
要模拟它们的确不是完全没办法,
但那些方法要从 trick 变成标准实在是难上加难,
这都是因为 C 实在太简单的关系,
上面的例子之所以可以进入 C99 标准 (其实 C99 有把外观稍微改了一下),
是因为它还没有太奇怪。
> > C++ 在 performance 上的问题没你想的夸张,
> > 甚至是 memory 的占用上;
> > 就算是使用纯 OOP 撰写程式也不会比 C 差到 10% 以上,
> > 擅长使用 template + OO 的人可以降到 1.5 - 3% 左右,
> template 为何会比 C 省 memory, 举个例吧
上面第三行跟第四行是一组的,
第三行说「比 C 差到 10% 以上」,
第四行是说「降到比 C 差 1.5 至 3% 左右」。
实际上我这种说法还是在不公平的状态下比较,
如果你真的把 C 用 OO 的样子来写,
C++ 反而会占有优势,
因为 C code 是由人手工去撰写那些 code,
但 C++ 是由 compiler 来产生那些 C 必须以手工方式撰写的 code,
compiler 的最佳化对於不确定的事情非常胆小,
但是当整件事情都是 compiler 决定的时候,
compiler 就有办法很大胆的做最佳化。
用 C 模拟 OO 还有一个非常致命的地方,
C 只能用 function pointer 这种东西,
C++ 却能用 functor (function object) 这种东西,
而 functor 的好处就在於它可以 inline,
以致於独有这个「以空间换时间」的好处,
如果发生位置在於 80/20 法则中的 20% 部分,
那可以说牺牲那一点点空间是非常值得的,
这个好处 C 没有模拟的余地,
C 只能模拟相似的行为而已。
> C 一定可以做出和 C++ 产生的 machine code 一样的东西, 最多就是 source 大点
姑且不论产生 machine code 完全一样是否办得到 (其实不行,上面有说原因),
「最多就是 source 大点」这句话显然考虑不够周详,
这也代表了许多事情:
1. compiler 能做的最佳化减少 (compiler 100% 确信的事情变少)
2. debug 和 maintain 成本增加 (compiler 不会生错 code,人会写错 code)
... (剩下的请自由发挥)
> obj C 的 dynamic typing 才是 C 一类 static typing 不能比的
C++ 的爸爸对於 C++ 的效率有所坚持,
过大的弹性被 C++ 舍弃,
换来的就是远高於 C 的弹性、便利性,
且仅以微小的效率/空间损失做为交换。
obj C 这样做也是一种 trade-off,
出发点跟 C++ 差太多了,
如果你不清楚「为什麽 C++ 有提供某某机制」或「为什麽 C++ 不提供某某机制」,
这世界上确实存在一本书可以告诉你答案,
那本书叫做 The Design and Evolution of C++,
这是 C++ 的爸爸写的,
从 C++ 一开始存在一直到 1994 年的心路历程全部都记在上面,
也正因为这本书存在而且我读过它,
所以我有办法很笃定的这样说;
设计出 Java 跟 C# 的公司,
总是忽略了这些考量而刻意以片面的方式攻击 C++,
说一些不着边际的广告词,
在这背後其实还存在许多商业因素考量,
然而这些说词影响世人甚钜,
所以在讨论的双方都弄清楚事实之前,
比较它们是没有意义的,
也因此我前一篇才刻意简短的说因为面向不同难以比较,
要比较当然可以,
但是在比较之前大家都有不少功课要做 (包括我自己),
可是我不想为了战语言而再做更多的功课,
语言是一种工具,
在不同的环境明智的选择适当的工具才是正确的态度,
一开始我就这麽说了,
何况你目前为止所做的比较并不是优劣的比较,
而是输赢的比较,
这种比较的意义确实不大。
> > 而且 memory space 也不会因为使用 template 占用太多;
> > 使用 template 来搭 OO 可以压低继承树增加 performance,
> > 甚至是将 runtime 的 recursive call 改写成一连串相异函式的 tail-call,
> > 懂得如何帮 template 瘦身的人更不会因为 template 而让程式特别吃记忆体,
> > 这些都有书可以学。
> > 事实上 C 那个不是标准化,
> > 只是 x86 上有特别订一套而已,
> > 你去别的 architectrue 上 try 也是死成一片,
> MIPS, ARM, PPC, FR 都有标准, 但 C++ 各家 compiler 差太多, 怎麽定
> 一个 pointer to class method 就有 n 种作法
这是因为 C++ 保留给编译器厂商实作的弹性,
不过这东西其实跟 C 一样有办法针对各 architecture 制订公约,
只是 C++ 从来就不曾如 C 这麽盛行过,
所以设计 arch. 的公司也没有什麽兴趣去订。
标准规格书说「保留给编译器实作」这件事,
并不是真正代表「什麽都看 compiler 高兴怎样就怎样」,
只要 C++ 能跟 C 一样深具影响力,
arch. 的厂商还是会像 C 那样订出一套公约让大家遵循。
> > 因为 gcc3 用 bison 做的 parser 实在有够烂,
> bison is a version of yacc, bison != yacc
> 一堆 compiler 用 byacc
我知道它们的差异,
FreeBSD 从以前到现在 man bison 都会在第一页前几行看到,
我也读过 O'Reilly 的 lex & yacc,
这本书也简略的比较了 yacc 和 bison 的异同,
所以倒是不必担心我不知道这件事,
然而正因为我知道它们的共通点在哪,
GCC 遇到的问题又落在共通点上,
所以我才会直接这麽说。
> 如果用了非标准的 pointer access, 是写的人有问题
> 就如 ++i 的问题一样
嗯,其实事情也没你想的这麽简单,
看看这个 link 在「Run-time issues」中的第一点:
http://nchc.dl.sourceforge.net/sourceforge/open64/gcc3-build.pdf
这种 code 在 C 可以说是常见的写法了,
10 几年前大家用 GCC 2.x 的时代都用得顺到不行,
结果一上 GCC 3.x 就炸了,
GCC 2.x 可是活了非常久的时间 (甚至到现在还到处凌虐各国的研究生和玩板子的),
大家的习惯都养成了,
现实就是这麽残酷。
另外,虽然这 link 里面是 C++ code,
但这是 C 本身就存在的问题。
> 较少人使用 (...) 的 function, 这是个很少出现的问题, 也很好改 search/replace
> 旧的 binary 和新的还是能相容
如果你看过真正的 legacy code,
你可能就不会这麽说,
过去为了 K&R C 跟 C89 的 function prototype 大地震,
各家 compiler 对 C89 的支援性不一,
为了相容两种 function prototype,
曾经出现过这样的 function declaration 遍布於所有 source code:
array_t *allocate_class_by_size P1(int, size);
array_t *allocate_class P2(class_def_t *, cld, int, has_values);
char *read_buffer P4(buffer_t *, b, int, start, int, len, int *, rlen);
注意是「遍布於所有 source code」喔,
要 replace 当然传统的 sed 就能办到,
但是大家当时选择的是在 coding 的时候就先 keyin 了;
还有更多例子可以举,
举都举不完。
至於 ellipsis (...) 的问题并不是很少出现的问题,
小有规模的系统程式和应用软体都会用上,
这种东西也不是光靠 editor 就能 search/replace 的,
因为「第一个 type 是什麽」这件事 editor 不知道,
没稍微看一下 function body 在写什麽的人也不会知道,
所以事情并没有你想的这麽简单,
BBS server 这种小小程式就够你改到手酸了,
不信去抓个老旧版本的 BBS source code 来试试。
> C++ 的问题如 pointer to class method != fucntion pointer,
> callback 刚好是 function pointer 主要用途, 又常是 class method
C++ 的 non-static member function pointer 设计概念,
是希望 member function pointer 跟 object 能分离,
所以出现了 .* 和 ->* 这种 operators,
加上 non-static member function 本来就会跟某个 obj 绑,
如果不是自己制造的 platform (如 CLR 跟 JVM),
扯到最後几乎就等於是要自己去写一个 OS。
native 的东西本来就会面临到「要自己写一个 OS 才能支援」的问题,
问题是「新的 OS 要能盛行」是一件不容易的事情,
首当其冲的就是 driver 的支援,
写出王道的 OS 但是各家硬体厂商不甩你,
2007 年的现在要说服这些硬体厂商 support 你实在很难,
要说服众多懂得写各家 hardware driver 的 hackers 来 support 更难,
最後不管写得多王道最後都只会被埋没而已,
这也是为什麽要 VM 执行的语言跟 native 执行的语言不应比输赢的原因之一,
只能说各有利弊,
在正确的场合选择对的就好了。
> 造成很多 GUI 程式的 callback 大改, 又无法 binary 相容
这是一开始就存在且已知的问题,
并不是要「大改」,
还是一开始就要花费成本去「设计」,
但是久了也会变成一个设计阶段的经验,
成为 maintain 成本的只有「无法 binary 相容」,
但这本来就是 native code 的宿命,
除非先去炸了 Microsoft 跟各家 *nix 大厂。
> include, export 等让书上的 hello world 都要改, 程度上很不同
header files 的问题用 autoconf 就能解决,
export 目前没什麽 compiler 支援,
书上也有教 workaround 的方法,
这方法也没有惨到跟上面的 P1 P2 P3 P4 ... 一样可怜,
所以这个成本其实还蛮低的。
> OS 不能用 C++ 写, 因为会有 compiler/archiecture 相依性,
> object 的 memory allocation, method calling 不同 compiler 都做不一样
> dynamic linking 也完全没定义, 这种东西不能写 portable 的 OS kernel
> 古早的 OS 有用 PL/I 等高阶语言写, 都不好 port, 才会弄出万用的 C
设计 architecture 的是老大,
如果他不想当老大,
那做 OS 的就是老大,
architecture 没规定 compiler 要怎样,
OS 就不能规定 compiler 要怎样?
C 也是没规定在标准规格书上,
愿意当老大的人出来讲一句话就会遵守了,
只是现在当老大的只管 C 不太管 C++ 而已。
当然想当老大也是要有本钱,
在 2007 年的今天不是设计出 architecture 写出 OS 就能当众人的老大,
还存在非常多天时地利人和等等非技术性因素的问题,
否则也只能当成两三只小猫的老大而已,
有本钱当众人老大的说 NO,
那我们也没有办法;
然而,
这绝对不代表「OS 不能用 C++ 写」,
那是完全不一样的意思。
--
Name: Tseng, Ling-hua E-mail Address:
[email protected]
School: National Tsing Hua University Department: Computer Science
Interesting: C++, Compiler, PL/PD, OS, VM, Large-scale software design
Researching: Software pipelining for VLIW architectures
Homepage:
https://it.muds.net/~uranus
--
╔═══╗ ┼────────────────────────╮
║狂狷 ║ │
* Origin:[ 狂 狷 年 少 ] whshs.cs.nccu.edu.tw ╰─╮
║ 年少║ ┼╮
< IP:140.119.164.252 > ╰─╮
╚╦═╦╝ ╰
* From:61-230-220-241.dynamic.hinet.net
─╨─╨─ KGBBS ─ ◎ 遨翔"BBS"的狂狷不驯;属於年少的轻狂色彩 ◎
1F:推 ykjiang:推 61.59.14.41 04/28 01:20