Skip to content

Instantly share code, notes, and snippets.

@FrankHB
Created August 8, 2022 09:42
Show Gist options
  • Save FrankHB/c3eaf1fe4b61f6eb89a9329a9bfd110b to your computer and use it in GitHub Desktop.
Save FrankHB/c3eaf1fe4b61f6eb89a9329a9bfd110b to your computer and use it in GitHub Desktop.
关于所谓“弱类型”的误导问题

缘起

因为 Bilibili 莫名其妙吃回复,那就在这里存档。

原始内容见这里

如无另行指定,默认回复 @LanceMoe

预备 +TODO 改换文体,+FAQ

(不过虽然议题有相关性,仍然远远不够继续升级话题就是了。)

引用内容

(无关内容略。)

LanceMoe回复 @喜羊羊与刘备 :本来设计之初就是强类型的语言一般都不需要三等号,因为三等号本来就是给弱类型时候设计的双等号擦屁股用的[doge]

2022-07-16 18:44

LanceMoe回复 @星海如昼 :这个属于运算符重载,不是隐式转换 [doge]

2022-07-16 18:45

LanceMoe回复 @STEStellar :除0和对0取mod在类型安全的语言都是要抛异常的,建议你了解一下PL的基础知识:类型理论

2022-07-16 18:46

LanceMoe回复 @不氪金之后 :是b不能为0,除0和对0取mod都是应该抛异常的

2022-07-16 18:47

LanceMoe回复 @--诺安-- :没有说需要c和c++,况且c语言是弱类型,c++是强类型,c++和c早就是两门完全不同的语言了。做项目当然要选合适的语言和框架,现在正常来说,预判项目是大型需要多人合作的,新起项目都是首选强类型语言,比如前端原来用js现在基本上都是用ts(ts整体还算类型比较完备的),原来可能没得选必须用c起步的现在起步尽量用rust。现代语言基本上都有类型推论,所以也不需要显式声明类型,没有历史包袱的情况下写起来方便度和弱类型的东西也没什么太大区别了。

2022-07-16 18:54回复

幻の上帝回复 @LanceMoe :除0抛异常……C:?C++:?

2022-08-06 22:04回复

幻の上帝回复 @LanceMoe :如果你真正经学过PL,就不要用所谓强类型/弱类型这样的混沌说法。所谓弱类型也不是指类型检查强度低下,而是指根本上瞎指派类型+滥用多态。

2022-08-06 22:06回复

LanceMoe回复 @幻の上帝 :上来就喷?强弱类型虽然没绝对界限,但是几门语言放在一起比很容易判断。你只要看这个语言设计本意,是不是编写代码时需要“经常”做隐式cast、或者类型的decay是不是“通常”是隐式的就能轻松判断,我从来也没说什么“类型检查强度低下”这种话。比如c的malloc返回void*可以分配给任意type的pointer,数组传递会decay成指针,这就是弱类型。c++相当于c的差异点都是强类型的特征,如果这两门语言一比,那c++显然是强类型,但是跟rust这种新出的设计更先进的语言比,没有除0异常之类的点,那么跟rust比c++略弱,但是依旧是强类型,c++不搞除零异常只是为了最大程度兼容历史的包袱。有什么混沌的?难道有人认为js和lua这种是强类型?正经学过PL你告诉我什么叫瞎指派类型,我是没听说过。你的意思是隐式cast?更加通俗的说,弱类型简单来说就是非常容易让人在不注意的时候,因为类型问题犯错的语言。我回复也是因为这个层主在这边说是因为“动态语言”的0和"0"导致的,我只是想说动态语言也有强类型,静态语言也有弱类型的,跟动态静态无关。弱类型本来就是方便大家沟通产生的概念,不然你出去咋描述,把强类型的特征和弱类型的特征全部说一遍?

2022-08-07 13:14回复

幻の上帝回复 @LanceMoe :我不知道你是哪来的PL基础,总之少拿民科理论开腔误导。

动态语言跟强弱类型自然无关,但是很遗憾你的理解错的离谱,虽然这种错误理解流行得像模像样,以至于我仍然不得不见一个就纠正一个以免继续流毒。

所谓强类型(strongly typed)就是在负责在typing environment给出type assignment的type formation rules里明确指派nominal types并且要求接口约束能确保类型保持compatible的设计,对立的就是所谓的无类型(untyped)或者单一类型(unityped),而本来就没什么弱类型,就跟本来就不存在什么面向过程一样。你杜撰的差别是在强行扭曲原意,架空原本需要描述的性质。类型检查强度低下就是分不清typing和typechecking导致的常见脑补出所谓强弱类型的例子,但你既然没这样想,那么就更没有什么借口使用什么弱类型。

C和C++都是典型的强类型语言,而且符合最早的Liskov那时候定义的强类型仅限在函数签名的狭义的情况。你提及的所谓C和C++的差异属于type compatible rules/polymorphism机制的不同,根本没什么模型能给出可比较的强弱性——比如说C++根本没像C定义compatible type,却又多了conversion function之类的自定义polymorphism,你何德何能能把这些具体的不同设计抵消比较出“强弱”?Rust差得更大,就更加没可比性了。

C和C++没除零异常是你典型想当然的遗漏。刻意不用异常也不是你所说的理由——要知道,除零异常的标准化是所有UB中相对容易的一类(最简单地,搞成信号)。真正的理由虽然还是接口语义上的决策,但明确和想要什么类型系统达到什么目的无关。用WG21的话,这是所谓的narrow contract并且不应当被widen,宁可故意允许UB,也不添加用户程序无法回避的检查开销。

幻の上帝回复 @LanceMoe :你所谓的“方便沟通”很大程度是伪需求。你确定正经的讨论中真有把所谓“弱类型”一网打尽的共性存在(除了刻意设计一门表现可以烂到什么程度的语言专门拿来黑的情况)?尽管把0和"0"混同的类似设计在不止一个语言中存在,但这基本就是没搞清楚在自己设计什么的无意识跟风。这样的一个现象本来就是artifact,堪称反模式。但另一方面,这种性质没流行到需要专门污染词汇表去命名的程度(非得硬说,那就是一种type confusion,但这一般是描述用户程序的)。

另一方面,没有混淆0和"0"的正儿八斤的动态语言多得是,甚至Lua的设计者都知道不好在Lua5.4给改了。

至于具体的转换特性本身,我已经说过了,就叫强制(coercion)。这是个比“强类型”还老资格得多的跨语言标准术语,也是Lua手册中的用词。(某些不上道的C用户学歪了导致没意识到“强制”是指隐式转换这样的常识……就不要怪我咯。)

不懂也没什么,但是还要出来乱说给正经科普增加工作量,就是你的错了。

注释 这里还有个关于 LanceMoe 对 void* 的理解偏差描述的回复被 Bilibili 吃了没显示。

LanceMoe回复 @幻の上帝 :挺有意思,先说强弱类型是民科说法,然后你花了这么大篇幅讲你认为的“强类型”是什么,然后又说“方便沟通”是伪需求,合着是在这说废话。[OK]别说什么除0不抛异常是为了零开销,要真是一切零开销,现在c++这套异常系统就别做了。除0定为ub本来就是历史包袱,在大部分主流处理器里都产生硬件中断,就算不抛异常也可以根据ieee754返回非数常量,没必要定为ub,事实就是改掉包袱远比新起一个复杂。然后也别说什么你要做“正经讨论”,真要做“科普”麻烦你写篇论文给个你的建议,你再造个概念来“方便沟通”,我支持,否则就别说什么“方便沟通”是伪需求。欢迎多做做你觉得是对的“科普”。我本来那层只是想提醒“弱类型”不代表是“动态语言”,无论怎么说这点不接受反驳,无论你怎么定义,这两个都不能划等号,所以不知道你在杠什么。还有我给你提个建议,个人觉得像你这种上来就给人扣“民科”帽子的,对“科普”是最大的阻力。“正经讨论”就别在这盖楼回复了。

正论

直球回复

我是觉得挺无聊的,你自己明摆着不搭边的外行,非得强行出头连续回复一堆人,推销你自己的观点,动机是什么,显摆你理解更深刻?“弱类型”不代表是“动态语言”没问题我没反对,但是你其它地方这槽点出现率得也太离谱了,还特意扛着PL基础知识的旗号…… exm ,PL 你说而是这样的?

我花篇幅是因为即便这种性质恶劣的误导(尤其是当事人九成九毫无自觉)平时一两句话能忍则忍,但是集中连续输出错误忍无可忍无需再忍,就逮到猛薅以确保源头上的效率最大限度打击这种谎话说多了就是真理的心态而已。这也是主流民科的态度,只不过典型的民科更加刻意。另一方面,稍微细究一点就发现PL界本来就没你所谓的这种需求,“弱类型”的提法过一遍发现不是以讹传讹就全是一知半解的外行口嗨的发明,你非得无中生有,别人不当你民科无理取闹还能咋地?

强类型到底什么东西,弱类型的说法到底多不靠谱,这种随便一搜二手文献都马上七七八八的东西,对知道什么“PL 的基础问题”的人来讲,真需要我再继续指出来源吗?还有你真能指望一个中括号都会替换歪的地方里贴啥靠谱的文献引用?

除零问题我看你是彻底的外行,不知道这里的开销到底有多大——不止是运行时开销,还有维护spec上消除硬件实现差异的问题。虽然我说过“最简单的”做法,这仍然工程上极端复杂,跟往 C++ 里加 Windows SEH 没太大(不)可行性上的差别。这种异常要整清楚就得把常见 CPU 的异步异常机制在 C/C++ 的层次上整明白,干掉所有不一致的部分,光这坨就比整个 C++ 异常都复杂了。而刻意拒绝包装内建操作符的错误条件为 C++ 这样而同步异常则是明确实现开销上的考虑。(当然你也可以硬说 C 没 C++ 那种异常算“历史包袱”所以连这种机会都没有……那你开心就好。题外话,我不反对缺乏控制作用是 PL 设计史上的包袱,但支持控制作用的实现方式就不应该钦定是“异常”。)关于 C 或 C++ 这方面设计决策不服的,建议直接提交 proposal 。

Issue 1

首先的问题是为何“弱类型”之类的话题自然被视为民科,这个最基本的一般理由上面已经提及不再解释。

可能需要补充的是如何断定“弱类型”这个说法有资格被视为民科内容、应当被视为民科内容以及属于伪需求的理由或推理过程。

其一在于 PL 界的见闻。不管是正式的论文还是还算正经的 PL 讨论(比如 LtU 上的)实际上都难以看到会提到这个说法,乃至可以认为相关人士已经对这种提法的离谱性和搞乱说话人含义的危害程度有充分认识而刻意回避了。

其二,更直接的理由是有不止一个独立来源给出反对“弱类型”的提法,基本上是这方面比较内行去深究过这些问题人的共识。

其中的技术理由各有不同,但共同点是同一“弱类型”这个概念来源可疑、定义模糊且不大有希望挽救,不如彻底放弃。

TODO 补充参考文献的内联展开片段。

当然作为已经流行的讹用术语,它的存在感已经无法让一般二手文献忽视了。但是,其中除了出处相对地靠谱以外,说法仍然不得细究——有很多判定标准上无法推敲的地方,例如“什么算弱”“如何衡量相对的强弱”。

如果要以“类型检查的强度”来建模而允许明确定义相对意义上的“弱类型”弱在哪,其实并不是不行。但至今没找到这种归纳强度并建立包含序理论意义上的模型的正式工作。(作为对比,抽象理论就有,所以谁比谁更能抽象在 PL 的一般意义上是可比的——虽然也没落实到具体 PL 上。)

退一步讲,就算有这种研究,也不可能建立放之四海皆准的全序关系(而且不限于类型检查)。这在先前提到的 C 和 C++ 类型系统的差异的例子上就已经暗示过了。

然而这个方向更大的问题是把弱类型和类型检查混淆根本就是说不通。关于这些讹传的背景成因,更严肃的技术分析和观点概述很早就已经有了,不再赘述。

Issue 2

C 和 C++ 不标准化(整数)除零异常显然不是单纯的历史包袱。

因为部分历史原因以及现在仍然强调的“性能优先”的习惯,C 和 C++ 的 /% 内建操作通常直接实现为可被 ISA(指令集架构)支持的操作。

在遇到除零的错误条件时,ISA 操作通常规定会引起硬件实现支持的异常。这种异常通常是异步的。这基本不是历史包袱,而是这种方式允许利用的硬件实现意义上的空间分散性允许物理同时直接支持并行处理。其中的历史包袱只有具体异常到底是单独的硬件器件(比如协处理器)还是和其它中断或异常机制一同实现的问题,但这在 ISA 以上的软件原则上是不可见的。

当然,以 C 和 C++ 为代表的 ISA 以上的软件实现难以利用硬件的并行红利,那就是另一个问题了。不过这也不是历史包袱,而是这类语言设计的抽象机语义本身就和并行的体系结构模型不搭——要说历史包袱,那么所有 ALGOL-like 语言整个都是。

这个意义上,不以 ISA 异常标准化除零异常的应该容易理解。考虑到具体设计的细节,比如如何与其它异常交互之类的问题,ISA 的异常远比现有的几乎所有高级 PL 中的异常机制都复杂。除了先天异步+并行和大多数语言的语义模型不搭这个麻烦的问题(比如考虑如何通知事件完成或取消——注意这里的延迟限制比外部 I/O 严格得太多,实用上这里不能引入本机线程),还在于客观上无法确保不同的 ISA 一定会有相同的设计。对标准化 PL 来说评估兼容不同的可能实现,又要尽量不引入间接层次的性能开销,这很要命。

所以强调兼容性、连调用栈都直接架空的 C ,此路不通,对一般的异常充其量就是提供了一个信号表示不是无事发生,但除零这个问题没到 SIGILL 这种程度还是完全无法保证。继承 C 的 C++ 更加不待见信号上下文对程序的运行时操作的限制,更加没扩充的余地。而 C++ 的同步异常扩充为异步异常在工作量上也是异想天开——不说绝对意义上 C++ 同步异常的复杂性已经非常离谱了(比如考虑 ABI 兼容性)。

