| ||||
| 终于有人回复了…… T.T 其实,我现在最想看到的是给gc揭短的文章,因为兜售gc的文章已经太多了…… 其实,gc只是内存自动管理机制的一种,而且还带有很大的不确定性……桟就是一种自动机制,只不过它的生存期模型很特别,所以很多情况下表达能力不够。所以我想,也许可以思考其他的生存期模型,不一定是桟,而是其他的模型,但是确定性仍然要比较高……如果能有更多的像桟这样的确定型的自动模型,那么依靠它们的互补也许就可以做到在绝大多数情况下不需要使用gc这种高度不确定的模型了…… 我很少看到文章从这个角度考虑问题。但是前几天搜索到一些论文,虽然也比较老,但是其中谈到了基于Regions的管理方法,其实就相当于内存池,但是是变成了一种语言特性。这是我所看到的唯一有点接近上面那个桟机制的思考角度的文章…… Elminster有什么看法不? ![]() |
| ||||
| 引用:
汗,好像扯远了…… 总之,从语言设计的角度看,gc就是一个可以选择的东西。因为是否使用gc是针对具体应用进行分析时所作的选择,也许某些机械的事情程序员做得不如程序好,比如增加和减少refCount,但是是否使用gc这种决策,应该相信没有机器能做得比人好,也没有人能够在设计语言时就替别人做出选择。 |
| ||||
| 引用:
引用:
GC 是不是一个可选项,取决于这个语言打算支持什么样的程序设计范式。如果打算支持函数式程序设计范式,那么 GC 就是必须的;同样的,书写面向对象程序的程序员如果能有 GC 的支持,也会舒服得多,能够应用更多可以提高生产力的技术。是否使用 GC 这种决策,确实没有机器能够比人做得好,不过设计语言的时候做出这个决策却一点问题也没有:虽然你不能决策这个语言是否内建 GC,但是你可以决策究竟使用哪种语言,不是么? |
| |||
| 完美的确定性会带来太多的不便,从而导致工程上不可行。stack确实是确定的,但是它的表达能力并不够强。所以我们才需要heap (或者说为了得heap的表达能力代价太大). 那gc和stack作比较本来就没啥用。stack很优雅,但heap很有用。工程上可能更需要一些有用的东西吧。 .NET宣传它的GC在generation 0的一次collect平均只需要1不到毫秒,所以对performance的影响也没有想象中那么巨大。(也可以用perfmon看到,我姑且相信他的说法……)主要原因是generation 0的大小正好可以放在L1缓存,但现如今的L1 cache都够大了,放好多个string什么的没啥问题,何况还有大对象自己的GC generation. 另外扯出去,C++做的难用感觉上一个原因就是它目的太多,又要最底层的操控又要高级的抽象。另外感觉上当初设计的时候也没考虑周全…… sigh 此帖于 2008-02-15 09:16 PM 被 cat 编辑. |
| ||||
| 不知道你的“完美的”确定性是个什么样的概念…… 我觉得确定性本身不会带来不便,带来不便的只是有限的表达能力,这跟确定性不是一回事。 stack的表达能力的确有限,但是在其表达能力之内的事情,它却能处理得很好。我觉得我们需要的是更多像stack这样的确定型的生存期模型,用来填补stack的表达空白。 使用gc,其实丢弃了很多信息,我觉得这是种浪费。可以说gc可以让程序员免于生存期分析,但是这样的安逸自然也有代价。所以在我眼里,gc永远都是下下策,永远都是别无他法时的选择。 gc的性能不仅要考虑一次collect的时间,还要考虑对cache miss和内存换页的影响。 C++难用的原因,我结合其他人的文章,我觉得是人们误用的程度比C++自身的难用更严重。C++想要做成通用目的的语言,但是使用的时候人们却往往想要既全面又深入的理解C++,这是不必要的。根据自己的需要来选择C++中的特性,我觉得这样C++就不是那么的难用。 |
| ||||
| 引用:
|
| ||||
| 首先我假设任何内存块的回收时机都存在一个事实上的最佳时机。过早的回收会导致程序出错,就不去考虑它了。而过晚的释放会有这样几种趋势影响: 1.倾向于占用更多的内存。 2.倾向于导致更高的cache miss概率和内存页面错误率。因为如果及时的回收,将能够使得内存占用的分布更加的紧凑,而且这种紧凑是不需要对对象进行复制的。内存回收的滞后会倾向于使内存占用铺得更开。 3.难以把其他资源的管理绑定到内存管理上。首先,不管内存怎么降价,它都不是免费的资源。第二,还有其他的资源可能比内存更加宝贵,无论是os资源、硬件资源,还是应用程序自己的各种资源,都不能假设它们便宜到“不需要尽快回收”的地步,毕竟将来产生的新资源不应该受这种假设的约束。第三,就我所知,RAII是把各种资源绑定到内存上进行管理的一种比较好的办法,但是它是用析构函数把内存的释放和其他资源的释放绑定到一起的。第四,内存释放的滞后意味着其基础上的RAII对其他资源的释放也是滞后的,更重要的是,如果没有手动的调用collect,那么gc对内存及绑定其上的其他资源的释放时机是未知的,虽然可能不会等多久,但是我们无法确认其释放是否能够做到“及时”。如果手动调用collect,但是这个collect是针对所有类型的对象的,那么各种不同生命期模型的对象会使collect的时机很难正确把握,毕竟用了gc就说明这些对象的生命期模型对于程序员来说不是非常明确的。如果collect是有很强的针对性的,那么我不认为这是gc,我会认为这是内存池。在我看来,内存池与gc的不同是,使用gc时程序员对各种对象的生命期的知识是非常少的;使用内存池时,虽然程序员无法明确各个对象的生命期细节,但是对于某个对象集合的生命期还是比较了解的,所以才能把握好释放内存池的恰当的时机。虽然内存池可以与gc的一些技术结合(比如内存扫描),但是其设计思想上的分界线在我看来还是比较明显的,并且结合后所引入的内存扫描也只是这种内存池的辅助功能而非关键。 4.gc线程与应用程序之间的切换可能会带来一些问题。如果不是增量gc,那么会引起应用程序停顿的问题。如果是增量gc,那么相对于非增量gc会产生更多的线程切换和竞争。如果可以讨论极端情况,那么一个极端是gc的频率很低,于是导致非增量gc那样的应用停顿问题;如果gc的频率很高,那么线程切换和竞争也会降低整体效率,仍然可能使应用的响应变慢。理论上如果在这两个极端之间有一个良好的平衡点,那么以上的极端问题都不会严重。但是对于应用环境会变化的应用来说,寻找这样的平衡点也是个麻烦。在以后CPU内核越来越多的环境中,我怀疑寻找这样的平衡点会越来越困难。我觉得,gc可能会在同一个线程中处理很多原本是低耦合的内存块,这些内存可能由很多不同的线程使用,它们本身没有太多关联,但是如果要由同一个gc线程来处理它们,那么就认为使它们发生了耦合,结果就是使得原本低耦合的各个应用线程因为gc而提高了耦合度,线程之间的高耦合会因为竞争而降低性能。当然这最后一点与gc的不确定性关联不大,不过也是我不看好gc的理由之一。虽然猜测的成分很大,不过毕竟让我不放心。 关于gc“丢弃信息”的问题,我举个极端的例子。就我所知,java里程序员无法在桟上建立对象,于是即使是生命期很明确的对象也需要依赖gc或者vm的自动化分析。这个例子中,程序员对对象生命期的知识就无法明确的在程序里表达出来,这就是对知识的丢弃。java的vm好像能够进行自动化的分析,但是这是先让程序员把自己知道的知识丢掉,然后再让程序来进行分析,就我所知目前即使是AI也还没有人那么聪明。 我对这些的理解也很肤浅,所以还请各位大大多多指教口牙…… ^_^! |
| ||||
| 个人觉得就这个问题讨论下去,只能是无休无尽的口水仗。这在很多论坛上都是发生过的。 说说我个人的观点。也许是混的日子长了,我现在不怎么认死理了。 * 我以前相信世上存在最伟大的语言(C++)和一统江湖的方法论(曾经是OO后来是OO+GP),现在我知道每个语言(哪怕是个脚本)都很不简单。并且存在着众多同样优秀的方法论。 * 世上不止只有C++和Java两个作品,也不仅只有design pattern这一组智慧结晶。 * 一把钥匙开一把锁,不存在万能钥匙。如果把计算机语言比作钥匙的话。 * 行万里路,读万卷书,越学习越觉得发现自己知道得太少。 退回到语言和GC的话题上来,我首先推荐的两个例子是lisp和smalltalk,一个是60年代发明的,一个是70年代发明的,那时内存多紧张我就不多说了。相信了解完它们后就可以释怀GC这个题目。因为GC不是个问题。 同样我也推荐看看lambda演算,看看另外一种表达可计算性的体系。并思考在那个体系中GC扮演什么角色。 曾经《程序员》杂志的编辑批评我用C++来写语言解释器是“空对空”,毫无意义。他觉得我应该干些实事,用C++多写一些网络和操作系统方面的东西。我的回答是,C++对我来说就如同数学公式,我只用它来帮助表达思想。 |
| |||
| 够用就好,有些华丽丽的东西你能理解也能用好不说明别人也能理解能用好。 再有就是你说“难以把其他资源的管理绑定到内存管理上”让我觉得你希望找到一个通用的资源管理模式。不过实际上各种资源都有他自己的区别,从而也没必要有一个通用的管理模式。比如内存,现在gc很好很快,内存也是白菜价,就不需要太扣,可以减少程序的错误也可以提高写code的速度。但是对于其他一些资源就不是这种情况,比如OS资源和硬件资源,甚至特定环境下的内存资源(对于这种对内存控制要求非常精确的环境,C#/Java等语言直接是“不适用”,用别的语言好了,这些语言也不是为了支持这种应用而设计的)。 另外,我也不觉得会存在什么完美的资源管理模式,总是各有各的优缺点。GC的优点就是方便,而且在目前很多应用里工作的非常好,缺点么你也列了不少不过在某些应用中都可以忽略不计。然后其他管理模式的优点可能是精确,最优化内存使用,但缺点就是不够方便。取得某一方面的优势往往不是免费的。 还有你一直很关心效率问题,但是效率真的可以这么省吃俭用下来吗?在C#/Java所在的应用里面,真正的瓶颈往往在DB, 硬盘, 用户输入,网络延迟等。多了几次线程切换又如何?少了一点cache hit又如何?比起那些来就是小巫见大巫。你看看你得task manager上CPU占用最多的是谁?还不是System Idle Process? 闲着也是闲着,整理整理内存不是正好?不要觉得用了GC地球就会变暖。 另外就是GC的效率问题,对于特定的应用可以自己测测看,就我们用下来效率还是很不错的。.NET的话gen 0的清理本来就利用了cache, 然后分级也解决了不少问题。我们的某个网络服务本来C++写的,担惊受怕到处check空指针到处check HResult到处copy string(有人要说了,怎么这么写C++?? 这是网络服务,7*24运行的,如果因为一个空指针Access Violation然后进程重起虽然后面有人看着总还是不太爽。至于string copy么,传给了某人不知道生存期会如何,就让“某人”copy一份自己留神吧……我还要传给别人呢……). 后来转成了C#测试的latency比原来小了30%. 里面主要的提升是什么?是因为人不够聪明写得太多的null pointer check被优化掉了?因为那些HResult check变成了exception check? (C++也有exception, 但是COM对象太多大家都不用) 因为少了那些string copy? 我也不知道,但个人觉得换了新的CPU更快的硬盘才是最大功臣。 再来点空洞的,某些东西是个好东西,但也有他自己的应用范围,如果把一些不适用的情况下强行去用的话,乍一看很有趣,很cool, 很优雅,但其实隐患无穷。语言也是如此, stack也是如此。 此帖于 2008-02-16 11:45 PM 被 cat 编辑. |
| ||||
| 看了你的帖子之后我才想起来,我没有说明我所考虑的应用领域…… 我所针对的是网络游戏。对于游戏来说,无论是CPU还是内存,都是不能随便挥霍的东西。特别是那些想要有所创新的游戏,更要尽量节省出各种资源,为新的特性留出余地。 我的确是想要找到一种通用的资源管理办法。资源各有不同,这一点我同意,但是这不代表我们不需要通用的管理办法,也不代表我们不可能找到这样的办法。而且,所谓通用的办法,也并不是用一个一成不变的死板的机制来管理,而是在一个通用的机制基础上进行定制。我所理解的RAII,就是在内存管理这个机制的基础上来定制其他资源的管理。内存管理实现了各种内存生命期模型,RAII则把各种资源的生命期与内存生命期绑定起来,这样对于非内存的管理,我们只需要实现这个绑定过程,以及在生命期的开头和结尾的针对性操作,而生命期模型则靠内存管理来维护。我觉得资源管理最困难的不是如何申请和释放资源,而是决定何时释放什么资源。内存管理实现了对内存释放时机的控制,这解决了最大的难题,RAII使得我们不需要为每种不同的资源重复解决这个难题。由于RAII与目前的gc很难结合,所以我觉得gc在提供了一个不错的内存管理机制的同时,恶化了其他资源的管理。 不存在什么完美的资源管理模式,这我也认同。但是,一方面,目前对gc的批评淹没于对gc的追捧之中;另一方面,我觉得应该大力发展其他确定性高的内存管理机制,与stack等高确定性机制形成互补,我觉得这种局面比gc包揽全场要更好,因为我觉得让一群各有所长的专家互相配合会比一个博而不精的人做得更好。 在特定的领域,gc不会带来太多的问题,这一点我同意。但是对于游戏等性能敏感的应用,优化仍然是有必要的。当然其可行性就要看能不能发展出更多的像stack那样优秀的高确定性内存管理机制。 关于空指针,我觉得本来是不需要到处检查的,因为完全可以做个类把指针封装起来,并且对对象进行统一的生命期管理。再结合异常,把出错情况(如访问空指针)集中在一起处理,应该也不会带来太多麻烦。手工编写大量的重复性的代码来进行有效性检测,这似乎更像C而不是C++的做法。另外曾经听别人说过这样的感受:他们觉得随着经验的增长,对new/delete的调用在不断变少,并且也没有使用gc。如果这是真实的、具有一定普遍性的,那么我觉得这从侧面印证了stack等内存管理机制的有效性。 至于拷贝语义,我觉得这是好的。拷贝语义实际上具有解除耦合的作用,并且在性能上不一定是无法接受的,毕竟gc也可能会进行对象的拷贝。另外拷贝语义也能提高程序访问的局部性。所以我觉得一开始大量使用拷贝语义也是可以的,在确保正确性之后再根据profile的结果来进行局部的优化,最终也可以在性能上有所确保。我觉得如果性能能够接受的话,应该优先使用拷贝语义。如果说存在一些性能之外的理由来阻止使用拷贝语义,那么我目前能够想到的只是一种情况:当需要在一个模块内直接修改另一个模块的状态时,这时因为没有接口可用,所以无法使用拷贝语义,但这个情况说明模块间的耦合度是很高的。从这个角度看,如果没有明确的性能方面的理由(时间或空间),却有大量的接口无法使用拷贝语义,那么说明设计上出了问题。 我对C#并不了解,但是我知道,即使同样使用C++编写的程序,也会有快有慢、有好有坏。当一个人从C++转向C#之后获得了收益时,可以理解为C#这个语言写出的程序的平均水平高于C++的平均水平,也可以理解为两种语言的能力的下限之间的比较,或它们的上限之间的比较,或者掌握的难易程度的比较。所以在获得更多更详细的信息之前,我觉得这样的比较很难说明什么具体的问题。 关于硬件带来的性能提高问题:有一种观点,觉得软件优化的最好手段就是什么都不做,等上18月后更换更好的硬件。这话是有道理的,但是对于用户来说,他们要花费额外的金钱;对于硬件开发人员来说,这是软件开发人员在推卸责任。现在单个CPU内核的性能提高已经非常困难,于是开始以添加内核数量的方式保持硬件性能的增长速度。但是如果软件开发人员不进行针对的设计和优化,那么单线程的任务就无法从硬件升级中获得收益,硬件开发人员再也不能当作挡箭牌了。 应用范围的确是个问题,之前的讨论一直没有明确这一点,这是个失误~ ^_^!!我个人关注的还是那些性能(时间和空间)敏感的应用。不过,我看不出stack有什么无穷的“隐患”,如果它不被误用的话…… 另外,我思考这些内存管理问题的原因之一,就是看到越来越多的观点,他们认为gc对其他内存管理机制的替代性越来越高,所以我需要考虑是否应该全面拥抱gc,是否应该把gc作为最主要的内存管理手段。我个人的结论是:不行,至少目前还不行,至少目前在游戏领域内还不行。 ^_^!! |
| ||||
| to cat: 这里例子对C++可是不公平的,因为如果直接把C++代码写的好一点,可能也会变快很多 ![]() to sjinny: C++代码里面我用的最多的资源管理方法,对我来说是ref counted smart pointer。这个其实也是GC的一种。相比其它GC方法来说,sp具有比较高的确定性,但是在多线程情况下也需要或多或少的同步。 至于你要求的堆栈一级的析构确定性,和GC并不冲突,你可以同时拥有两者。我不是GC的热衷爱好者,但是,我觉得大部分情况下,GC是一个plus,它没有从我手里夺去什么。大多数时候你的不爽,是语言级别的,和GC无关。 |
| ||||
| 嗯,跟其他人讨论的时候,有的人把ref counted smart pointer当作一种gc;但也有人不认为这是gc,他们认为要采用了内存扫描技术的才能算。 ref counted smart pointer是“类gc”技术里我相对最喜欢的技术了,虽然循环引用问题让人不爽。 关于智能指针的同步问题,我想了一下,可能可以这样做: 首先,每个线程都会有个接收共享对象的接口,在这个接口里,会首先在堆上创建一个智能指针,这个智能指针指向的是传入的共享对象,然后再在桟上建立一个智能指针,它指向的是之前那个堆上的智能指针,当前线程对贡献对象的访问全部通过这个桟上的智能指针(及其副本)完成,而那个堆上的智能指针对象是不会被直接访问的,堆上的智能指针对象的地址也不会被传递给其他线程,如果要把共享对象传递给其他线程,那么就要把堆上的智能指针复制给其他线程。 class SharedObject;//共享对象 typedef SmrtPtr<SharedObject> SharedP;//一级SP typedef SmrtPtr< SmrtPtr<SharedObject> > SharedPP;//二级SP SharedObject和SharedP都在堆上建立实例,SharedPP只在桟上建立实例。同一个进程中,对于同一个SharedObject,一个线程里同时最多只有一个SharedP指向它。某个线程里,多个SharedPP指向一个SharedP。当一个线程对SharedObject使用完毕后,它的SharedPP都被销毁,此时线程中的SharedP被销毁。对于一个线程来说,仅仅在接收SharedObject和使用完毕后需要修改SharedObject的refCount,仅需要加锁两次。 这样每个线程仅仅加锁两次,我想大多数应用这也是能够接受的开销。空间上也只是多了一个SharedP。如果把SharedPP做成专门的类,那么使用时也能像普通的SmrtPtr一样方便,并且堆上的那个SharedP可以被完全隐藏起来。 不知道这样能不能达到“或多或少”里的那个“少”的程度。 嗯,每当谈到gc的时候我想到的第一个东西就是java,现在想起来C#里也是有桟的。不过一块内存的直接管理者只能是这两者之一,所以还是有个选择问题的。 在大多数情况下,gc会降低我对我的代码的理解程度。如果说没有gc时理解各种各样的存储管理会很费力,那么有了gc后,很多存储管理我是想看都看不到了。所以如果使用gc的话,付出的代价就是对程序掌控的减弱,当然其中还有gc与RAII难溶这一因素。 所以虽然以后C++会加入gc,但是我仍然把gc当作最后的选项。 ^_^! |