另一方面,不想要 UB 去实现替代手法本身的工作量实在很低——最简单地,加个 if 判断,返回错误码。但 C 这样的抽象捉急的设计中,if 加在哪也是有讲究的。加上早期连内联都不健全的年代(这确实是历史包袱,但是也不全是——现在也不可能保持 ABI 解决跨翻译单元内联兼容问题),标准化封装个带有 if 的“安全”操作实在没现实吸引力。

让 C++ 异常包装错误条件看上去更正常(因为除零异常逻辑上其实是可以同步的,所以可行),但是 C++ 异常机制的一些过度设计(比如过于具体的异常对象类型和过强的 unwind 保证之类)和现实(非历史)的包袱(比如异常运行时 ABI 兼容性)会让八成用户明确反对这里使用异常的必要性。甚至还有不少 C++ 用户至今以此反对整个 C++ 异常机制,那么自然更加无可救药。

这些背景下,保持内建操作整数除零 UB 自然是共识。(说实话,我很奇怪 C 和 C++ 用户会有这方面认知偏差。)

(至于浮点数除零 inf 那是成熟的硬件方案里早就支持了,不用白不用。但是浮点数这玩意儿细节远远更头疼,不过这完全是另外的问题了。)

原则上,更重要的问题在于 UB 是被期望的。与之相对,滥用检查避免包括 UB 之类的“错误”的所谓防御性编程不受待见,因为它本质上不信任任何接口的用户,以至于用户即便完全清晰无误地理解了需求,也无法干预次等的实现——除非把实现拆了自己改了。但是既然我都得自己实现了,那么要你何用?

WG21 的讨论中,这种预期的无错误检查的接口和存在“防御性”检查的接口被分别称为 narrow contracts 和 wide contracts 。一般地,只提供 wide contracts 是有害的,因为检查的复杂性基本可以忽略,总是存在一个更自由的 narrow contract 的版本的接口,而同时满足 narrow 和 wide 的需求——封装底层的 narrow contract 的接口并添加检查就得到了后者。而反过来只要不是知道实现细节,一般是做不到的。

一个根本原因是,C++ 这样的语言不足以精确建模像度量性能这样的性质(顶天能在文档里约定一个复杂度要求,连检查都做不到),而相同接口的劣等实现的却又实实在在地影响程序的行为(甚至可能作为攻击向量),这就是一种抽象泄漏。

这种不对称性揭示了一些更深刻的普遍规律:如果想要隔离接口及其实现,又不想有实现开销的惩罚,那么 widening contracts considered harmful 。同理,一般意义上依赖这种问题操作的所谓防御性编程是会自动破产的:任何无条件地设定前置条件而拒绝让它的直接用户配置是否启用,是一种无能的反模式。

更正常一点的做法是,把接口的检查集中到(不)信任边界上。例如,不能信任最终用户的输入,就集中检查最终用户直接决定的输入,但是不要再嵌套调用中重复检查——这不仅多少在持续损害最终用户的体验,还可能会掩盖遗漏检查的根因,造成更大的问题。再者,还有不少严重的灾难性错误(比如硬件失败)是根本无法检查的,对系统状态一致性的维护天然地就依赖其它的手段。

是时候对鼓吹无能和剥夺用户自由的愚蠢编程臆想作斗争并纠正这些真正的历史包袱了。

@LanceMoe
Copy link

LanceMoe commented Aug 8, 2022

我大概知道你在意的点在哪里了,bilibili那边回复只能显示是回复我,我也看不到你是针对哪条回复的,看起来你是很在意有关pl的这句话,那么我收回那条回复。另外,你质疑为啥我要回复这些人,你可以完整看看上面回复区我回的人都说了什么,人家问我,我礼貌性回复,我也很少在bilibili评论区发言,你可以看看下面人的整体水平,在这种情况下我认为讲强弱类型是有意义的,至少对“动态类型语言”有误解的人不少,你把我回复他们的上下文看一下应该就懂。至于你认不认同“强类型”和“弱类型”这件事,你认为讲强弱类型是民科,我也不会在“正经”场合去写强弱类型,即便是用,我也得去先定义什么是强弱类型。我在那个回复里所有的回复,出发点只有一个,不要妖魔化动态类型语言,我希望他们理解不是所有动态类型语言都是“弱类型”。在不同场合有不同讲法,如果你能跟我回复的这些人这些人把概念讲清楚,那欢迎你继续“科普”。对强弱类型如何分类,这点是每个人都有不同意见,但是同样是动态语言,至少没有人认为lua和js这种是强类型的,也没有正常人觉得python是弱类型,在这个情况下我不认为讲“弱类型”有什么歧义。如果你能给出一个更科学简便、容易“科普”的概念,来形容现实存在的这个情况,欢迎你消灭全世界所有称呼“强弱类型语言”的讲法。这就像物理界更新了这么多理论,“牛顿力学”依然在基础教育体系里存在,有现实意义的概念广泛存在,我并不认为是你所谓的“性质恶劣的误导”,这只能代表你个人的情感意愿。你如果真有这么大意见,欢迎你去“源头上”喷(请你至少先把维基上各个词条干掉,因为在你眼里根本不应该存在这个词),就像你文章最后一句一样,你可以去逮着狠薅,把全世界所有相关描述都给干掉。

@LanceMoe
Copy link

LanceMoe commented Aug 8, 2022

另外,包括vczh在内,都默许“强弱类型”的分法。我戒中国互联网也四五年以上了,基本上不说话,声量跟你比起来那都是听不见的程度,大多数时间也就看看潜水,所以你来薅我没多大意思。我人也不在墙内,pl这些也没在墙内学过,不过大概跟你说的pl不是一回事?至少我大学时那节课主讲教授在课上提过这个问题,认为除0不抛异常是不“类型安全”的,编译原理我们用ocaml学的,所以你说的对,我是个“业外人士”,不过不是“民科”(我根本不是科学家,你才是科学家,我就是个开发工程师而已),这点就不需多言了,并且,其实你写的大部分观点我都认同。

所以为了实现“对鼓吹无能和剥夺用户自由的愚蠢编程臆想作斗争并纠正这些真正的历史包袱”,你不如先把全网的“二手文献”源头(先别全世界了,清理维基百科还挺难的,不如先从中文互联网开始吧)先薅一遍?比如先从轮子哥开始吧233,他这个论述影响比较深远,贴个链接给你(vczh轮子哥知乎号已删,不过有网页历史):https://web.archive.org/web/20160923054902/https://www.zhihu.com/question/19918532/answer/21645395

比如说这里还有别人复制转载的:https://howiezhao.github.io/programming-terminology/language/strongly-weakly-typed.html

你搜索一下内容也许可以找到更多转载。我也很喜欢认真的人,至少在墙内,愿意认真讨论问题,贴论据的人很少,这也是我这几年基本上不在墙内互联网发言的原因之一(当然莫名其妙吞贴这种也是原因之一)。当然,像回bilibili这个层主,我是实在没忍住,我是觉得lua把number隐式cast到string的这种很差的设计品味问题不应该全体动态类型语言背(“弱类型”就是个不好的品味,当然不否认早年动态语言品味差的确实很多)。

ps.以上是站在pl立场,我支持你的“严谨”,不过站在工程的角度讲,“弱类型”几乎一定会导致项目难以维护变成屎坑,根本不是写注释和文档之类的操作可以挽救的。比如之前像php这种恐怖的东西,过去二十年都很流行,但是最近几年新启动项目几乎没人用了,js这种es规范上修修补补,不过大部分大型项目也都上ts再去编译到js了,弱类型的东西总要想办法填坑,像bilibili这种,像这种运行时都不检查异常的错误一旦出问题不花很久基本上查不出。我不知道你自己除了编译器和基础库以外,平时维护过商用的业务工程代码没有,如果你做过,我相信你会明白为什么“强弱类型”这个概念会在口头上这么常用。

以上。

@n0099
Copy link

n0099 commented Mar 6, 2023

有请

  • 四叶信安底层壬上壬上海贵族C++带手子杨博文阁下 @yangbowen
  • 四叶头子CS硕士PLT理论中级高手仏皇irol神 @kokoro-aya

如何看待本文以及 https://github.com/FrankHB/pl-docs/blob/master/zh-CN/typing-vs-typechecking.md

  更严重的问题在于“强类型”(strong type/strong typing/strongly typed) 这个说法。考虑到这个概念词源上基于 typing ,引用起来却杂糅了 typing 和 typechecking ,不同作者对什么是足够强的理解还不一样,混乱可见一斑。

  这里的一般建议是避免混乱的用法。如果不能避免,仅在确定类型时使用,而讨论类型检查时直接以具体的机制(对 ××× 类型的检查)来避免混乱。特别地,应注意避免“弱类型”这样望文生义还模糊了原本可能已分清界限而使之失去实用价值的生造词。(后者的使用者通常还具有其它的认知混乱,比如无法区分强制(coercion) 和铸型(casting) ,不清楚强制可以作为一种多态(ad-hoc polymorphism) 而不一定有损类型安全性等等。)对特定实体声明保持类型的性质,使用 manifest typing/latent typing 论述——这样“强类型”这个说法才有一些无歧义的生存空间,不至于像“弱类型”一样鸡肋且造成误会。

注释 技术上,以削弱类型检查为由而强调“弱类型”的造成的混乱是相当容易理解的。须知,一个不支持隐式类型转换的强类型语言,其类型规则中可以通过添加更多的 typing 规则,规定隐式转换所在上下文的源和结果应当具有什么相对于特设上下文(这里自然不能无条件适合任意类型,而需要明确更细粒度的条件)多态的类型,来引入原本不存在的隐式转换,堂皇地变成了所谓的“弱类型”。问题来了:为什么一个已有的“强类型”语言中加入了更多细粒度的类型规则,反而变“弱”了?明白 typeing 和 typechecking 的不同之后,这类问题就变得不攻自破;而“强”“弱”类型的说法是如何散播愚蠢的这点也愈加鲜明了。

  另外,虽然不如上面的提法直接,还有其它一些观点指出强类型和类型检查的“强度”无关。和上面的结论类似,为了避免不相关的误导,持有这类观点的作者也同样不建议使用“强类型”“弱类型”的说法。

足够光滑的语言不需要提供特设的“反射”这样的特性,就能取得(不论是否是关于类型的)元数据,因为之前的元数据从没必要像“静态类型”一样预期被丢弃,而需要通过反射迂回地引入。作为特例,关于所谓的类型内省(type introspection) 之类在普通静态语言需要“黑科技”的特设接口设计的经典问题,通过不依赖类型系统设计方法的路径就被轻描淡写地消解了。与之相比,类型系统的具体设计的话题甚至琐碎得像是“实现细节”,实在没什么相提并论的必要。(当然,也应该不算白讲……)

疑似刻板印象之动态语言壬天天无意识地反射https://www.v2ex.com/t/910403

@n0099
Copy link

n0099 commented Mar 6, 2023

C++吧小吧@幻の上帝 https://www.zhihu.com/question/27762469

虽然只有译文(第 13 章)提到了关于 GB18030 的观点,但这里的忽略 GB18030 这种有代表性(由国家标准化组织而不是国际组织维护,对特定字符集子集优化)的变体,本来就是一个选型上的技术缺陷——特别正文存在一章单独讨论亚洲文本的空间效率却仅考虑比较 UTF-16 和 UTF-8 的时候。英文文本在这一章中甚至还链接到了中译文章,显然没理由不注意到“亚洲文字”在这里在乎的是什么。(当然,标题上打倒 UTF-16 的目的还是基本达到了。)

【管理员】幻の上帝 22:59:41
定义自然数的公认基础是皮亚诺公理。
确定自然数基本性质的皮亚诺公理本身确实不关心几是自然数的起始,它只明确有下限。
但是如果不是0为下限,其它用到自然数的地方基本就没卵用,自然数这个概念几乎(除了数学归纳法等极少数应用)就没存在的意义了。(而数学归纳法这类东西的基础也可以直接架空皮亚诺公理单独定义,当然这下基本上只能从0开始了。)
所以基于需求,把0不是自然数就是挺傻逼的。那些以1开始当自然数的定义还得加个whole number来描述包含0的情况,于是“正整数”冗余出来哭晕在厕所。
从0开始也是ISO 31-11钦定的标准做法。

【管理员】幻の上帝 23:15:56
如果新数学能教干净的话,至少不会有什么人对为什么unsigned会wrap之类的问题稀里糊涂。

关于追踪 GC 的实现。
启发式策略实际上呢……说白了,很多时候是瞎猜。
比如什么weak generational hypothesis?——好吧,某些样本的统计上也许真符合煞有其事。但这丝毫不能掩盖指望让用户“按常理出牌”而剥夺用户对资源控制的自由的事实。
顺便,关于分代GC,设计者恐怕也未必能讲清楚,具体几个代在什么场景下是最好的。搞不好实际profiling都很难设计用例。
这种人为附加的系统性分析困难有那么值钱么。(说不定这能解释难怪有那么多VM“调优”蹭钱的了?)
而用户实际上被坑的更多的是……为了回收资源这么些破事就得stop the world(PAD长即视感……),放到客户端一旦能体验出来就是个笑话。
别说相对于引用计数总的开销有优势这些胡话,引用计数就是再不能忍,好歹也能平摊时间复杂度,在时间利用率和响应上没这么搞笑。
此外整体上,GC,尤其是追踪实现的GC,内存利用率令人发指。《Why mobile web apps are slow》指出要想流畅利用GC,需要准备至少5x的内存空间。 因为本性难移,没法指望GC实现技术的飞跃发展,所以至少在移动平台上,稍微“大型”的应用(游戏?),短期靠依赖GC的语言作为主要实现,已经被毙了。
老实说,我认为只要上面为例的这样成吨的冗余困难不被解决,GC就不适合放到严肃的系统语言里面作为默认配置。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#6%E7%8E%B0%E7%8A%B6%E5%92%8C%E5%8F%91%E5%B1%95

6.现状和发展
不算怎么应用,光说资源管理本身的实现技术上,大方向无非两个:在特定特定环境下更优化的GC实现以及确定阶段(比如说,源码)上的静态分析。
更严重的问题,包括标准化,还是在于人。
现状是,大部分用户连GC适合干什么和不适合干什么都一问三不知,却被默认使用GC的策略绑架了,根本就没什么机会细心思考怎么管理资源。
他们被灌输重要的是“解决问题”的抽象和具体手段比如算法和算法的实现,却不知道资源管理是贯穿“使用计算机”这个领域的核心问题,无论怎么都无法回避,用不合理的通用策略就是推卸责任,让解决问题的效果更差,而并不会让这部分自然失踪。
注意这里的效果包括两个方面。一方面是程序运行的性能可能不理想,这点前面提了;另一方面是表达意图的模糊。
后者和GC本身也没有直接关系,甚至糟糕的手动显式资源管理也能有类似的效果(一个例子是我坑了若干年的Auckland Layout Model的C++实现,里面线性规划的手动new/delete直接看根本不知道在干啥),但是影响恶劣到值得作为GC的缺点之一。
实际上,当我去阅读这样的代码,除非有更清楚的文档解释,就被迫去当人肉GC——根据上下文去猜什么地方该填充释放或者释放的资源之间的所有权。这是非常糟糕的体验,即便以对资源管理的认识不足为由需要一定的妥协,放任这种可维护性极差的代码也是不可理喻的。
所以说,未来要发展,最大的问题恐怕还是教育。特别是应该教会正视现实问题。

7.互操作性
这确实是非常闹心的玩意儿。语言在这里毫无疑问有一定的责任(例如C的对象语义和布局耦合),但更大的问题恐怕还是在于人——作为专业用户,却错误地依赖本来不保证靠谱的东西(通常的ABI都算)。
我不认为C++能解决这里的问题。比如说布局,虽然它已经通过non-standard-layout允许实现自由选择而提醒用户不要依赖这种不靠谱的玩意儿,却没有提供可移植的更明确的操作方法。
这个问题并不限于GC,而且本质上恐怕比资源管理更麻烦,因为这里依赖的历史包袱不是通过提升教育水平和生产力看着不爽的东西重写替代就能了事的。就是C和C++完全退出历史舞台也不太可能解决(虽然可以变通)。

8.易用性
除了不重视资源管理的教学导致的“易用性”错觉,语言设计者自己也有责任。 GC一开始是John McCarthy发明的,他某种程度一厢情愿地认为这是通用解决方案。 遗憾的是虽然AI winter击碎了不符合现实的幻想,这一块却被James Gosling等捡起来了。 我不完全清楚他们当时的动机,但是基于对C++复杂的叛逆心理,“易用性”显然是重要的一点。 现实仍然慢慢瓦解这些人的幻想,可惜就是不够快,结果“业界”充斥这一大堆垃圾。 不管怎么说,出来混就是要还的。 要是C++不幸会有这种麻烦的错觉,那么估计也会有脑袋清醒的划清界限发明在其它方面都能取而代之的语言。现实的C++没有这种强迫症,自动资源管理也几乎总是更清晰,很好。

9.安全性
像Java、C#这样增加运行时中间层的做法,在预测确定的程序行为(基本避免未定义行为)上的确有一些帮助,不过代价就是特性缺失。
操作更底层实现的功能就是features。虽然我不觉得底层的功能就应该内建,但是通过抽象使之不得不用完全不同的做法来实现不同层次的功能,这是对语言抽象能力的损害。
作为妥协,这种抽象的损失在DSL或许可以接受,但作为通用目的的语言来说,这种artifact是不合理的。
C/C++这里其实也并不怎么样,脱离了__扩展还是很多残废。不过,好歹没有发明不同的“native”之类的东西把问题更复杂化。
内建GC是一种乍看起来有效的策略,但另一方面,也就是体现对“资源”抽象能力弱,而不得不特 殊化特定资源的抽象能力缺陷罢了。
这里所谓的安全,基本也就是流于实现的马后炮而已。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-08-1416-tippisum

2.还是Finalizer。
“设计上不得不用Finalizer”,其实问题不在这里。

资源管理总是有限度的 *。最简单的例子,任何资源管理器(无论它应用什么策略,无论它是用户态还是内核态),都不可能自动管理不属于这台计算机的外部资源。(e.g. 数据库连接) 要完成这个抽象,必然需要一个可扩展的点存在。对于C++,这是destructor。对于Java,则是Finalizer。在这个意义上讲Finalizer的存在本身没有任何问题。你就算不用GC,不用Finalizer,仍然会需要一个机制来告诉资源管理器“应该如何回收这个资源”。说白了,这跟“任何抽象都必然会有某种程度的泄露”是一个道理。用这种泄露的存在来否定抽象本身我看来是不合适的。
问题的关键还是在于,本质上,某些资源是比内存更宝贵的——比如说内核对象,比如说数据库连接,甚至比如说互斥锁。但目前的绝大多数自动资源管理,它不管这些有的没的,不提供任何机制保证这些重要的资源能够得到尽早的释放,也没有在抽象泄露的时候给用户提供扩展和弥补的手段。这从根本上来讲其实还是个“实现细节”的问题,上面已经讨论过了。理论上并没有什么东西禁止资源管理器优先回收这些明显比CPU周期更重要的资源,只不过没有人做而已。Java假装根本没这回事,C#稍微好一点,弄出一个IDisposable,但最后的结果还是把问题丢回给用户。 至于对象死了又活,其实跟上面讨论的话题比起来,是个更次级的问题了。真要说的话,设计Finalizer机制的时候偷懒,搞出一堆乱七八糟的麻烦。但其实也仅此而已了,没有更多值得讨论的地方。

互操作性
是的,大部分语言的互操作能力都是翔。我完全同意你这句话。

为什么?因为语言是平台无关的(至少大部分语言设计上如此),而互操作是平台相关的。

互操作总是平台相关的。 *
道理很简单,有平台才有互操作。在一个理想的世界里,整个计算机上只运行着一个由某语言编写的程序。这个程序里也只有一个模块。这样当然没有互操作问题了。 ——别笑,实际上绝大多数语言的设计者在设计语言的时候可能都是这么想的。至少,我觉得C++委员会里肯定有不少人是这么想的。何况就像你说的,C++的二进制兼容已经是一坨翔了,破罐子破摔的心理负担几乎可以说是没有。

所以问题的关键还是——语言的抽象和实现的细节之间到底如何找到一个平衡点。有不少语言都过于看重抽象能力(有的时候甚至是盲目的看重抽象的强弱,而忽视抽象是否有良好的定义,是否能够尽可能的避免泄露等,这是另一个问题),而忽视实现的一致性。而C++我感觉就是其中的一个几乎是极端的例子——通过不断的增加各种语言特性,C++的表达能力,绝大多数语言都难以望其项背。但这些强大的表达能力背后却几乎没有多少一致的实现。导致的结果就是只要你还能把所有的库文件都#include进来,那么感觉就会非常的爽。可一旦开始准备要跨模块……呵呵呵……

C语言本身的标准里虽然对于实现也没有加入太多的约束,但可别忘了平台的开发者。把MSDN和POSIX里关于C调用约定的部分都加上,恐怕还不能简单的说C ABI Compatibility只是个“错觉”。——毕竟互操作是依赖于具体平台的。 至于COM Interop,是的这东西真的极其傻逼,全方位的傻逼……但问题在于,如果C ABI Compatibility也是“错觉”的话,那它就真的是唯一一个确实有良好定义,而且确实能用的ABI标准了……

至于Java,其实JVM本身就是一个平台。Java只要搞定和JVM的互操作就行,剩下的是JVM的问题。C#同理。

安全性
当然,安全性可以只是一个feature,也可以是一个requirement。这对应不同的应用场景和不同的需求。对于C++,反正一段C++代码已经在你的计算机上执行了,那你还是认命吧。C++运行时对于一个C++程序到底会不会把自己跑飞或者把整个系统跑飞是一点也不关心的。

这当然也没什么不好,但不同的需求肯定也是存在的。总会有不能允许你随随便便就把整个程序或者甚至整个系统跑飞的场景存在。这种脏活总得有人干,要么运行时来干,要么就得操作系统来干。(其实操作系统也可以看作是运行时的一种,或者说其中一个层次。这又是另一个话题了。)

总之,在这里我想要表达的是,在某些场景下(例如安全性是一个requirement的情况下),运行时必须要承担起管理资源的责任。自动资源管理在这时是必须的——这是设计要求。在这之后才会谈论到语言抽象层次这样的问题。当然,这里的要求仅仅是“自动资源管理”,至于实现是否一定使用GC,这属于实现细节。 前一篇回文里举的例子主要是为了说明,如果设计要求运行时不允许undefined behavior,那么必然导致运行时不可能赋予用户完全的自行管理资源的能力。即使用户使用的语言原本存在对应,在这种情况下也不能使用。(关于这一点可以参考C++/CLI。虽然C++/CLI有些微软特定的东西,但事实上自己尝试一下的话也会得到类似的结果。无论如何,不可能不修改new/delete原有的语义。)

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-08-1635-%E5%B9%BB%E3%81%AE%E4%B8%8A%E5%B8%9D

2.资源管理实际上比这里之前说到的更广。
之前讨论的RAII这样的determinstic释放策略是基于用户可以并且负责确定资源管理的所有权的假设。实际上GC用到的启发式释放策略就无视这种假设。如果约定附加的接口接受运行时确定的所有权,那么实现自动管理原本不在计算机上的资源是可行的。
只不过这种indeterministic很难推广成一般情况,也只有GC这样的特例相对成熟一点,所以普遍情况下不多考虑。
在自动管理中需要finalizer/destructor很容易理解,否则回收的方式只能写死在语言内建规则里,无法扩展。
至少需要finalizer或destructor作为自动资源管理的基本抽象之一,这点我没有否定。 我要说的是,即便回收资源这个目的相同,选取哪种作为基础是方法论上的根本不同,并且我不支持以finalizer作为基础。
或者说,我认为在合理的设计中,destructor这样的deterministic机制总是应该被用户(不止是语言的设计者)考虑到,即便退化成trivial/no-op;而finalizer则可有可无,因为从destructor到finalizer,只是通过限制了资源的种类来换取允许indeterministic以减小释放开销这样的优化。
其实从finalizer加上determinstic的限制得到destructor,或者从destructor为基础加入indeterminstic性质得到finalizer,两者在逻辑上都是能够自洽的。就好像牛顿第一定律在数学形式上是或者不是作为牛顿第二定律的特例,都说得通。

那么区别在哪呢?
finailizer的设计者(以及一些搞并发模型的研究者)可能认为世界的本质就是indeterministic的,所以建立抽象时,首先尽量少预设它们的属性,包括资源什么时候应该被回收。
但我和其他大部分人不同意这种抽象的方式。物理世界是该indeterministic,但宏观上并没有一致地、普遍地体现这一点;更重要地,这里建立的抽象是人为的设计,应该为高效简洁地解决问题服务;否则,这种抽象自身就欠缺意义。这具有更明确的目的和效果。
具体一点,就是大量“比内存更宝贵的”的资源了;更确切地讲,是indeterministic释放会导致问题的资源。
因为这些问题,我认为坚持不提供deterministic作为基础机制的自动资源管理在根本意义上是错误的或者至少是次等的设计;这里的缺陷要靠手动管理其实是一种妥协。
C#这样只是口嫌体正直而已:它没法回避deterministic资源释放这种根本需求,却又不愿意放弃之前依赖的已经被硬编码到语言规则内的GC,所以就搞出了一些不伦不类的接口。这正说明了现实中使用indeterministic自动内存管理作为基本机制是错误的设计。
而Java……脸皮厚还是历史包袱重吧,直到try-with-resource之前的deterministic就只能全靠手动了。
对象死了又活的问题,我只能看出设计者自己不知道在干啥。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-08-1635-%E5%B9%BB%E3%81%AE%E4%B8%8A%E5%B8%9D-1

2.广义上的安全的话题说到底也就是可信性的问题问题。所有技术手段到后面都需要人的判断。 只是,当大部分用户自己没有判断的基准时,提供什么样的默认策略就成了之前的问题。 当用户对允许计算机信任程序的判断都值得怀疑时,提供再多保证的安全策略也是没有多少实效。
对于典型的最终用户,权限模型还不见得有提醒用户风险的清晰的UI作用更大;对于专业用户,即便非强制的规范起到的作用也可以更显著。
总的来说,考虑可用性,现实的目标是“让用户允许自己想要的行为”和“禁止不受信任的行为”。 在用户无法保证总是做出正确的精确判断时,这两点有一定冲突的地方。
设计系统时,要考虑这两者的冲突合理地小。通用目的的语言实现也不例外。
运行时隐藏检查的前提是用户明确需要这种行为;这不是反作弊程序,强制覆盖不是这种机制的本来目的。
当然,分辨可执行映像标识特定模式的二进制代码是可以做到的。这样,运行时系统可以提供附加的功能帮助系统管理员对程序运行实施一些强制策略;不过这种机制的实现以及系统管理员的判断是否能保证安全,那又另当别论。而且这个是另外的机制了。
不自觉的问题体现在——比如说——开发者可能根本就没想到使用这种机制帮助提升安全性。
中间路线也是有的,但不是作用在相同的地方——分层设计。现在CPU和操作系统使用不同特权级执行不同风险的程序就是典型例子。只不过典型实现的二进制层次和接口抽象上都只有内核态和用户态这一个边界,没法被大多数应用直接使用,还是太粗糙了。这时候运行时进一步在受控的执行环境虚拟出了这种机制,这可以说是一种变通。

纯路人@Tippisum锐评现代信安威胁模型:
https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-11-1758-tippisum

首先还是最终用户和开发者的区分问题。在我的观点里,这里不是用“是否作为开发者”(或者说,是否进行实际的程序编写)来区分。
在安全性的模型里,一个典型的应用场景会有四个主要的角色存在:
计算需求的提出者和执行者
计算数据的拥有者和提供者
计算资源的拥有者和提供者
解决方案的设计者和提供者
另外还有一个额外的角色,那就是体系外的攻击者
其中 1) 是典型的最终用户,而 4) 是典型的开发者。
当然,在实际问题中,这四个主要角色可能部分或全部存在着重叠。因此一个人当然可以既是“最终用户”(提出需求)同时也是“开发者”(解决问题)。这部分就先不展开了。
在理想的模型中,这四个角色是互相重叠的,或者至少,这四者应当有共同的利益。因此理想的安全模型里只有一个主要的目标,那就是“体系外的攻击者”。
但现实却往往不是这样。四者互相之间心怀鬼胎甚至明争暗斗的情况几乎是常态。DRM就是个最大的典型。除此之外还有Trusted Computing和SaaS之流……明眼人我想不太可能会觉得它们的目的是如同它们对外声称的那样吧。
说白了,这些问题的根源,与技术无关,纯粹是来自人类的愚蠢和贪婪。只不过有的时候,这些非技术问题却需要靠技术来埋单,或者说需要拿技术当枪使。所以才会有各种不可理喻的设计产生。归根究底,就如我们之前已经得出的共识那般,这问题实际上根本就不是技术问题,只能当作既定的现实来接受。当然,合理的设计可以在一定程度上缓和或对付这些矛盾,但绝不可能从根本上解决。

回过头再来说说技术上的问题。
native代码托管当然有,而且现在已经被广泛使用。我自己也使用VPS来托管网站。不过,native代码托管和我之前所论述的观点并没有矛盾——它依然是通过为资源增加额外的抽象层的办法来解决安全性问题。特别的,在这里,“安全性”对应的是前述角色 3) 计算资源的拥有者和提供者 的安全利益。显然,他不仅有对付“外部”的对手(网络黑客)的需求(这是个正常的需求),也有需要对付“内鬼”,也就是那些所谓的“用户”(“最终用户”也好,“开发者”也罢)滥用自己所提供的计算资源的需求。(这个需求确实很可笑,但它也确实是客观存在的)
而不同解决方案的不同点本质上只是在于具体使用什么方式来实现这个额外的抽象层罢了(平台虚拟化 v.s. 应用虚拟机),套用前面的说法,那就是这其实还是实现细节。
当然,落实到实现上,虚拟化方案现在有硬件提供支持,某些场合更加高效。但相应的,也有常常需要虚拟一个完整平台的代价。在实际的应用场景中,究竟哪个更加合适,还需要具体问题具体分析,不能一概而论。
“两者之间也并不是取代和被取代的关系,同样得看不同需求和场景而定。”,这一点我完全赞成。
“不过Python、C#、Java这类怎么说也不是当成玩具的东西,搞得这么麻烦/无能就有点不地道了。”
归根结底,其实还是人自己给自己下的套。

最后总结一下观点:
在理想的世界里,用户应当受到完全的信任。在计算机的智能没有全面达到或超越人类之前,应当赋予用户根据自己的判断来进行资源管理的权利,语言和运行时在这里应当起到的是一个合理且有效的辅助作用,而非越俎代庖,“代替”用户来下决定。
然而现实中,由于各种非技术原因,导致不能完全信任用户,甚至从设计上就将用户当作潜在的“敌人”,这种可笑的需求仍然客观存在。而为了满足这种需求,系统必然要在某个层次上面对资源进行额外的包装,从而剥夺用户对资源的完全控制能力。这个矛盾的根源是非技术的,因此不可能单纯通过技术的手段得到完全合理的解决,只能以不同的方式、在不同的层次上达成某些妥协(e.g. 硬件物理隔离 / 硬件虚拟化 / 硬件权限控制 / 系统权限控制 / 应用虚拟机 / etc.)。
回归技术层面,GC本身只是实现自动资源管理的其中一种手段。它既不能代表所有的自动资源管理,当然也不一定(甚至不太可能)是其中最优的手段。在需要提供(或强制)某种自动资源管理的时候,应当针对具体的应用场景进行合理的分析,在必要时向用户提供合理的选择空间和扩展机制。不问前提的滥用GC,甚至强制GC作为唯一的资源管理手段,是一种不理智、甚至可以说不负责任的设计思路。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-11-1943-%E5%B9%BB%E3%81%AE%E4%B8%8A%E5%B8%9D

再把话题换成一个更和原来的主题相关的例子说明我的立场和其他某些人可能有的差异。 例如,一个自用的Windows桌面应用,可能用C++或C#实现,技术难度看起来差不多,选择哪个?
对于我来说:
1.如果是作为开发者,去读C#实现的源码乃至修改以适应自己的附加需求,虽然通常也没什么大问题,但也可能容易不爽。
主要原因就是上面说过的,默认的GC语言会让资源管理方面的逻辑变模糊,规模一大就比较疼;还容易罗嗦。
虽然不得不承认现实C++不合格用户更多导致有更多看起来功能实现得好好的实际却是烂代码的风险,但还是容易找到好的、干净的、容易实现需求、节约我成本的代码。
作为有能力提供全部实现的开发者,我不喜欢这里C#表现的中规中矩——因为在我会去考虑实现的规模内,我十分不缺鉴别并扔掉烂实现然后转而寻找/自己实现替代方案的能力。
所以即使C++槽点更多,我在这上面也更不待见C#。

2.然而作为一般用户乃至最终用户,要开发Windows桌面应用,强调解决问题本身而不是尽量挖掘实现和方案的可复用的价值,我会在不十分严肃的场合优先选择C#而不是C++;也包括类似场景有人咨询如何选择方案时。
中规中矩意味着“智商兼容性”,或者一定程度上来说的低风险。尽管远离最优解,使最终用户往往付出了更大的代价,却有更大机会避免什么问题也解决不了的最差情况。虽然消极,但是有用。
当然,卡翔到某些Java程序的方案,不管作为什么层次什么角色的用户,都直接pass:不满足性能需求。
因为是对我来说,这里满足一个假设前提:知道开发人员在怎样的条件下能怎么完全发挥出本来的优势,使其中哪一个更合适。
然而因为各种因素,公认C++比C#在编码上麻烦,而且工具上有欠缺导致开发效率和质量问题,所以许多情况下还不如C#实现。
结果时间久了就变成了在没有预设前提下“C#更适合写Windows桌面应用”这种都市传说。 实际上呢?恐怕存在不会具体分析问题只会盲目重复这种不靠谱结论的这个问题本身,造成的麻烦可能才更严重。
其实专业到一定程度的开发者都应该明白哪个选择有哪些局限性,以及具有在熟悉项目背景以后做出合理决策的能力。
然而“业内人士”有太多云里雾里乱传谣的了。
我在此非常不满的,归根结底就是这一点:大量“专业”人员明明自己了解得不够明白还说大话,有意无意蒙骗外行的用户。
再追加一些的话,还有很多小白半桶水咣当响,不去自己分析问题,反而给会独立思考解决问题的人灌迷魂汤,绑架用户需求,似乎以为只要实现用户字面上提出的东西,做出妥协也是光荣了。
至于一直作为外行用户的,了解这个也不算是义务,所以我倒是没什么好说的。

对8年后electron套壳客户端libcef.dll遍地爬的惊人预言: https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-12-2026-%E5%B9%BB%E3%81%AE%E4%B8%8A%E5%B8%9D

PS1.MSBuild和*ant这类XML类似物读起来还大概明白,写起来那疼得……而且参考资料也不好找。
也许在特定平台环境(IDE)下能基本满足需要,但是拆开来单独用远不如makefile成气候。 所有这些DSL还有共同的问题:DSL到通用语言缺少过渡。
也有一些利用现有语言(语法和语义)的替代方案,减少从头设计DSL多出来的artifacts,比如用Ruby的Rake,用Haskell的Shake之类。
然而这些宿主语言在一定程度上本身就够麻烦了,所以整体上也不见得更简单……
考虑扩展也许这里用经过严格限制的Lisp方言的子集类似物作为基础最容易。
……以后再说吧。
PS2.图形库的轮子我基本没造(只是撸了几个光栅化以及替换了EGE的底层实现)。
我造的主要轮子之一正是native GUI的一般解决方案中强调需要去除体系结构和操作系统依赖的部分,而不是GDI+/D2D之类的图形库——为了简便起见还刻意弱化图形功能了(因为这是另一大坨适合单独实现的东西),硬件加速什么的也完全没实现。
声明式(布局)语言只是GUI的一部分,而剩下的部分即便不算图形也很麻烦。例如,实现一个浏览器的排版引擎,几乎就是实现大半个GUI框架(当然还有其它一些东西)。(顺便,我发现“尽可能分离UI和非UI”不总是必要的需求,在很多场景下导致不可能实现优化的效果,浏览器却在这里尤其受到影响而需要妥协——不过这些是另外的问题了。)

HTML设计一开始就没有考虑这类相对通用的目的,导致很多坑,即便没有W3C也会自己残——现在没js也就是残的(听说HTML6要去js化?)。
相对来说,XML以及XAML和QML之类的派生物就靠谱得多,可就是没有HTML+js这样有流行的多种实现。
ECMAScript现在大概也是朝通用目的而不是DSL设计了,这样倒正好腾出些位置。不过还有很多要取代js什么的跳出来……
可以预见,运行环境在很长一段时间内还是会相当混乱。
技术上来说,我不喜欢SGML派生物——要说通用文本序列化格式,不管是对机器还是对人,不管是时间空间效率还是实现复杂性/可靠性来说几乎都被S-expr等等完爆。即便只限存储、传输和编辑,也经常不如JSON之类。
只是因为历史包袱多,可用资源也多,所以才妥协罢了。所以不需要兼容,可以扔掉浏览器的场合,我绝逼不用。
退一步讲,浏览器的内部实现(不管是不是排版引擎)很多地方也满是洞,要接受这坨东西建立应用环境就得做好随时丢失可用性的准备,比如习惯吃内存太多就干掉浏览器进程。(V8分配不到内存就boom什么的……)
于是Web应用在本机环境上怎么说都是备胎(即便算上桌面应用嵌入Web页面的之类的情况),很长时间都不能指望太多了。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-14-2349-tippisum
2015-04-14 23:49 Tippisum

我觉得Flash在可预见的将来是不会死的,而且Adobe就算嘴上说放弃Flash,实际上真的放弃掉的可能性也不大。这玩意儿说白了也已经是尾大不掉了。能真正意义上干死Flash的只有HTML标准。但从HTML5的情况来看,我对W3C的工作效率极其没有期待。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-15-1609-%E5%B9%BB%E3%81%AE%E4%B8%8A%E5%B8%9D

Flash……大势所趋没办法,毕竟主要厂商姿态都定型了。而且毕竟没C这种关键领域的应用(如操作系统内核、硬件驱动程序、版本控制系统)的不可替代的依赖性。
当然死成什么样子不好说。Turbo C某种意义上也不是没死么(呵呵)。
所以我觉得除非是DSL,以后会是能随意按需静态化的动态语言才是王道。当这里的问题搞清楚以后,“极端的以编译性能换运行性能的语言”也不会成问题,而是可以根据用户需要调整侧重点。 关于通过元数据反射,C++也能做到,就是不自带、没标准化、需要人肉写很多冤枉代码,所以基本不能实用罢了。说C#严格弱于C++也是不对的,一个比较明显的硬伤是variadic,而C#的lambda缺乏类型导致一些地方写起来远远比C艹麻烦,这些应该算是一厢情愿的设计失误。
而真正意义上体现“动态”特性的东西如evaluation tower,几乎没什么语言实现能做得好,C-like的静态语言就别说了,C#加上dynamic也得靠另外糊DLR,原生机制根本不够用。
C确实不被指望能表达高级抽象,但是C的发展和规范本身体现出来的也未必就对“底层”友好——C++对实际上的底层的限制还更严格清楚一点,比如指针关系操作的全序,C就没有。当然C这里未必就设计得更烂,只是要说在这个意义上说C“接近底层”,实在有些那啥。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-15-1836-tippisum

Flash很难再有什么大的发展,但死肯定是不会很快死掉的。而且其实Flash在很多场合还是很有用的(我经常在各种场合看到用Flash来做UI,甚至有比如说像the Elder Scroll这种大型3D游戏也在用)
C++不得不吃的翔基本上就是编译器和操作系统了。其他的反正理论上讲怒造轮子都是可以的。 我同意编译时和运行时的区别不是绝对的,而且能够直接对语言本身静态化而不是刻意区分编译期和运行期也确实有潜力(C++的模板元编程还是略疼,constexpr也不总是顶用)。
不考虑翔一样的互操作性的话,运行时对象创建和接口查询之类的基本反射C++肯定是能做的,甚至现成的库也有。但模板运行时实例化基本上是不现实的,就算能做出来,考虑到效率和蛋疼程度估计也不会有太大的吸引力……(C#泛型对值类型的特化基本上还是要靠运行时JIT支持才有的玩,在iOS之类的设备上实现的时候就疼的要命)
C#也是一开始没有考虑到模板,后来才有了泛型。不过好歹微软还是没判断错局势,把泛型支持搞到CLR里面,从实用角度来说是个质的提升。Java这方面真是反面典型。为了所谓的兼容性,把自己最大的优势——互操作性给丢了。Java里涉及到泛型的互操作真是一个不小心就掉坑,比之C++有过之而无不及。
至于你说的情况,我只能说静态链接libcmt简直是会玩的……
shared_ptr,反正就连我这个不喜欢造轮子的人,造轮子列表里还是常年有这东西,真的是槽点太多了,令人印象深刻。有了unique_ptr之后已经好多了,不过unique_ptr的轮子有时也还是要忍不住自己弄一个,毕竟这东西几乎到处都会用到,能把性能稍微提升一点的话还是值得的……

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-16-1310-%E5%B9%BB%E3%81%AE%E4%B8%8A%E5%B8%9D

关于这个问题,我觉得C++和C#乃至现在几乎所有语言都是闭门造车。正确的姿势应该是一开始就向用户(库的作者)提供扩展重写系统的机制,而类型系统建立在重写系统的规则之上。模板这种东西的核心规则不应该那么复杂,包括参数类型、依赖类型这些常规语言的特性或者和语言实现以外的互操作的保证等,都应该让(系统)库的作者实现,一般用户使用这些库完成任务。 当然,语言实现内部的互操作性方面对语言的设计者要求会很高——要提前预知清楚哪些东西是可扩展的,在编译器和运行时给库的作者留下接口。但无论怎么说,在同时保留现在C++和C#类似特性的前提下,扩展和兼容问题上不会像现在那么被动。
这种的设计实际上有更广泛的意义——让一种通用的语言通过加上特定的库,自然“退化”到另一种更具体的语言:加上某个库就是C++的实现,另一个库就是C#的实现——因为肯定会公用(比CLR更一般的)IR,二进制以上的互操作性根本就不会是问题。现实最接近这种角色的是抽象的LISP,然而Lisp的各种现有方言仍然主要和其它语言竞争来解决具体问题,并没有认清这里的定位,这里的玩意儿长期没有进展,更没有标准化中间层技术到现实能用。如果我要造语言,迟早要把这块补上(向用户证明可能性,是不是真的值得要我亲自实现另当别论)。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-04-21-1116-cqwrteur
2015-04-21 11:16 cqwrteur

@幻の上帝 经常听到有人鼓吹说"大多数人自己无法管理好内存,GC做的比他们都好。至于网络文件这类的资源,都不会让这类程序员去写。"
不知道你怎么看。

2015-4-21 14:09 幻の上帝

也许还真是大多数。这类小白用户不配进入专业撸码领域,速速滚粗。

2015-4-21 22:07 Tippisum

回复 幻の上帝 :会有些非正常需求,比如说折腾一些Reverse Engineering之类的东西。有源代码的情况下本来也就犯不着捣鼓二进制。

2015-4-22 09:00 Tippisum

回复 幻の上帝 :底线是,指针算术可以自己做,但调用约定在C/C++语言的层次上是没有办法的。如果编译器不肯开洞,那只能要么上汇编,要么发明一些奇怪的宏……所以说C++的二进制说多了都是泪……(当然,仅限公开或者半公开的接口。纯私有函数编译器没有义务使用标准的约定,该上汇编的时候还是得上)

2015-4-22 12:07 幻の上帝

回复 Tippisum :那就没办法了……老实折腾吧。反过来说,有些人还有“难破解”的需求呢,这方面就正好是C艹>C>>Java啥的了。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#%E8%A1%A5%E5%85%85%E6%9D%90%E6%96%99%E8%AF%84%E8%AE%BA-2015-05-07-1727

One 语言具有 first-class function。函数可以作为值任意传递。跟 Scheme 一样,这是一种真正的 lambda。很多语言虽然有叫 lambda 的东西,但却不能正确的实现,比如 Python 和 C++11。
具有真正的 lambda 的语言都需要 closure optimization。这个优化如果不做,就会产生大量的内存访问。C++11 要求程序员自己写出 free variable 在 [...] 里,就是因为它的编译器不能做好这个优化。在 Chez Scheme 和 Kent 的课程里,这种优化都是做得很好的,做到了极点。
...
不区分stack和heap变量
跟 C++ 和 C# 不同,但是跟 Scheme 和 Java 类似, 所有的变量都只是一个名字,程序员没必要知道它被放在堆栈(stack)上还是堆(heap)上。做这样的选择是为了让“名字”这个概念更加有一致性。为什么程序员需要知道它在哪里呢?这种事情本来就应该是编译器来做的。程序员所要知道的只是“一个名字对应一个对象”。
编译器尽量把对象放在堆栈上,这样可以减少 heap 的碎片现象。
——王垠《one语言的设计理念》

专门挂婊。
其实这篇文章本身倒和GC没多少关系(作者在后文中倒是表示考虑实时性现时不考虑使用GC,虽然我记得实时性问题在GC会议上多少也有被研究过,一个结论是并没有根本矛盾),挂的是一种具有普遍性和代表性的、显然错误大多数当事人却不自知的观点。
总结起来还是老调重弹——无知+瞎YY用户需求,嘛……在分析这点先说引文本身——为了表现错误的简便,先说第二段。
1.所谓stack和heap变量的区别,在具体语言中是否有所区分的情况不全相同,这点没错。 但是,名义上的区别和实质上的区别也不是一回事。例如,C++本来就没所谓stack和heap变量的说法。
具体到C和C++(搬出C是因为C的设计在这里显著影响了C++,而且C同样被普遍地、类似地误解,尽管ISO C甚至就没什么“变量”的概念),猜一下比较接近的意思,需要考虑的至少有两点:C对象、C++对象和引用的生存期;C和C++对象的存储期。
两者完全是相关但不同的抽象。而所谓stack或者heap,根本就是实现细节。稍微沾边的正式说法是,C里面叫自动/分配生存期/存储类,C++该叫自动/动态(剩余操作接口细节不一样的地方略)。注意,这是对象(以及C++引用)自身的属性。而所谓stack和heap根本就是不同的东西——在C里就没有,在C++里也就是后者用来实现free storage。
所以说所谓stack和heap变量的说法至少在一些语言里本来就是荒谬的:既混淆了接口和实现;又混淆了不同层次的抽象。而其它严肃的语言也并不敢直接在规范里这样扯蛋,非要说也是捆上运行时(而不只是语言本身)的规范才敢,否则就说不清楚。
另一个槽点是,ISO C和ISO C++是无所谓什么stack或者heap,JLS反倒分清楚了。(有JVMS唯恐天下不乱?Dalvik喝西北风是吧。)考虑到StackOverflowError,倒还真不敢故意跳过不碰stack(虽然敢语焉不详)。相对地,因为带有明确的用户可控的副作用(导致显式的程序行为),C++即便要说清楚stack unwinding是啥不碰stack都绰绰有余。哪种设计加重用户的负担呢。
对于这样一个错漏的概念照用不误然后一本正经批判,吃饱了没事撑着的。

2.“所有的变量都只是一个名字……程序员所要知道的只是‘一个名字对应一个对象’”——至少在通用语言中这实际上除了能说明了“变量”这个概念的废柴,并没有什么卵用。
这里实质蕴含一个陈述:用户只需要知道名字就能让“变量”顶用,即便不知道所谓的“变量”是什么、从哪里来,该到哪去,乃至是不是必要。
作者在这里暗示,编译器(语言实现)应当能够完全代替用户,了解变量放在哪。了解变量放在哪的理由就是“减少 heap 的碎片现象”。
有点常识就知道这是笑话。(不少Java用户和GC厨在这里没常识倒是真的。)
了解变量放在哪本身并不是什么目的。知道变量“放在哪”实质上是“怎么放”的一个副产品(上面也说了“放在哪”是另一层次的抽象,对用户而不是实现者来说许多时候根本无所谓)。
至于知道“怎么放”,为的是使用户能够按需控制程序符合预期的行为,这包含以下几个方面:
(1)清晰的边界和逻辑:在程序的哪些片段中变量是可用的,而另外一些时候这些变量是不需要考虑的,以使程序的逻辑更清晰;
(2)更明确地指示语言实现允许优化的外延;
(3)简化语言实现需要实现的优化逻辑,同时获得较好的翻译时性能和运行时性能。
光考虑下面两点,这些需求现实能被编译器很好地实现了么?显然没有。否则,C#抄了那么多Java特性却为什么在这里重新捡起来Java扔掉的区别呢?
只是因为C#的编译器写得烂?即使没常识也应该知道事情不是那么简单。
要编译器代替用户了解放在哪显然是不够的,也没多少直接的用途。对于语言实现来讲,更重要的问题是时机:什么时候引入一个变量(的存储),什么时候释放。
如果用户在抽象上对存储策略进行分类,那么他们就可以相对精确地控制(至少能做到保证相对顺序)。编译器在这里能做的都是硬编码的逻辑,也就是所谓的启发式策略,例如分代GC依赖的假设。
在什么时候最合适这个问题上,一个不明白精确规格的非强AI程序永远打不过一个了解精确需求和正确实现姿势的人类,更不可能方便地调校策略。这就是GC之类偷懒方法的一个根本缺陷。
若用户没有能力或者不被语言设计者允许表达不同的资源管理策略,任何形式的自动优化同样叒逼。
在可预见的未来内,强AI或完全形式化设计和验证的现实不可操作性注定了通用目的语言上的设计在方向总是错的。还想再来一次AI Winter?2young2simple。
更别说不能很好地满足(1)给真正想理解清楚需求的用户造成了更大的麻烦。
这怎么看都是鼓励无知的反智主义,最可能的起源正是作者对现实需求的无知。

3.“很多语言虽然有叫 lambda 的东西,但却不能正确的实现,比如 Python 和 C++11。” 撇开GvR的强迫症不讲(黑得好),这里所谓的不“正确”并不是什么实现,而是设计本身。
C++的问题非常明显,总结起来一句话:closure type非一等类型。(当然,光说C++11还有一个就是残废——比如参数多态的“泛型”lambda和capture list里的initializer因为进度问题,到C++14才填坑。)
反过来说,真正的“实现”(比如,closure type使用其它已有语言中的概念的定义),C++在这里简直做得没法更伟光正:不让用户做多余的假设、照顾了实现可行性、给实现足够的余地进行优化。
(反倒是C++11以后CWG的修正不够意思:要求显式C++ linkage导致没法做signal handler,以及禁止作为literal type简化实现和减少用户的潜在错误假设却妨碍优化。)
这段别的地方看来也没说语言的实现(只说lambda的“实现”),说来说去都是语言必须要求的“设计”,否则一坨说不通(或者只能体现对语言实现的外延缺乏概念)。

4.“具有真正的 lambda 的语言都需要 closure optimization。这个优化如果不做,就会产生大量的内存访问。C++11 要求程序员自己写出 free variable 在 [...] 里,就是因为它的编译器不能做好这个优化。在 Chez Scheme 和 Kent 的课程里,这种优化都是做得很好的,做到了极点。”
closure optimization没说清楚是啥,大概是指以存在closure为前提的一类程序变换效果的统称。
这里就有几个先入为主的低级槽点:
(1)谁要求lambda必须就实现成closure的?只要不是ABI有要求(明显不是这里C++的情况),没有capture的free variable的lambda很容易优化成一个function pointer,根本就没这种“不优化就会产生大量内存访问”里要“访问”的东西。
(2)谁保证不做程序变换之类的optimization就必须蠢到有“大量”内存访问?话说回来,非要说受到这类optimization可能影响的“大量”,只可能是不限制捕获范围的情况。像C++的explicit capture list反而能最大情况下减少“大量”的可能性。
退一步讲,真要不限制具体捕获什么变量,还有[=]和[&],反而更加精确自由。不过靠这种程度的扯蛋智商可能也没法理解这里的区别了,我也懒得展开了。
然后是一个关键问题——显然语言规范不需要也不应该指明这类optimization的外延。至于不做是实现烂,纵容不做是用户智商低,能关语言设计毛线?
退一步讲,真说实现——没做?还是说,为了让优化更能发挥作用,就应该纵容用户写稀里糊涂的代码?
“就是因为它的编译器不能做好这个优化”这种大言不惭的笑话也看不清是怎么脑补出来的。 话说,发挥成这样,这里没顺带噗let expression也太可惜了吧。
## 2015-05-07 17:28 幻の上帝 ref:http://tieba.baidu.com/p/2411685175

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-08-02-0012-tippisum

自动资源管理 vs 手动资源管理 / deterministic vs indeterministic.
虽然都跟 GC 有关但实际上还是有很大不同的。
自动管理跟手动管理这个问题,以一个比较极端的方式来考虑的话——其实自动管理在很多时候都是强制的,你没法摆脱它。
这最终还是要取决于如何看待“自动管理”和“手动管理”的差别和界限。但毫无疑问,任何现代计算机系统上的资源——没错,任何资源,它们一定在(比我们讨论的“程序”要低的)某个层次上是受到自动管理的。
你当然可以写一个 C 程序,申请内存从不释放,打开文件从不关闭。但只要程序一结束,世界依然清净。你真的“手动管理”了这些资源么?从某种意义上,是的。但资源的所有权实际上仍然被操作系统掌控。
运行在现代计算机系统上的程序,无论用何种语言写成,它们一定是在操作系统和运行时所严格划定的圈子里面执行,绝不能越雷池半步。它们所使用的一切“资源”,也都是由操作系统和运行时控制的。无论有没有本文语境里的那个“GC”(或者范围放大点说“强制的自动内存管理”),这个事实都不会改变。有差别的地方无外也就是圈子大一点和小一点的差别罢了。(有例外,但不在这里讨论的范围内)
话说既然我们讨论“资源”,倒是有一种特殊的“资源”,对程序的执行至关重要,但在(程序语言设计这个框架内)却基本上没有人讨论。甚至很少在此类语境下被当成“资源”来讨论。 那就是

「CPU 时间」。 “在现代计算机系统上,几乎永远受到强制的自动管理的几种资源之一”
这个东西实在是太理所当然了所以很多时候是根本意识不到的。但 CPU 调度确实是个很大的课题。而且抢占式线程模型当然也存在各种缺点和不便之处。直接或间接与之相关的各种改进,如协程 / 异步等等也都是现在比较受关注的问题。最近的操作系统,为了更好的提高某些服务程序的吞吐量,也允许进程本身有限度的参与调度过程。
然而根本的图像上并没有改变。作为每个程序执行所最根本的“CPU 时间”这个东西,程序本身是没有能力去进行管理的,当然也没有语言提供这样的机制。协程的存在使得「有限度」的管理成为可能——说“有限度”是因为它只能提供转移分配的能力,申请和释放这样的正规资源语义当然是out of the question。
之所以会变成这样,一个很简单的理由还是,这个资源实在是太重要,也太敏感了。一个程序拒绝交出 CPU 时间,就可以直接挂起整个系统。这可是比泄露个把内存或者文件描述符什么的要不知道严重多少倍的事情。
The Good old days,Windows 3.x 就是一个协同式多任务的操作系统。尽管基于窗口的 CPU 协同调度设计精巧,而且大部分时候(令人惊讶的)工作的不错(以当时的眼光看来)。但还是没有办法改变只要一个程序出了点差错整个系统都会跟着完蛋的事实。所以当支持基于硬件的权限控制的处理器推出的时候,操作系统(毫不令人惊讶的)立即实现了抢占式调度,彻底将这个资源完全掌控在手中。
相比之下,内存调度这个东西之所以这么容易被玩坏,归根结底还是内存这东西确实没有 CPU 重要。应用程序到底是手工管理内存,还是自动管理内存,抑或是干脆不管理内存,在如今大多数的场景下确实没有什么大不了的。只不过这样的现实毫无疑问助长了不合格以及不负责任的程序员写出垃圾程序。
从这个角度来说,GC 并没有什么错,它的存在是为了解决问题。而且它关于“内存相比于 CPU 而言是更贱的资源”这样的假设,虽然不爽,但无疑也是事实——至少一定程度上是,否则的话打从一开始就不会有 GC 出现的必要性了。
但在某种意义上,也许这个问题不解决也好。

由此引申出的另一个点是资源管理有两个问题,效率问题和安全问题。这个之前也讨论过。只和效率问题有关的语境下,允许手动管理(或至少是有限的手动管理)当然是更加合理的选择。不过一旦牵扯到安全语境,强制策略几乎总是必须的。操作系统必须强制的自动管理 CPU 资源,并不是因为抢占式调度器总是效率更高,而是因为如果它不这么做,任何一个程序的 BUG 都会严重影响到整个系统。显然,总会有程序以为自己能料理好自己,但实际上却并非如此。同样的道理,CLR / JVM 这样的平台一定需要一个“强制的自动内存管理”(不一定是“非确定的内存管理”),因为它们的设计目标里面包含了对运行时 type safety 的保证——而一旦允许程序自己动手艹内存,这种保证就是在扯淡。显然,总会有程序以为自己可以正确的艹内存,但实际上却并非如此。严格来说,只有在操作系统内部,才是真正意义上“手动管理资源”的……吗?其实也不尽然。总会有操作系统觉得自己能管好一切资源,但实际上却并非如此。这也是各类“管理操作系统”的虚拟化技术出现的原因,或者至少是原因之一。
至于 deterministic vs indeterministic 这个问题,这方面的共识倒是比较多。总体上来说 indeterministic 会有更多的坑,强制应用 indeterministic 策略的话就会有更多。 finalizer 基本上是 GC 的阴暗面,里面的坑说实话不见得比 C++ 的析构函数少。finalizer 是不提供任何保证的,包括不保证它最终真的会被运行……如果只涉及本地资源的话还可以指望操作系统,但如果里面封装了非本地的资源句柄,good luck……
C# 好歹还是意识到了这个问题,所以有了 IDisposable。不过有了手动管理之后随之而来的就是资源所有权的问题……有些比较神奇的坑,比如说用一个 Stream 构造一个 BinaryReader 的话这个 BinaryReader 会默认自己取得了这个 Stream 的所有权,于是如果你 Dispose 了这个 BinaryReader,Stream 也跟着 BOOM 了。(新版的运行时给 BinaryReader 增加了一个构造函数重载来解决这个问题。)
为什么一个对象可以死了又活……
这个问题完全莫名其妙嘛,直到现在也还是不能理解这背后的设计逻辑。之前提到过一个,就是可能在执行异步的析构操作时需要保留状态。但从设计上讲这个问题不是不可避免的。还是觉得这样的设计很可能就是基于“万一有人在 finalizer 里面做奇怪的事情,比如说把这个对象注册到一个全局对象上去怎么办?”——“那就干脆让这个对象复活吧”
明明应该禁止这样的事情发生才对吧……(掀桌)

@tippisum 自动和手动的界限是相对的,不会是铁板一块。
扔给操作系统算是一种策略,代价就是进程生存期内的确定性泄漏。手动的灵活性体现在用户有权决定是否采用这种策略。(当然这种策略一般很糟糕,这个另当别论。)
更自动的东西,就不方便有那么多可能性了。不过,损失可能性是不是值,还得看是否覆盖足够多的情况,或者说通用性。
愿意提供给下游用户多少控制,除了看设计者的心情,也得看实现是否距离边界足够远。
如果太高级又不够普遍,中间的资源管理层次多了效果也不好,自然会倾向于不提供底层资源管理接口的设计。
注意到不管是RAII还是典型地GC都也可以和new/delete共存,但不管是实现者还是用户付出的代价显然不一样。
因此GC这种“自动”注定更不靠谱——不止是习惯,而且不得不隐藏太多东西。
关于这里的一些资源的假设是建立在经典的hosted environment也就是一般意义上的操作系统的背景下的。
如果无视这些惯例,要刻意分配这些资源,也不是做不到。例如分配CPU时间,主要就是尽量确保实时性(不适合大部分系统的程度)以保证资源分配可预测的这个前提。
要做到底确实不得不深入指令集架构以下——例如任务切换可能是硬件提供的接口。然而,其中的大多数形式仍然来自于习惯。只是打破这种习惯的代价更大(需要重新制造硬件)。
涉及到时钟信号的资源的确是特殊的,它具有过期作废的物理属性,所以直接保存状态没有意义,没法抽象出保留给以后使用的资源。(我前几天刚好考虑过这个。)
形而上地要解决这里的特殊性也并不十分困难——把产生时钟信号的硬件作为资源,随用随取。说穿了关键就是虚拟化。
当然,一般不需要做到理想中的程度,非得放着硬件不用太傻了,除非原始需求指定了自己制造体系结构(自己造硬件或者模拟器/虚拟机之类的东西)。即便如此尽量抽象在实现上是不必要的,开销太大。
话说回来,最特殊的不过如此,其它资源的抽象就容易多了。

所以结论是“强制的自动管理”基本上还是人为约定,更多地仍然是习惯和现实需求的因素。考虑重要性,是这些因素的结果。
关于强制策略还是得提一下,根本问题还是在于需求。
不对面向的最终用户做假设的情况下,如果需求包含多任务/多用户,又不能预测执行任务的程序的可靠性,那么自然只能采取保守策略。
拿CPU时间分配来说,公平调度永远都不会是最终目的(更实际的一个目的是让用户程序合理地分配到CPU时间),而只是在上述环境下的实现策略。
一旦需求变化允许上面的假设不成立,这类强制策略也就不总是必要了。只是通用的系统不太容易有这些情况,所以一般就偷懒不考虑“优化”了。
退一步讲,在这类环境下,直接写死强制规则也不算是理想的解决方案。
这样就出现了权限的需要。其实更彻底的设计是权能(capabilities),而且不应该限制到只能由操作系统来保证强制策略的实施。不过这是另外的问题了——本质上过于依赖特定底层机制就是上层安全体系的缺陷,虚拟化在此无能为力,即便实现了变通也经常不划算(这也是许多任务中为什么轻量级容器更受欢迎的主要理由)。
关于语言提供什么抽象的问题,我认为主要的坑都踩过一遍了,不应该继续拘泥在会普遍造成失败的套路上。所以我对一些不开窍的“新”设计相当不满。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2015-08-02-1121-tippisum

说穿了还是设计思路的问题。
C/C++运行时不介意你程序把它给艹了,反正UB糊脸你看着办,剩下的事情一概扔给OS。(其实在大部分时候CRT基本也可以算OS半个组成部分了)
相比之下CLR/JVM显然有着不能允许用户程序把运行时给艹了这样的需求。这就是问题所在。
OS有硬件撑腰,给进程提供的一切资源本质上都是虚拟化的,就算进程乱搞,在OS看来也是不疼不痒。但运行时没有足够的能力来这么弄,就只能艹资源抽象了。
悲剧的根源是运行时也把自己当操作系统。所以程序其实运行在多重的资源抽象上。何况OS可以假装让用户进程“手动管理”资源而不至于出什么大乱子,运行时往往做不到,所以自然就更疼了。
归根结底,虽然强制策略有时候不可避免,但能集中到一个层次上(比如OS)总比叠着好几层要好。能有硬件虚拟化支持的,总比纯粹靠艹抽象语义要好,这应该是没有疑问的。所以说OS层次的内存管理(包括GC)我也认为是一个正确的方向。
不过从实现的现实性来考虑,增加抽象层次总是比把现有抽象推倒重来要更容易。尽管现有的OS设计上很多时候不尽如人意,但相比于直接改变OS内核,显然目前更加主流的想法仍然是继续增加层次,往上走是各种应用虚拟机,往下走是各种硬件虚拟机……
而且现在计算机的计算能力确实过于强大以至于很多人已经习惯于在资源管理的问题上偷懒了。计算能力大部分时候过剩的PC平台姑且不论,实际上资源并没有那么富余的移动平台上,偷懒和乱搞的风气也很明显。

还有一点,“内存”这个概念在不同的层次上指的其实是不同的东西。而真正珍贵的那种,也就是我们通常说的“物理内存”,* 实际上也是由操作系统强制的实施自动管理的。 *
在现代 OS 的视角看来,应用程序可以“手动管理”的那种“内存”实际上真的是一种相当贱的资源。因为那东西 * 只不过是由操作系统控制的虚拟内存罢了 *。就算程序真的申请内存从不释放,它实际能泄露的东西也不是内存,只不过是硬盘上的分页文件罢了。移动设备姑且不论,对于大多数桌面 PC 而言,分页文件这种资源的价值大约不会是一个很大的数字。
所以归根结底,还是 CPU 资源更敏感,而且更不好虚拟化——除非发生的非常频繁,否则进程一般很难察觉到自己的页面被操作系统强制的换出。但正在执行中的线程被抢占这个事情却有着实实在在的可见效应——同步、竞争、死锁……无数的麻烦事都和这个有关。
在一个通用的现代多用户、多任务系统(也就是在绝大多数时候我们所讨论的环境)下,如果一个资源,操作系统觉得它可以交给用户进程自己手动管理,那么有一个潜台词大概就是,用户进程其实也可以不管理,最多也就是把自己搞崩溃,或者把它权限所及的范围内折腾的一团糟。底线是这东西肯定不会也不能影响到整个系统的稳定运行。如果放任一个资源由用户进程乱搞有可能会把系统跑飞,那么操作系统一定会强制的接管它,最终暴露给用户进程的是一个被虚拟化的资源,抑或是某种抽象的接口(句柄 / 描述符 / etc.)
所以这也就是我之前说的,“如果安全性是一个 Requirement,那么自动资源管理一定会被强制的应用,无论这是以何种形式或者在何种层次上”。
当然,操作系统的资源管理和资源抽象既不一定合理,也不见得完善。所以才会有各种硬件虚拟机的出现——在操作系统的下面再加一个层次,替可能泄露的抽象兜底。另一方面,大多数时候 OS 它只关心用户进程不会把整个系统跑飞,至于程序会不会把自己跑飞这个事情才懒得操心。那些基于 GC 的“保姆型”运行时大约也就是基于这样的原因而开发出来,当然最终这些运行时反过来变成大爷把用户程序都按死在自己的条条框框里或者干脆假装自己也是个操作系统,这就是另一说了。

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#2016-10

Created @ 2016-10-03, titiled "默认 GC 的危害".
GC = garbage collection/garbage collector ,垃圾回收/垃圾回收器。
一些技术性观点: http://tieba.baidu.com/p/3171730339
GC 特指非决定性(nondeterminstic) 的自动资源回收机制,主要管理在线存储资源(内存)。静态 GC 等本质上属于决定性资源回收的机制不属于此讨论范围。
GC 本质上是一种以闲置存储换取吞吐量或变通其它问题(如 ABA problem)的优化实现,不适合解决一般问题的默认手段,特别地,不适合被通用的语言作为公开接口被依赖。
GC 可省略显式释放,给一些人造成了“通用”的错觉。当这些人设计语言时,会诱导语言的用户(通常是比他们更不了解语言应该如何设计的开发者)使用“简单”的机制,反而把问题复杂化,同时让最终用户体验受损。
不够通用:因为延迟释放的特性, GC 也只能用于和内存足够近似的特性,不能像其它决定性自动资源管理机制(如 RAII )推广到其它资源(锁、数据库连接等),造成无谓的实现冗余,增加维护代码的工作量。

GC 造成的问题很多(也正因为如此在一般意义上只能作为谨慎的优化),典型情况下最突出的包括以下几类:

  • STW(stop-the-world) :
    • 回收基本上都会对响应(latency) 有负面作用,不适合实时任务。
    • 增加的时延不容易平摊(amortized) 且无法可靠地预测发生的时机,引起明显的不确定场景发生的卡顿,损害可用性和用户体验。因此 GC 也不适合对绝大多数和最终用户交互的程序,或至少容易增大实现和质量保证的难度。
    • 存在不受到 STW 的“无暂停”甚至可以适应实时任务的 GC 实现,但其它问题(实现难度、并发调度、内存占用)更严重。如 Azul Zing 使用 C4 GC ,设计为一个实例占用 “a few Gigabytes to 2 TB of memory”
  • 显著增加内存占用:
    • 在内存紧缺的环境下难以使用。
    • 可能导致更高的能耗。
    • 资源利用率低下:典型地,要实现相同的流畅响应,使用 GC 可能相对不使用 GC 需要占用 5 倍的内存,考虑能耗问题,使硬件的选择更加困难。在移动 Web 应用上这些问题尤其突出。
    • 在允许分页的系统配置中引起换页:可能增加 I/O 负担,进一步损害响应,极大降低运行时效率。
    • 更容易引起 OOM(out-of-memory) 问题,降低应用乃至整个多任务系统的可用性。
    • 某些系统如 Linux 默认使用 OOM killer 允许 over commitment 导致内存最终耗尽时行为难以控制引起比内存短缺时直接禁止分配更严重的系统可用性问题。这本质上是半吊子的系统内核级 GC 。
  • 释放资源的时机不确定导致程序行为难以控制,造成额外的风险和成本:
    • 释放内存的时机不可控,这点在 STW 体现得很明显。同时,考虑对宿主系统中其它不共享 GC 任务,也可能存在一些效果(对内存分配调度等的)不可预期的负面作用。
    • 增加回收频率会使 STW 更严重;反之,减少回收频率可能使资源非预期地占用,实际上也就是泄漏了。而启发式的回收策略本质上不靠谱,只能瞎蒙,所以生产环境经常需要人工“调优”试错。
    • 终结器(finalizer) 的副作用。终结器可能包括比内存资源回收更敏感的资源回收,实际上多数情况本应使用决定性回收策略。交给 GC 之后程序行为更不可控。
    • 附带的 object resurrection 问题引起潜在的 bug 、混乱的程序流程和逻辑并降低程序的可维护性。(另外一个类似但不同的失败例是传统 UNIX 允许僵尸进程 的设计。)
  • 使程序逻辑复杂化:
    • 为解决以上缺陷在程序中插入难以复用的冗余代码。
    • 非常重要但容易被不少开发者忽视的一点:名义上的省略释放使代码更“简单”,但实际上使代码自身的逻辑混乱,更难自注释(self-document) 。
      • 这集中体现在所有权(ownership) 边界的不明确:非原作者光读代码,很难看出哪里对象确实应该被释放了,而哪里是原实现者预期资源确实可用。
      • 要在这个方面读清楚这样的代码,读者需要自行脑补,实际上就是把自己变成人肉 GC(这个问题实际上不限 GC ,在不严格约定策略的手动释放的程序中也一样)。而在没有形式化工具的支持下,模拟非决定性的策略下的副作用执行本来就有困难,更别提人脑了,基本没法指望精确证明可靠。
      • 所以对代码以外的文档依赖更加严重——然而这类情况下,设计者未必就有意识会提供讲清楚问题的文档(运行时有点问题 GC 兜着,大不了“调优”)……也就是说,使用 GC 这种实现诱使程序从设计到实现整体上更差了。

可以看出 GC 不适合多数消费和桌面级产品;在某些特定类型(特别是怕内存多得没地方用)的服务器上, GC 才可能发挥积极作用。然而即便如此,这也只是通用资源管理中的区区一种实现而已——一个 GC 实例说到底就是一个把所有权松散提升到程序实例生存期允许延迟释放的资源池;不带默认 GC 的场合,用户仍然可以自行实现 GC ——并且因为可定制性充裕,容易实现得性能更高,更适合具体应用场景。
造成这种选型错误的主要原因:对资源管理需求的错误理解且缺乏足够系统学习相关设计的经验,他们通常不明白:

  • 特定场景下最终用户需要的是什么。
  • 什么是成本和效率之间的妥协,什么是一开始就应该考虑满足的需求。
  • 什么是开发者本应做到的,什么是适合让机器自动完成的。
    反面教材例: http://yinwang.org/blog-cn/2016/09/18/rust
    这个反面教材还体现了对资源管理问题的外行。根据教育背景和历史,按理说,该作者对这篇论文应该不至于没有听说过才对,如此还理解不了表达式中的值的转移相对复制(或者更一般地,确保资源的单一实例相对资源的复制)是更基础普遍的操作(而不是反之),就很费解了。可能这和类似所谓的 有类型 λ 演算被作为无类型 λ 演算更基础的理论(en-US) 这样的反工程常识(因为 multirooted ,没有单一理论选取的基准,无法管理依赖)的主观认识相关?

https://github.com/FrankHB/pl-docs/blob/master/zh-CN/about-garbage-collection.md#%E8%A7%82%E7%82%B9%E5%B0%8F%E7%BB%93

这里旗帜鲜明地反对的是语言设计意义上的 GC 的滥用,其症状和分析包括:

  • 默认依赖 GC ,无法可移植地移除(opt-out) 。
    • 不兼容最小依赖原则。
      • 因为默认依赖,而总是引入 GC ,不论是否需要。
    • 一些场合无法忽略开销,限制了可用性,进一步限制了语言的通用性。
      • 程序的遵循语义规则不足以决定对程序的含义(meaning) 以及行为的期望(如绝大部分性能需求),因此有必要提供便利的方式避免损害可用性。
    • 混淆 GC 适用场景的通用性和通用目的语言的通用性。
      • 典型地,使用不依赖对象语言特性的单独的运行时库实现,却无法和其它库一致的方式维护。
  • 使用较弱的语义规则使程序无法有效表达一般的资源管理的抽象并引起其它后果。
    • 不切实际地假定资源总是可延迟释放,损害语言特性的通用性。
      • 阻碍使用程序抽象确定性释放资源的行为。
      • 不允许随释放资源的副作用跟随一等对象在过程之间传递的机制。
  • 事实上鼓励难以理解的代码和欠缺良好工程性质的设计。
    • 典型地,鼓励对理解程序行为不必要而且有害的循环引用。
      • 阻碍关注点分离。
      • 确有必要的循环引用可使用明确的外部所有者不需要依赖 GC 实现。
        • 事实上, GC 充当了退化的所有者。
      • 不利于在程序中推断所有者,使读者在推理资源行为时需要不切实际地负担和语言实现近似的开销。
    • 对学习者产生误导,并损害可用性。
      • 使用户不容易注意到语言保证的程序的抽象语义和实际实现的程序的含义的差异。
      • 使用户忽视资源管理对程序作为实际问题解的正确性这点的重要性。
      • 导致特定应用领域内的路径依赖。
  • 依赖默认提供的全局 GC ,而不允许在程序中指定局部存储策略。
    • 冗余由资源管理机制决定的隐含的全局参数,破坏程序的局域性。
      • 事实上,全局的 GC 充当了退化到全局的所有者。
    • 非 GC 的程序也经常是参数化的(如分配函数),但即便必须使用,用户至少可以选择使用何种策略。
  • 冗余复杂性。
    • 添加允许带状态程序的语言的语义规则的复杂性,
    • 把开发者控制资源管理的能力要求转移给运行维护者,添加冗余假设,

不反对 GC 的合理使用,包括:

  • 程序中的可选的(opt-in) 优化。
    • 通常以存储资源利用率和延迟换取吞吐量;有的实现同时保证有限延迟(实时性)。
    • 作为补充确定性资源管理的机制,通过参数化的组合接口或调度操作允许被选择性地使用。
  • 特定场景下的资源管理机制中的所有者。
    • 如存储资源循环引用检测器。

@yangbowen
Copy link

yangbowen commented Mar 8, 2023

关于除以零( division by zero )的问题。

C和C++没除零异常是你典型想当然的遗漏。刻意不用异常也不是你所说的理由——要知道,除零异常的标准化是所有UB中相对容易的一类(最简单地,搞成信号)。真正的理由虽然还是接口语义上的决策,但明确和想要什么类型系统达到什么目的无关。用WG21的话,这是所谓的narrow contract并且不应当被widen,宁可故意允许UB,也不添加用户程序无法回避的检查开销。

除以零 UB 是 narrow contract ——这个操作的 precondition 是除数不为 0 。从一个 UB 改成任何行为都是可以的,所以说是 难以纠正的历史包袱 我认为是错的。反过来把已经规定的行为改掉才是难以纠正的历史包袱。

考虑这个操作的开销的时候值得考虑两种情况。一种是除数并不为 0 的情况的开销,一种是除数确实为 0 的情况的开销。
考虑产生硬件中断的机器(主要说整数除法指令),然后就是识别这种除数为 0 的情况大致有两种策略。一种是给前面加一个分支判断,这样两种情况下都会增加一个分支的开销(如果除数为 0 的情况 cold-path ,那么这个分支会被准确预测,开销不会太大);另一种就是捕获机器产生的硬件中断,这样(应该是 hot-path 的)除数不为 0 的情况下就没有额外开销(除去捕获并抛出的代码带来的一点空间开销),而除数为 0 的情况下就产生抛出异常的很大的开销。但添加分支判断属于用户也很容易自行完成的事( widen 一个 narrow contract ),而“未发生零除的情况下无额外开销,同时检测偶尔的零除”则是只有实现成抛出异常才能办到的。所以从开销的角度讲,我认为把整数除法除数为 0 改成抛出异常是最合理的。

不止是运行时开销,还有维护spec上消除硬件实现差异的问题。虽然我说过“最简单的”做法,这仍然工程上极端复杂,跟往 C++ 里加 Windows SEH 没太大(不)可行性上的差别。这种异常要整清楚就得把常见 CPU 的异步异常机制在 C/C++ 的层次上整明白,干掉所有不一致的部分,光这坨就比整个 C++ 异常都复杂了。

不论 CPU 对异常采取何种具体的机制,它总归

  1. 达成条件的情况下发生,发生之后通过某种机制能够让程序知道并处理
  2. 发生的情况下,不能继续执行之后的指令,要先处理(异常处理的时候,异常的指令后续的对该指令依赖的指令不能影响架构状态)
    只要这两点成立,我想这东西被封装到 C++ 异常这个抽象当中,应是不难的。

更何况,要支持实现差异广泛的不同硬件,且要消除它们的差异而得到公共的抽象,而且历史包袱重、不好改的,可不止是 C++ ,还有操作系统。用户态程序本就通过操作系统的抽象间接地对异常进行处理,在此之前在 ISA 层面上都发生了哪些细节,对这个处理的spec是不太相关的。


这个意义上,不以 ISA 异常标准化除零异常的应该容易理解。考虑到具体设计的细节,比如如何与其它异常交互之类的问题,ISA 的异常远比现有的几乎所有高级 PL 中的异常机制都复杂。除了先天异步+并行和大多数语言的语义模型不搭这个麻烦的问题(比如考虑如何通知事件完成或取消——注意这里的延迟限制比外部 I/O 严格得太多,实用上这里不能引入本机线程),还在于客观上无法确保不同的 ISA 一定会有相同的设计。对标准化 PL 来说评估兼容不同的可能实现,又要尽量不引入间接层次的性能开销,这很要命。

指令异步并行执行的复杂性并没有阻止汇编语言在很多时候(多线程之类的情况除外)是(这方面)简单的拥有同步指令式语义的语言。从异步并行执行抽象出一个同步指令式语义,在这里跟在 C++ 这样的语言当中,也有根本的相通之处——一先一后的两条指令,其先后顺序无法影响何种架构状态被允许,除非它们访问的东西存在依赖关系。所以只要底层架构尚且允许一个同步指令式语义的汇编语言,那么这种异步+并行对 C++ 也不会有太大问题。
异常的方面也是类似的道理。它总不能都对架构状态产生不可逆转的改变了再来让你处理,那岂不是异常了个寂寞。那只要“后于异常指令的指令还没有发生”,它具体如何异步了又如何并行了,也应是不妨碍其接口的标准化的。


WG21 的讨论中,这种预期的无错误检查的接口和存在“防御性”检查的接口被分别称为 narrow contracts 和 wide contracts 。一般地,只提供 wide contracts 是有害的,因为检查的复杂性基本可以忽略,总是存在一个更自由的 narrow contract 的版本的接口,而同时满足 narrow 和 wide 的需求——封装底层的 narrow contract 的接口并添加检查就得到了后者。而反过来只要不是知道实现细节,一般是做不到的。

我同意这里保留一个“除数不得为 0 的整数除法”的 narrow contract 从性能和其它方面来讲是合理的。问题是截至目前标准 C++ 里仍然没有自动检查 contract 的基础设施。
在没有这样的基础设施的情况下,我认为把“除数传了 0 ”这个还算常见的错误 foolproof 成至少抛个异常而不是 UB 是更好的。
虽然以“程序员皆无能 问题不可能良好解决”为前提设计接口和编程规范是坏的,但也不该以用户和/或实现者自己绝对无错为前提。我认为适量使用 assert 这类设施来检测并立即报错某些易错的地方是好的。
其实话说这些年来的各种 ABI 攻击缓解特性也是这个道理啊。若不是满大街随手写出缓冲区漏洞的 C 程序员,哪有那些缓解特性的用武之地。


再者,还有不少严重的灾难性错误(比如硬件失败)是根本无法检查的,对系统状态一致性的维护天然地就依赖其它的手段。

这个。我的理解是,一个系统不可能从内部检查并纠正任意的错误(因为无法分开“检查错误的”和“可能出错的”),真有那么重要的情况下必须从它以外检查它的错误。


但 C 这样的抽象捉急的设计中,if 加在哪也是有讲究的。加上早期连内联都不健全的年代(这确实是历史包袱,但是也不全是——现在也不可能保持 ABI 解决跨翻译单元内联兼容问题),标准化封装个带有 if 的“安全”操作实在没现实吸引力。

这里我倒认为,倒不妨说翻译单元(对象文件 .o )的存在,或者至少它的用法上的惯例,就是历史包袱。
编译-链接的范式意味着,程序的不同部分(主要以功能逻辑划分)之间的组合方式,受限于 ABI 的非常单一且固定的组合方式。函数内联只是这种组合方式局限性的一小部分。
语言及编译器,而不是有着简单的 ABI 规则的链接器,懂得如何有效地对程序的不同部分进行组合。

在一个理想世界里,语言、编译器、加载器、操作系统被放在一起设计,就算动态链接也能像 同一个源文件里内联并优化 那样高效,而不是让这些接口只能用这种极不 flexible 也不 feature-rich 的方式描述及实现……
而在实际当中,不同层级的组件互相抽象对方的当前情况来进行设计,把整个系统的抽象结构搞得非常糟糕……

@yangbowen
Copy link

是时候对鼓吹无能和剥夺用户自由的愚蠢编程臆想作斗争并纠正这些真正的历史包袱了。

这个。我倒想说,

当你思考如何正确纠正历史包袱的时候,比你不用心的人又用充满无可饶恕的低级bug的产品抢占了第十个市场。

@FrankHB
Copy link
Author

FrankHB commented Jul 26, 2023

是时候对鼓吹无能和剥夺用户自由的愚蠢编程臆想作斗争并纠正这些真正的历史包袱了。

这个。我倒想说,

当你思考如何正确纠正历史包袱的时候,比你不用心的人又用充满无可饶恕的低级bug的产品抢占了第十个市场。

其实不见得需要很有所谓。为什么我非要关心我八竿子打不着的所谓“市场”呢?只要我没靠这个所谓的市场吃饭,我就有底气保持无所谓;至于是不是实际无所谓就看心情了。

正常来讲,只要不是有别的更离谱的缺陷,不犯同等低级错误的一方就能代表有生命力的先进事物。很多东西错的就是错的,只要认知缺陷不改进,就不会有实质性变化。但若不是有生态资源冲突,为什么需要在乎这个呢?让它们自生自灭便罢。

尔曹身与名俱灭,不废江河万古流。

根本上,观点的生命力跟用户多少和市场容量占比根本没有关系。否则,Worse Is Better 早就赢了,连争议都不会有。正是因为不确定的任何涉众都有发现客观缺陷并克服之以达成进步的刚需,即便是占据市场绝对少数,也会不断前赴后继。这个过程中不断会有人发现前人的错误而反复提出相同的问题。这种现象的不断发生,是不以个别参与者意志为转移的客观规律。

我也就是意识到这部分重复劳动过多之后,刻意采取一些措施降低这部分的成本罢了。而真正能够体现独创性的新的东西平时我都大不屑于放到一起讲。

所以,如果所谓的市场没有什么喧宾夺主的显著性的问题,各玩各的相安无事也无关紧要;但如果上门来往我这拉屎撒尿,指鼠为鸭地给我增加无谓成本,那么就得打回去了。

道路是曲折的,斗争是艰难的;未来是有希望的,前景是能乐观的;所以,对不会进化的猴子投降是不可能的。

@FrankHB
Copy link
Author

FrankHB commented Jul 26, 2023

考虑这个操作的开销的时候值得考虑两种情况。一种是除数并不为 0 的情况的开销,一种是除数确实为 0 的情况的开销。 考虑产生硬件中断的机器(主要说整数除法指令),然后就是识别这种除数为 0 的情况大致有两种策略。一种是给前面加一个分支判断,这样两种情况下都会增加一个分支的开销(如果除数为 0 的情况 cold-path ,那么这个分支会被准确预测,开销不会太大);另一种就是捕获机器产生的硬件中断,这样(应该是 hot-path 的)除数不为 0 的情况下就没有额外开销(除去捕获并抛出的代码带来的一点空间开销),而除数为 0 的情况下就产生抛出异常的很大的开销。但添加分支判断属于用户也很容易自行完成的事( widen 一个 narrow contract ),而“未发生零除的情况下无额外开销,同时检测偶尔的零除”则是只有实现成抛出异常才能办到的。所以从开销的角度讲,我认为把整数除法除数为 0 改成抛出异常是最合理的。

ISA 的异常不是 C++ 异常,涉及到非常多的没有在 C++ 中定义的机器实现细节,标准化的工作量很大。现在明显还有大把别的工作要完成。所以这条技术路线几无可能落地。

实际的结果就是没人去提案。

不管是设计是否必要承担这些复杂度,还是实际无法负担标准化开销的问题,都可以概括为“得不偿失”。

不止是运行时开销,还有维护spec上消除硬件实现差异的问题。虽然我说过“最简单的”做法,这仍然工程上极端复杂,跟往 C++ 里加 Windows SEH 没太大(不)可行性上的差别。这种异常要整清楚就得把常见 CPU 的异步异常机制在 C/C++ 的层次上整明白,干掉所有不一致的部分,光这坨就比整个 C++ 异常都复杂了。

不论 CPU 对异常采取何种具体的机制,它总归

  1. 达成条件的情况下发生,发生之后通过某种机制能够让程序知道并处理
  2. 发生的情况下,不能继续执行之后的指令,要先处理(异常处理的时候,异常的指令后续的对该指令依赖的指令不能影响架构状态)
    只要这两点成立,我想这东西被封装到 C++ 异常这个抽象当中,应是不难的。

这种程度的模型太略了,描述清楚行为都不大够,没有多少实用性。即便是 C++ 已有的异常特性,都比这更复杂。看看各种 ABI 中的约定,会让你对这里的设计为什么不够用的理由大开眼界。

更直接地,如果坚持这样做,你首先就得提供在 C++ 中处理异步异常的整套机制。因为你没法排除包括除零在内的这些异常实际上在 ISA 层次上就已经是异步的(无条件假装同步异常会有和预期不一样的行为),更没法去说服硬件厂商回收已经生产的支持异步异常的硬件。

最直接的工作量是描述清楚兼容异步异常处理流程的架构状态。C++ 现有的设计中,暴露异常处理状态(当前异常),反而造成无法优化,有过度设计的问题。约定架构状态在这里更加麻烦。在语言层面上,没有类似 continuation 的抽象,原则上想简单透明支持异步操作基本是不可能的,会有各种各样的 corner case(有 C++ coroutines 的经验,这已经很显然了)。

而一旦按 ISA 字面意义公开能够直接映射状态,还会有人会想该不该要提供一套 ucontext 了。从可移植性的角度来讲,连 ucontext 都很难站得住脚,外加还容易用错,所以 POSIX 都给 deprecated 了,即便在异常以外这实际上也算是接近刚需的特性(模拟实现会慢到离谱)。

我同意这里保留一个“除数不得为 0 的整数除法”的 narrow contract 从性能和其它方面来讲是合理的。问题是截至目前标准 C++ 里仍然没有自动检查 contract 的基础设施。 在没有这样的基础设施的情况下,我认为把“除数传了 0 ”这个还算常见的错误 foolproof 成至少抛个异常而不是 UB 是更好的。 虽然以“程序员皆无能 问题不可能良好解决”为前提设计接口和编程规范是坏的,但也不该以用户和/或实现者自己绝对无错为前提。我认为适量使用 assert 这类设施来检测并立即报错某些易错的地方是好的。 其实话说这些年来的各种 ABI 攻击缓解特性也是这个道理啊。若不是满大街随手写出缓冲区漏洞的 C 程序员,哪有那些缓解特性的用武之地。

Contract 啊,一直是在做了、在做了……然后跳票。不过就算这样,也应该比提案支持异步异常的模型进度快。

再者,还有不少严重的灾难性错误(比如硬件失败)是根本无法检查的,对系统状态一致性的维护天然地就依赖其它的手段。

这个。我的理解是,一个系统不可能从内部检查并纠正任意的错误(因为无法分开“检查错误的”和“可能出错的”),真有那么重要的情况下必须从它以外检查它的错误。

其实可以有专用于错误检查的相对隔离的子系统,从不同角度上看能算是内部的,也能算是外部的。如 IA32 的 machine check 。不过,只要有隔离性,使用起来多少是上下文受限的,更适合当做备份系统容灾而非日用。内部原生支持的错误处理才是日常应该依赖的。

这里我倒认为,倒不妨说翻译单元(对象文件 .o )的存在,或者至少它的用法上的惯例,就是历史包袱。 编译-链接的范式意味着,程序的不同部分(主要以功能逻辑划分)之间的组合方式,受限于 ABI 的非常单一且固定的组合方式。函数内联只是这种组合方式局限性的一小部分。 语言及编译器,而不是有着简单的 ABI 规则的链接器,懂得如何有效地对程序的不同部分进行组合。

翻译单元确实是有历史包袱的成分,不过按现在 C++ 的定义(源代码中的声明序列)其实原则上不存在这里的问题,因为组合程序是各个层次上都存在的问题(源代码还是二进制程序都能有组合问题),本就不存在非得在某个固定的层次上解决某些固定问题的套路;翻译单元原则上也不需要跟 ABI 有关系,实际甚至都不需要一定生成二进制代码(比如整个翻译单元都是空的或者都是内联掉的定义),非得默认把翻译单元映射到 .o 而不是在工具链的实现按需优化流程,也是实现细节(一部分理由是跨工具链的互操作,不过这和强调 ABI 的必要性多少有点同义反复了)。

剩下真正地问题,概括起来就是粒度不够细。从设计简单性和性价比讲,我是历来想要干掉(包括声明在内得)“语句”这种冗余设计,并无视 top-level 构造的(因为光是高级语言的语义上问题就一箩筐),所以默认翻译单元就是表达式,能确保允许用户怎么 AOT/JIT 颠三倒四翻译过来过去(以及是不是有持久格式)都无所谓。但这就是另一回事了。

在一个理想世界里,语言、编译器、加载器、操作系统被放在一起设计,就算动态链接也能像 同一个源文件里内联并优化 那样高效,而不是让这些接口只能用这种极不 flexible 也不 feature-rich 的方式描述及实现…… 而在实际当中,不同层级的组件互相抽象对方的当前情况来进行设计,把整个系统的抽象结构搞得非常糟糕……

一起设计是不可能的,总有不同的工具链厂商,系统厂商更不可能是同一个(特别是考虑硬件适配工作)。真正实际需要的,是一种代替 ABI 的抽象 HIR 规范。CIL 这种还是太 LIR 了。Wasm 的 AST 原本可以是候选,被他们自己放弃了。所以现在还是万古如长夜的状况。大有可为、大有可为……

@yangbowen
Copy link

yangbowen commented Jul 26, 2023

真正实际需要的,是一种代替 ABI 的抽象 HIR 规范。

Contract 啊,一直是在做了、在做了……然后跳票。不过就算这样,也应该比提案支持异步异常的模型进度快。

因为组合程序是各个层次上都存在的问题(源代码还是二进制程序都能有组合问题),本就不存在非得在某个固定的层次上解决某些固定问题的套路

我认为这些问题实际上是密切相关的。
现有大部分 组合程序 方案(包括.o上的链接或者动态链接——把 native ISA 看作一种 IR 的话)所使用的 IR 都是基于“语句”的。这些 IR 围绕着描述“做哪些运算步骤”而不是“运算值之间满足哪些不变量约束”。后者恰恰是优化、查错等需要的信息,而 contract 也恰恰是描述“约束”的语法。
因此我认为你说的这个 HIR 规范必须具备的一个特性是“高效地描述约束,而不是运算步骤”。然后语言层面的 contract 、优化器需要的“证明程序满足何种性质”都能统一到这个上面。


翻译单元原则上也不需要跟 ABI 有关系,实际甚至都不需要一定生成二进制代码(比如整个翻译单元都是空的或者都是内联掉的定义),非得默认把翻译单元映射到 .o 而不是在工具链的实现按需优化流程,也是实现细节

Clang-LLVM 的 LTO 选项会自动编译到 LLVM bitcode .bc 而不是 .o ,算是一个值得一提的例子吧。

@FrankHB
Copy link
Author

FrankHB commented Aug 15, 2023

真正实际需要的,是一种代替 ABI 的抽象 HIR 规范。

Contract 啊,一直是在做了、在做了……然后跳票。不过就算这样,也应该比提案支持异步异常的模型进度快。

因为组合程序是各个层次上都存在的问题(源代码还是二进制程序都能有组合问题),本就不存在非得在某个固定的层次上解决某些固定问题的套路

我认为这些问题实际上是密切相关的。 现有大部分 组合程序 方案(包括.o上的链接或者动态链接——把 native ISA 看作一种 IR 的话)所使用的 IR 都是基于“语句”的。这些 IR 围绕着描述“做哪些运算步骤”而不是“运算值之间满足哪些不变量约束”。后者恰恰是优化、查错等需要的信息,而 contract 也恰恰是描述“约束”的语法。 因此我认为你说的这个 HIR 规范必须具备的一个特性是“高效地描述约束,而不是运算步骤”。然后语言层面的 contract 、优化器需要的“证明程序满足何种性质”都能统一到这个上面。

是相关的,但不同的人关注的方面不同。

硬件厂商自动会关心“语句”,因为那是硬件自身 IR(比如 microcode 和某些 RTL 支持的局部编码构造)基于实现可行性和复杂性的自然选择。对软件厂商,他们中一部分会关心的是 toolchain 这样 infrustructure 支持的可实现性,大多数非理论计算机科学出身的会往硬件方向(后端编码)如何省事这方向去靠。而目标用户和理论研究者的则不一定有这样的思想包袱,因为理论上语句表达的顺序构造不是什么基本的东西,即便广泛存在也只是实现上的简化,并且不需要是一种形态(比如 ISA 的程序计数器,语言运行时的顶层 trampoline ,应用软件框架中的顶层事件循环,其实都是一种东西——“默认”控制)。

尽管不同层次的 IR 都可以描述“做哪些运算步骤”,每引入一层明确不便被下游应用复用的要求都会导致似是而非的冗余,所以在抽象能力上,能描述清楚所有功能需求(不论效率)的 HIR 根本上是必须的。但为了包含最大的涉众(而不至于发现不够用整个推翻重来),这种构造又不能是传统意义上的静态的(对硬件和软件开发者,什么叫“静态”都不能有统一的定义——manufacture-time/translation time/runtime 不可能全部预设)。所以即便这种抽象可以是 contract ,也比现在 C++ 意义上的 contract 外延大得多,至少要能描述用户定义的 phase ,这不可能是一个具体 IR 设计中就能简单原生支持的特性。即便排除这个显式定义 phase 的需求,Racket 的 contract 也更接近这里的目的,注意这里的 idiomatic usage 大到已经能代替整个(静态)类型检查了。所以能满足这里的需求的 contract system 是个历史上前所未有地大的设计,至少能涵盖 gradual typing 对应的 typecheck 。从工程可实现性上,我不觉得这是能直接做到一套 spec 里就可能完工的东西。

翻译单元原则上也不需要跟 ABI 有关系,实际甚至都不需要一定生成二进制代码(比如整个翻译单元都是空的或者都是内联掉的定义),非得默认把翻译单元映射到 .o 而不是在工具链的实现按需优化流程,也是实现细节

Clang-LLVM 的 LTO 选项会自动编译到 LLVM bitcode .bc 而不是 .o ,算是一个值得一提的例子吧。

这是正常的思路,不过需要指出 .o 在 image format 层次上就跟 .bc 并列:a.out、PE/COFF+、ELF、Mach-O ……除了可移植性的现实差别,都是和 LLVM bitcode 能并列的。(考虑到 LLVM 自身就是个 target ,技术上加上 target ISA 这个维度在另一方面也可以是并列的。)由于 LTO 的 opt-in 特色,引入不同 image format 的时机又有更大的变数。于是无论哪方面看,这些都不是基本的东西了,需要在需求上做从用户侧更明确地的拆分才好理清其中具体特性的必要性(所以对应用来说自然就有很多是实现细节)——这也要求引入外部 HIR 才能提供的 high-level 抽象描述能力。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment