<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">博客园_Milo的游戏开发</title><subtitle type="text"/><id>http://feed.cnblogs.com/blog/u/66551/rss</id><updated>2011-06-14T14:00:25Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><generator>feed.cnblogs.com</generator><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/"/><link rel="self" type="application/atom+xml" href="http://feed.cnblogs.com/blog/u/66551/rss"/><entry><id>http://www.cnblogs.com/miloyip/archive/2011/06/14/alice_madness_returns_hair.html</id><title type="text">爱丽丝的发丝──《爱丽丝惊魂记：疯狂再临》制作点滴</title><summary type="text">今天(2011年6月14日)是《爱丽丝惊魂记：疯狂再临 (Alice: Madness Returns) Xbox360/PlayStation3/PC》(下简称《爱》)正式发售日，身为其开发程序员之一，特撰此文以作纪念。简介《爱》(图1a)是一款由上海独立游戏工作室麻辣马(Spicy Horse)制作、美商电艺(Electronic Arts)发行的惊悚动作冒险游戏。此全乃2000年发行的《爱丽...</summary><published>2011-06-14T08:27:00Z</published><updated>2011-06-14T08:27:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2011/06/14/alice_madness_returns_hair.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2011/06/14/alice_madness_returns_hair.html"/><content type="html">&lt;p&gt;今天(2011年6月14日)是《&lt;a href="http://www.ea.com/alice"&gt;爱丽丝惊魂记：疯狂再临 (Alice: Madness Returns)&lt;/a&gt; Xbox360/PlayStation3/PC》(下简称《爱》)正式发售日，身为其开发程序员之一，特撰此文以作纪念。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;简介&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;《爱》(图1a)是一款由上海独立游戏工作室&lt;a href=" http://www.spicyhorse.com/"&gt;麻辣马(Spicy Horse)&lt;/a&gt;制作、&lt;a href="http://www.ea.com/"&gt;美商电艺(Electronic Arts)&lt;/a&gt;发行的惊悚动作冒险游戏。此全乃2000年发行的《爱丽丝惊魂记(American McGee’s Alice) PC》(图1b)的续作。&lt;/p&gt;&lt;img width="300" src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/o_image01.jpg"/&gt;&lt;img width="300" src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/o_image06.png"/&gt;&lt;p&gt;图1(a): 《Alice: Madness Returns》Xbox360封面  (b): 《American McGee’s Alice》PC封面&lt;/p&gt;&lt;p&gt;在为期超过两年的制作期间，《爱》的制作团队最高达75人，另外有50人左右的美术外包团队。《爱》的制作团队有许多不同国籍的成员，但当中主要为华人。从制作地点及人员来说，《爱》可以说是一个国产游戏。但从目前的环境来说，《爱》应该不会在国内发行。&lt;/p&gt;&lt;p&gt;《爱》使用&lt;a href="http://www.unrealengine.com/"&gt;Unreal Engine 3&lt;/a&gt;开发，并使用了&lt;a href="http://www.scaleform.com/"&gt;Scaleform&lt;/a&gt;、&lt;a href=" http://usa.autodesk.com/adsk/servlet/pc/index?id=11390544&amp;siteID=123112"&gt;Kynapse&lt;/a&gt;和&lt;a href="http://www.radgametools.com/bnkmain.htm"&gt;Bink&lt;/a&gt;中间件。在PC平台上，合作伙伴nVidia加入了使用GPU加速&lt;a href="http://www.nvidia.com/object/physx_new.html "&gt;PhysX&lt;/a&gt;效果。但游戏主角爱丽丝的头发和衣饰模拟，并非使用PhysX，而是一个自定义解决方案，这也是本文将谈及的主要内容。&lt;/p&gt;&lt;p&gt;有时候，需求和技术，就像是鸡和蛋的关系──因某需求而开发新技术，或因某技术而产生新的需求。本人在《爱》的开发过程，清楚体会到这个关系。让我细细回想当天的事……&lt;/p&gt;&lt;p&gt;&lt;strong&gt;研究之始&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;2009年8月23日(星期日)，刚入职满三个星期了。这段时间的工作，主要是按游戏策画的需求，做了一些简单的游戏性编程(gameplay programming)，例如是一些关卡内的机关，这正好让我学习一下&lt;a href="http://udn.epicgames.com/Three/UnrealScriptReference.html"&gt;UnrealScript&lt;/a&gt;(Unreal引擎的脚本语言)。&lt;/p&gt;&lt;p&gt;上周看到新版本的主角模型，虽然比旧版本更精细，但我看上去觉得还有改善空间，于是分别和美术总监和动画总监讨论，大家也认为现时对头发和衣饰以手工关键帧动画(keyframe)方法表现，效果不够理想，而且用Phong反射模型来渲染头发，有点像塑料玩偶的感觉。从动画的工作来说，头发和衣饰的关键帧动画要做得自然，并不容易；尤其动画间的混合(blending)更为困难，不是动画不自然加减速，就是会穿过身体。&lt;/p&gt;&lt;p&gt;传统上，许多游戏会避免把角色设计为长发，也会避免穿着长裙。但爱丽丝无可避免要触犯此二禁忌。既然如此，何不尝试进行突破，并以此为游戏特色呢？&lt;/p&gt;&lt;p&gt;当天虽是周日，我在上海孤单一人，在炎夏就不外出了。脑里不断思考着上周工作上的事情。不过单单在想也没有用，就直接敲键盘实验一些方案。最先想到的，是常用于模拟绳子和布的弹簧质点系统(mass-spring system)，记得以前看过相关的入门文章[1]，就以该文的基础。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;弹簧质点系统&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;所谓弹簧质点系统，其实就是仿真一些有质量的粒子(质点)，再在粒子之间加入一些无质量的虚拟弹簧。例如要模拟一条绳子，最简单的方法是建立n个粒子，再在每两个连续的粒子之间加入弹簧，即有n-1个弹簧，如图2。&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image10.png"/&gt;&lt;p&gt;图2: 用5个粒子和4个弹簧模拟的绳子&lt;/p&gt;&lt;p&gt;要模拟粒子运动，可使用《&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/06/14/Kinematics_ParticleSystem.html"&gt;用JavaScript玩转游戏物理(一)运动学模拟与粒子系统&lt;/a&gt;》一文中谈及的欧拉方法(Euler method)，但[1]里介绍的Verlet数值积分在很多情况下是更好的选择。我使用了含简单阻尼效果的Verlet数值积分方程:&lt;/p&gt;&lt;div class="math"&gt;\mathbf{x}(t + \Delta t) = \mathbf{x}(t) + d \cdot (\mathbf{x}(t) - \mathbf{x}(t - \Delta t)) + \mathbf{a}(t) \Delta t^2&lt;/div&gt;&lt;p&gt;当中，&lt;span class="math"&gt;\mathbf{x}(t)&lt;/span&gt;是时间在t时粒子的位置，&lt;span class="math"&gt;\Delta t&lt;/span&gt;为时步(timestep)，&lt;span class="math"&gt;d \in [0,1]&lt;/span&gt;的阻尼系数，&lt;span class="math"&gt;\mathbf{a}(t)&lt;/span&gt;为时间在&lt;span class="math"&gt;t&lt;/span&gt;时作用于粒子的加速度(即当时作用于粒子的力除以其质量，例如引力加速度&lt;span class="math"&gt;[0, 0, -9.8]&lt;/span&gt;)。Verlet方法分的计算简单，不需保留或计算速度(velocity)，也比欧拉稳定，但缺点是时步(&lt;span class="math"&gt;\Delta t&lt;/span&gt;)必须是固定的。&lt;/p&gt;&lt;p&gt;Verlet积分的另一特点，是可以简单地加入各种约束(constraint)，例如某粒子在仿真之后，其位置位于地面以下，只需把粒子移至最近地面的点。对于绳子，另一约束就是相邻粒子的距离，在Verlet积分下，此距离约束可以模拟弹簧。假设两个相邻粒子的位置为&lt;span class="math"&gt;x_1&lt;/span&gt;、&lt;span class="math"&gt;x_2&lt;/span&gt;，两者间的止动长度(rest length)为&lt;span class="math"&gt;L_r&lt;/span&gt;，则可以这样调节两粒子的位置：&lt;/p&gt;&lt;div class="math"&gt;\begin{matrix}\mathbf{x}’_1 = \mathbf{x}_1 - (\mathbf{x}_2 - \mathbf{x}_1) \cdot \frac{\left \|  \mathbf{x}_2 - \mathbf{x}_1 \right \| - l_r}{2 \left \|  \mathbf{x}_2 - \mathbf{x}_1 \right \|}\\ \mathbf{x}’_2 = \mathbf{x}_2 + (\mathbf{x}_2 - \mathbf{x}_1) \cdot \frac{\left \|  \mathbf{x}_2 - \mathbf{x}_1 \right \| - l_r}{2 \left \|  \mathbf{x}_2 - \mathbf{x}_1 \right \|}\end{matrix}&lt;/div&gt;&lt;p&gt;此外，要模拟头发，必须避免头发移动至头颅及其他身体部分之内，此仍碰撞检测(collision detection)和碰撞决议(collision resolution)。如前所述，这部分也可以用约束来表示。由于头颅较接近球体，在此简单测试中，只加入一个球体去进行检测。此约束把球体内的粒子推至最近的球面上。&lt;/p&gt;&lt;p&gt;要同时满足多个约束，最简单的方法是松弛法(relaxation method)，即进行多个迭代，每次执行所有约束一次，那么其结果就会就趋近合乎所有约束的解。当天写的测试程序，其数据结构和伪代码表示如下:&lt;/p&gt;&lt;br/&gt;// 节点(粒子)&lt;br/&gt;struct Node {&lt;br/&gt;        Vector3 p0, p1; // 前帧/本帧的位置&lt;br/&gt;        float length; // 和上一节点的止动长度&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;// 发束&lt;br/&gt;struct Strand {&lt;br/&gt;        size_t nodeStart, nodeEnd; // 此发束中，节点数组的起始和结束索引&lt;br/&gt;        Vector3 rootP; // 发根的局部坐标(相对于头的变换)&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;SimulateHair(nodes, strands, sphere, damping, dt, headToWorld)&lt;br/&gt;    // 对每个节点进行Verlet积分&lt;br/&gt;    for each n in nodes&lt;br/&gt;    a = Accumulating force for n, divided by mass // 现时只是引力加速度常量&lt;br/&gt;    p2 = Verlet(n.p0, n.p1, damping, a, dt)&lt;br/&gt;    n.p0 = n.p1, n.p1 = p2 // 以新状态取代旧状态&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;    // 对每束发丝以松弛法进行约束求解&lt;br/&gt;    for each s in strands&lt;br/&gt;        for a number of iterations&lt;br/&gt;            for index = s.nodeStart to s.nodeEnd - 1&lt;br/&gt;                na = nodes[index]&lt;br/&gt;                nb = nodes[index + 1]&lt;br/&gt;                // 碰撞检测和决议&lt;br/&gt;                nb.p1 = collideSphere(sphere, nb.p1)&lt;br/&gt;                // 长度约束&lt;br/&gt;                na.p1, nb.p1 = lengthConstraint(na.p1, nb.p1, nb.length)&lt;br/&gt;&lt;br/&gt;             // 固定发根&lt;br/&gt;             nodes[s.nodeStart].p1 = transform(headToWorld, s.rootP)&lt;br/&gt;&lt;p&gt;用程序产生一些发束，并把模疑结果用直线线段渲染出来，就做成图3的效果：&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image04.jpg"/&gt;&lt;p&gt;图3: 最初的头发实验&lt;/p&gt;&lt;p&gt;程序中，能使用鼠标旋转头颅，表现暮然回首的飘逸；也可改变引力方向，表现风吹秀发的感觉。这个花了一天时间写的程序实验，其实并不复杂，至少比写这篇博文容易。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;把研究放进日程&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;次日，把成果带到公司，向程序同事和动画同事演示，决定把这个初步构思带到周三的定期技术会议。除了继续完成之前的工作，也花了些时间搜集、阅读关于实时头发模拟及渲染的文献，并把一些想法写到项目的wiki里。&lt;/p&gt;&lt;p&gt;终于到了周三的定期技术会议，把程序向项目组的主要决策者(包括制作人、创意总监、美术总监、技术总监等)演示，并展示一些文献里的最终渲染效果。基本上反馈是正面的，一些主要讨论重点大约如下:&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;问：使用程序化的头发，相比手工动画有何好处？&lt;br/&gt;答：节省工作量，而且效果应该会比手工更好。另外，《爱》中有海底场景，可使用阻尼等参数模拟水里的头发飘动效果；在室外、天空上的场景，也可以加入风的效果。&lt;br/&gt;&lt;/blockquote&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;问：此技术会否很耗CPU/GPU时间？&lt;br/&gt;答：具体开销暂时未能确定。由于我们游戏在Xbox360/PS3上，GPU应该会成为瓶颈，所以可以考虑在Xbox360使用空闲的CPU核，在PS3上使用SPU，去进行头发模拟。若假设发束之间无互动关系，还可以使用这种并行性作多核/多SPU并行加速。&lt;br/&gt;&lt;/blockquote&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;问：三维美术方面如何去设定发型？&lt;br/&gt;答：最简单的方法，是使用额外的骨头(bone)去设定发型，那么就不用更改导出工具或编写特别的工具。模拟中可以加入额外的约束，使发束自然回复至预设的发型。&lt;br/&gt;&lt;/blockquote&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;问：为甚么其他市面上的游戏不用这种技术？&lt;br/&gt;答：……(当时真答不出来，但也许这个问题也是我的重要得着，详见后文)&lt;br/&gt;&lt;/blockquote&gt;&lt;p&gt;然后也讨论了预计所需的研发时间。最后决定可以继续第一阶段的研发，以成果决定是否继续下一阶段。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;头发渲染&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;获准研究，除了我感到兴奋，动画组同事也非常希望此技术能成功应用到游戏，因为此技术会大大节省他们的工作量。因此，他们也热心准备爱丽丝的测试用发型数据。为了更容易设定发型，只要求建立一个头皮(sculp)的三角形网格，并在网格上每个顶点上加入一串骨架，以代表引导发束(guided strands)，程序中按LOD细分(subdivide)头皮网格，并以插值方式产生引导发束之间的发束。&lt;/p&gt;&lt;p&gt;我更新了测试程序，使用D3DX库去导入爱丽丝的白模及发型，再把发型数据转化为仿真用的节点和发束数据结构。接着是先尝试渲染部分，然后再改进模拟部分。&lt;/p&gt;&lt;p&gt;我尝试过几种渲染方法。[2]中以线表(line list)去渲染发丝，要表现稠密发丝所需的像素数目(primitive count)很大，在目标平台上需要占用很多GPU时间。而另一种方法，是把发束线段向屏幕空间展开，形成固定宽度的三角形表(triangle list)或四边形表(quad list)。&lt;/p&gt;&lt;p&gt;为了使用较少的节点而得到圆滑的发束，我采用了均匀三次B样条(uniform cubic B-splines)，把原来的发束插值为曲线(图4)，之后才把该曲线展开为三角形表。插值的数量可以成为运行期动态LOD的参数。&lt;/p&gt;&lt;p&gt;图4: 绿色线段为模拟结果，橙色为三次B样条&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image08.jpg"/&gt;&lt;p&gt;至于着色方面，采用了较简单的Kajiya-Kay模型[3]。此模型基于切线(tangent)而非法线(normal)，能表现出头发的高光(图5)。&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image02.jpg"/&gt;&lt;p&gt;图5: 早期基于Kajiya-Kay反射模型的着色&lt;/p&gt;&lt;p&gt;&lt;strong&gt;改进模拟&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;之前的做法，虽然能模拟出一条绳子，但它的行为更像一条锁链，因为它是完全柔软的，而真实的绳子在止动时通常是直线的，弯曲绳子需要施力。一个简单的实现方式是再加入弹簧(长度约束)，连接相隔一个粒子的每对粒子(图6)。&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image00.png"/&gt;&lt;p&gt;图6: 加入防止弯曲的长度约束(红色)&lt;/p&gt;&lt;p&gt;要把发束回复至原来的发型，方法是把目前节点位置向该节点的引导动位置(guided position，即发型中设置的位置)施以归还力(restitution force)。经过实验测试，发觉可以把接近发根的节点设定较大的归还力，越接近发梢则归还力越弱。我简单地使用一个衰变的关系&lt;span class="math"&gt;r_i=r_0^{ic}&lt;/span&gt;，当中&lt;span class="math"&gt;r_0&lt;/span&gt;为发根的归还力，&lt;span class="math"&gt;i&lt;/span&gt;为自发根起计的节点索引，&lt;span class="math"&gt;c&lt;/span&gt;是衰变的速度。&lt;/p&gt;&lt;p&gt;在碰撞方面，只是从一个球体扩展至多个球体，模拟更准确的头形，以及对脖子、胸、肩、手臂的碰撞。Verlet方法使碰撞计算简单之时又真实，可表现出发丝在肩上顺滑地流动。&lt;/p&gt;&lt;p&gt;研发通常都不是一帆风顺的。当在程序中加入了移动模型的操控后，发现在少量迭代的情况下，发丝像弹簧般弹来弹去，换句话说，长度约束的收敛不够快。此问题是技术关键，当时没找到好的现成办法，苦恼多时。我试过不同的方法，例如把约束的执行乱序化，或是以不同分组方法进行长度约束，但效果都不如理想。最后灵机一触，想到既然碰撞这么简单、效果又好，可以想象每个节点都被限制在一个球体之内，球体中心为发根，半径则是发根至该节点的止动长度之和，如图7a所示。&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image03.png"/&gt;&lt;p&gt;图7(a): 每个粒子限制在一个球体之内 (b): 不能满足长度约束的情形，但仍然保持每个节点和发根的直线距离&lt;/p&gt;&lt;p&gt;此法能有效地避免头发超出半径范围，但不能控制如图7(b)的情况。从实验得知，后者其实不太显眼，只要不做成弹簧伸缩的感觉，视觉上很难察觉出问题。&lt;p&gt;&lt;strong&gt;效能测试&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;至2009年8月30日(星期日)，在Windows上实现的基本头发模拟和渲染实验已经完成，代码亦使用了XNA Math做SIMD矢量优化。很兴奋地把屏幕截图发给团队成员，分享成果。&lt;/p&gt;&lt;p&gt;在往后的技术会议基本上满意这个研发进度及结果，但还有一点忧虑，就是此技术在游戏机平台的性能。&lt;/p&gt;&lt;p&gt;因此，之后赶紧把代码移植至Xbox 360上测试(图8)。实验证明此技术的性能是可以的，瓶颈主要出现在特写镜头时的GPU填充率。&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image09.jpg"/&gt;&lt;p&gt;图8: Xbox360上的测试截屏&lt;/p&gt;&lt;p&gt;除了效能，另一让我纠结的问题是发丝排序。使用半透明alpha混合(alpha blending)的渲染效果，比alpha测试(alpha testing)好得多。因为前者能表现出很柔滑的感觉，而后者则较粗糙。然而，实验里的排序是基于一个启发(heuristic)──按每根发束的第i个节点在观察空间(view space)的深度进行排序。当设i=0就是用发根来排序，以实验测试找出各种情况下较好的值。但以发束为单位的排序不能完美解决问题，只是一个折衷方案。&lt;/p&gt;&lt;p&gt;当时还考虑过一些次序无关透明(order independent transparency, OIT)技术，例如实现过screen-door transparency，但效果都不如理想。或许用硬件的OIT方案，如alpha-to-coverage(需要较高的MSAA)、Direct3D 11中在渲染目标的缓冲区做OIT。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;整合至Unreal引擎&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;效能测试大约花了一星期，之后就开始把此头发方案整合至Unreal引擎。&lt;/p&gt;&lt;p&gt;这个整合比我预期中困难许多，主要原因可能是我不熟悉Unreal渲染架构，而Unreal也缺乏这类文档，只能靠阅读原代码。由于所需的顶点信息和Unreal内置的不一样，所以要加入新的顶点格式、顶点着色器代码等。为了渲染第一个三角形，记得大概要增加、修改数十个源代码文件，并且由于变换矩阵的一些特别设置，以及多线程的问题，最终花了一星期时间才能渲染一个用自定顶点格式的三角形。&lt;/p&gt;&lt;p&gt;然而，之后的代码整合就变得容易，很快就可以为爱丽丝加上飘逸的长发。在战斗中猛烈摇头的效果，尤其令我鼓舞。之后再配合Unreal的工具，设置碰撞用的球体，最基本的整合就完成了。接着是要移植代码至Xbox360和PS3，我的同事Jake帮忙做了PS3的部分，把代码改写成SPU的方式。而专门做图形方面的杨同学也帮忙解决了不少问题，例如是景深效果(depth of field, DoF)时的问题(因为半透明的关系，头发本来并没有写入深度)。接下来一年的时间，也不断作出调整和新功能，例如爱丽丝缩小、跳跃、滑翔和风吹效果等等。&lt;/p&gt;&lt;p&gt;有一次EA来访，我们展示这个新技术时，对我印象最深刻的评语是：&lt;p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;不如让爱丽丝做洗发水广告吧！&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;新的需求&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;因为头发系统研发的成果，便希望爱丽丝的衣饰都能舍弃手工动画，采用程序式的动画。之前我们尝试过用现成的物理引擎的布料仿真，但效果不如理想。主要原因是，爱丽丝的裙子并不是轻薄柔滑的，而是里面有许多层布，形成一个较固定的形状。而且，设定中爱丽丝往下滑翔时，希望把裙子变成像降落伞的形状(图9)。&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image05.jpg"/&gt;&lt;p&gt;图9: 爱丽丝滑翔时，裙子变成像降落伞 (游戏截屏)&lt;/p&gt;&lt;p&gt;我采用和头发相同的弹簧质点系统代码，来做实验模拟爱丽丝的裙子。不同之处在于那些长度约束的拓朴(topology)，以及采用节点来驱动裙子骨架，用标准的蒙皮(skinning)方式渲染。&lt;/p&gt;&lt;p&gt;后来，这个衣服仿真系统被应用到其他衣饰，例如爱丽丝背上的蝴蝶结、其他装饰，甚至应用到最终boss的钢丝手。当中也花了许多时间做调整，例如采用类似连续式碰撞检测(continuous collision detection, CCD)来尽量避免膝盖穿出裙子。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;结语&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;爱丽丝的发丝只是《爱》中的小插曲，后续还做了不少用到及未用到的技术和游戏实现。但是，这一缕发丝将成为一段美好的回忆。&lt;/p&gt;&lt;p&gt;再次回想「为甚么其他市面上的游戏不用这种技术？」这个问题。也许，有人尝试过但失败了；也许，有人做了实验的效果或效能未达期望；也许，有人考虑到技术困难在设计时规避了长发；也许，有人因为答不出「为甚么其他市面上的游戏不用这种技术？」这个问题而从没尝试过。&lt;/p&gt;&lt;p&gt;这次经历告诉我，主动思考如何以技术改善项目，不要过早否定可能性，有想法时尽量动手尝试。&lt;/p&gt;&lt;p&gt;希望日后能继续从技术方面，开拓游戏中的应用。最后也希望大家喜爱、享受《爱》这个游戏。&lt;/p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_image07.jpg"/&gt;&lt;p&gt;&lt;strong&gt;参考&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;[1] Thomas Jakobsen, “&lt;a href=" http://www.gamasutra.com/gdcarchive/2001/jakobsent.doc"&gt;Advanced Character Physics&lt;/a&gt;”, Game Developer Conference, 2001.&lt;/li&gt;&lt;li&gt;[2] Hubert Nguyen, William Donnelly “&lt;a href="http.developer.nvidia.com/GPUGems2/gpugems2_chapter23.html"&gt;Hair Animation and Rendering in the Nalu Demo&lt;/a&gt;”, GPU Gems 2, Addison-Wesley, 2005.&lt;/li&gt;&lt;li&gt;[3] Thorsten Scheuermann, “&lt;a href="http://developer.amd.com/media/gpu_assets/Scheuermann_HairRendering.pdf"&gt;Hair Rendering and Shading&lt;/a&gt;”, Game Developer Conference, 2004.&lt;/li&gt;&lt;/ul&gt;&lt;img src="http://www.cnblogs.com/miloyip/aggbug/2080562.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/miloyip/archive/2011/06/14/alice_madness_returns_hair.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/09/17/behind_cplusplus.html</id><title type="text">C++强大背后</title><summary type="text">在31年前(1979年)，一名刚获得博士学位的研究员，为了开发一个软件项目发明了一门新编程语言，该研究员名为Bjarne Stroustrup，该门语言则命名为——C with classes，四年后改称为C++。C++是一门通用编程语言，支持多种编程范式，包括过程式、面向对象(object-oriented programming, OP)、泛型(generic programming, GP)...</summary><published>2010-09-16T16:56:00Z</published><updated>2010-09-16T16:56:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/09/17/behind_cplusplus.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/09/17/behind_cplusplus.html"/><content type="html">&lt;p&gt;在31年前(1979年)，一名刚获得博士学位的研究员，为了开发一个软件项目发明了一门新编程语言，该研究员名为&lt;a href="http://www2.research.att.com/~bs/"&gt;Bjarne Stroustrup&lt;/a&gt;，该门语言则命名为——C with classes，四年后改称为C++。C++是一门通用编程语言，支持多种编程范式，包括过程式、面向对象(object-oriented programming, OP)、泛型(generic programming, GP)，后来为泛型而设计的模版，被&lt;a href="http://www.erwin-unruh.de/meta.html"&gt;发现&lt;/a&gt;及&lt;a href="http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=AA022F3026015EF910CAEF5156901019?doi=10.1.1.14.3670&amp;rep=rep1&amp;type=pdf"&gt;证明是图灵完备的&lt;/a&gt;，因此使C++亦可支持&lt;a href="http://en.wikipedia.org/wiki/Template_metaprogramming"&gt;模版元编程范式(template metaprogramming, TMP)&lt;/a&gt;。C++继承了C的特色，既为高级语言，又含低级语言功能，可同时作为系统和应用编程语言。&lt;/p&gt;&lt;p&gt;C++广泛应用在不同领域，使用者&lt;a href="http://www2.research.att.com/~bs/bs_faq.html#number-of-C++-users"&gt;以数百万计&lt;/a&gt;。根据&lt;a href="http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html"&gt;近十年的调查&lt;/a&gt;，C++的流行程度约稳定排行第3位(于C/Java之后)。 C++经历长期的实践和演化，才成为今日的样貌。1998年，C++标准委员会排除万难，使C++成为ISO标准(俗称C++98)，当中含非常强大的&lt;a href="http://www.sgi.com/tech/stl/"&gt;标准模版库(standard template library, STL)&lt;/a&gt;。之后委员会在2005年提交了有关标准库的&lt;a href="http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1836.pdf"&gt;第一个技术报告(简称TR1)&lt;/a&gt;，并为下一个标准&lt;a href="http://en.wikipedia.org/wiki/C%2B%2B0x"&gt;C++0x&lt;/a&gt;而努力。可惜C++0x并不能在200x年完成，各界希望新标准能于2011年内出台。&lt;/p&gt; &lt;p&gt;流行的C++编译器中，微软Visual C++ 2010已实现&lt;a href="http://msdn.microsoft.com/en-us/library/dd465215.aspx"&gt;部分C++0x语法并加入TR1扩充库&lt;/a&gt;，而gcc对&lt;a href="http://gcc.gnu.org/projects/cxx0x.html"&gt;C++0x语法和库的支持&lt;/a&gt;比VC2010更多。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;应否选择C++&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;哪些程序适宜使用C++?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;C++并非万能丹，我按经验举出一些C++的适用时机。&lt;/p&gt;&lt;ul&gt;&lt;li&gt;C++适合构造程序中需求较稳定的部分，需求变化较大的部分可使用脚本语言；&lt;/li&gt;&lt;li&gt;程序须尽量发挥硬件的最高性能，且性能瓶颈在于CPU和内存；&lt;/li&gt;&lt;li&gt;程序须频繁地与操作系统或硬件沟通；&lt;/li&gt;&lt;li&gt;程序必须使用C++框架/库，如大部分游戏引擎(如Unreal/Source)及中间件(如Havok/FMOD)，虽然有些C++库提供其他语言的绑定，但通常原生的API性能最好、最新；&lt;/li&gt;&lt;li&gt;项目中某个目标平台只提供C++编译器的支持。&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;按应用领域来说，C++适用于开发服务器软件、桌面应用、游戏、实时系统、高性能计算、嵌入式系统等。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;使用C++还是C? &lt;/strong&gt;&lt;/p&gt;&lt;p&gt;C++和C的设计哲学并不一样，两者取舍不同，所以不同的程序员和软件项目会有不同选择，难以一概而论。与C++相比，C具备编译速度快、容易学习、显式描述程序细节、较少更新标准(后两者也可同时视为缺点)等优点。在语言层面上，C++包含绝大部分C语言的功能(例外之一，C++没有C99的&lt;a href="http://en.wikipedia.org/wiki/Variable-length_array"&gt;变长数组VLA&lt;/a&gt;)，且提供OOP和GP的特性。但其实用C也可实现OOP思想，亦可利用宏去实现某程度的GP，只不过C++的语法能较简洁、自动地实现OOP/GP。C++的&lt;a href="http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization"&gt;RAII&lt;/a&gt;(resource acquisition is initialization，资源获取就是初始化)特性比较独特，C/C#/Java没有相应功能。回顾历史，Stroustrup开发的早期C++编译器Cpre/&lt;a href="http://en.wikipedia.org/wiki/Cfront"&gt;Cfront&lt;/a&gt;是把C++源代码翻译为C，再用C编译器编译的。由此可知，C++编写的程序，都能用等效的C程序代替，但C++在语言层面上提供了OOP/GP语法、更严格的类型检查系统、大量额外的语言特性(如异常、&lt;a href="http://en.wikipedia.org/wiki/RTTI"&gt;RTTI&lt;/a&gt;等)，并且C++标准库也较丰富。有时候C++的语法可使程序更简洁，如运算符重载、隐式转换。但另一方面，C语言的API通常比C++简洁，能较容易供其他语言程序调用。因此，一些C++库会提供C的API封装，同时也可供C程序调用。相反，有时候也会把C的API封装成C++形式，以支持RAII和其他C++库整合等。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;为何C++性能可优于其他语言?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;相对运行于虚拟机语言(如C#/Java)，C/C++直接以静态形式把源程序编译为目标平台的机器码。一般而言，C/C++程序在编译及链接时可进行的优化最丰富，启动时的速度最快，运行时的额外内存开销最少。而C/C++相对动态语言(如Python/Lua)也减少了运行时的动态类型检测。此外，C/C++的运行行为是确定的，且不会有额外行为(例如C#/Java必然会初始化变量)，也不会有如垃圾收集(GC)而造成的不确定性延迟，而且C/C++的数据结构在内存中的布局也是确定的。有时C++的一些功能会使程序性能优于C，当中以内联和模版最为突出，这两项功能使C++标准库的sort()通常比C标准库的qsort()&lt;a href="http://en.wikipedia.org/wiki/Sort_(C%2B%2B)#Comparison_to_qsort.28.29"&gt;快多倍&lt;/a&gt;(C可用宏或人手编码去解决此问题)。另一方面，C/C++能直接映射机器码，之间没有另一层中间语言，因此可以做底层优化，例如使用&lt;a href="http://en.wikipedia.org/wiki/Intrinsic_function"&gt;内部(intrinsic)函数&lt;/a&gt;和嵌入汇编语言。然而，许多C++的性能优点并非免费午餐，代价包括较长的编译链接时间和较易出错，因而增加开发时间和成本，这点稍后补充。&lt;/p&gt;&lt;p&gt;我进行了一个简单全局渲染性能测试(512x512像素，每像素10000个采样)，C++ 1小时36分、Java 3小时18分、Python约18天、Ruby约351天。评测方式和其他语言的结果详见&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/07/07/languages_brawl_GI.html"&gt;博文&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;C++常见问题&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;C++源代码跨平台吗?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;C++有不错的跨平台能力，但由于直接映射硬件，因性能优化的关系，跨平台能力不及Java及多数脚本语言。然而，实践跨平台的C++软件还是可行的，但须注意以下问题：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;C++标准没有规定原始数据类型(如int)的大小，需要特定大小的类型时，可自订类型(如int32_t)，同时对任何类型使用sizeof()而不假设其大小；&lt;/li&gt;&lt;li&gt;字节序(byte order)按CPU有所不同，特别要注意二进制输入输出、reinterpret_cast法；&lt;/li&gt;&lt;li&gt;原始数据和结构类型的地址对齐有差异；&lt;/li&gt;&lt;li&gt;编译器提供的一些编译器或平台专用扩充指令；&lt;/li&gt;&lt;li&gt;避免作&lt;a href="http://en.wikipedia.org/wiki/Application_binary_interface"&gt;应用二进制接口(application binary interface, ABI)&lt;/a&gt;的假设，例如调用函数时参数的取值顺序在C/C++中没定义，在C++中也不可随便假设RTTI/虚表等实现方式。&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;总括而言，跨平台C++软件可在头文件中用宏检测编译器和平台，再用宏、typedef、自定平台相关实现等方法去实践跨平台，C++标准不会提供这类帮助。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;C++程序容易崩溃?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;和许多语言相比，C/C++提供不安全的功能以最优化性能，有可能造成崩溃。但要注意，很多运行时错误，如向空指针/引用解引用、数组越界、堆栈溢出等，其他语言也会报错或抛出异常，这些都是程序问题，而不是语言本身的问题。有些意见认为，出现这类运行时错误，应该尽量写入日志并立即崩溃，不该让程序继续运行，以免造成更大的影响(例如程序继续把内存中错误的数据覆写文件)。若要容错，可按业务把程序分割为多进程，像&lt;a href="http://dev.chromium.org/developers/design-documents/multi-process-architecture"&gt;Chrome&lt;/a&gt;或使用fork()的形式。然而，C++有许多机制可以减少错误，例如以&lt;a href="http://en.wikipedia.org/wiki/String_(C%2B%2B)"&gt;string&lt;/a&gt;代替C字符串；以&lt;a href="http://en.wikipedia.org/wiki/Vector_(C%2B%2B)"&gt;vector&lt;/a&gt;或&lt;a href="http://en.wikipedia.org/wiki/Array_(C%2B%2B)"&gt;array(TR1)&lt;/a&gt;代替原始数组(有些实现可在调试模式检测越界)；使用智能指针也能减少一些原始指针的问题。另外，我最常遇到的Bug，就是没有初始化成员变量，有时会导致崩溃，而且调试版和发行版的行为可能不同。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;C++要手动做内存管理?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;C++同时提供在堆栈上的自动局部变量，以及从自由存储(free store)分配的对象。对于后者，程序员需手动释放，或使用不同的容器和智能指针。 C++程序员经常进一步优化内存，自定义内存分配策略以提升效能，例如使用对象池、自定义的单向/双向堆栈区等。虽然C++0x还没加入GC功能，但也可以自行编写或使用现成库。此外，C/C++也可以直接使用操作系统提供的内存相关功能，例如内存映射文件、共享内存等。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;使用C++常要重造轮子?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;我曾参与的C++项目，都会重造不少标准库已提供的功能，此情况在其他语言中较少出现。我试图分析个中原因。首先，C++标准库相对很多语言来说是贫乏的，各开发者便会重复地制造自订库。从另一个角度看，C++标准库是用C++编写的(很多其他语言不用自身而是用C/C++去编写库)，在能力和性能上，自订库和标准库并无本质差别；另外，标准库为通用而设，对不同平台及多种使用需求作取舍，性能上有所影响，例如EA公司就曾发表自制的EASTL规格，描述游戏开发方面对STL的性能及功能需求的特点；此外，多个C++库一起使用，经常会因规范不同而引起冲突，又或功能重叠，所以项目可能须自行开发，或引入其他库的概念或实现(如&lt;a href="http://www.boost.org/"&gt;Boost&lt;/a&gt;/&lt;a href="http://en.wikipedia.org/wiki/C%2B%2B_Technical_Report_1"&gt;TR1&lt;/a&gt;/&lt;a href="http://loki-lib.sourceforge.net/"&gt;Loki&lt;/a&gt;)，改写以符合项目规范。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;C++编译速度很慢?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;错，是非常慢。我认为C++可能是实用程序语言中编译速度最慢的。此问题涉及C++沿用C的编译链接方式，又加入了复杂的类/泛型声明和内联机制，使编译时间倍增。在C++对编译方法改革之前(如&lt;a href="http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2073.pdf"&gt;module提案&lt;/a&gt;)，可使用以下技巧改善：第一，使用&lt;a href="http://en.wikipedia.org/wiki/Opaque_pointer"&gt;pimpl手法&lt;/a&gt;，因性能损耗应用于调用次数不多的类；第二，仅包含必要头文件，并尽量使用及提供前置声明版本的头文件(如iosfwd)；第三采用基于接口的设计，但须注意虚函数调用成本；第四，采用&lt;a href="http://buffered.io/2007/12/10/the-magic-of-unity-builds/"&gt;unity build&lt;/a&gt;，即把多个cpp文件结合在一个编译单元进行编译；第五，采用分布式生成系统如&lt;a href="http://www.xoreax.com/"&gt;IncrediBuild&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;C++缺乏什么功能?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;虽然C++已经非常复杂，但仍缺少很多常见功能。 C++0x作出了不少改善，例如语言方面加入Lambda函数、闭包、类型推导声明等，而库方面则加入正则表达式、采用哈希表的unordered_set/unordered_map、引用计数智能指针shared_ptr/weak_ptr等。但最值得留意的是C++0x引入多线程的语法和库功能，这是C++演进的一大步。然而，模组、GC、反射机制等功能虽有提案，却未加进C++0x。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;C++使用建议&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;为应用挑选特性集&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;我同意Stroustrup关于使用C++各种技术的回应：“你可以做，不意味着你必须这么做。(Just because you can do it, doesn't mean that you have to.)” C++充满丰富的特性，但同时带来不同问题，例如过分复杂、编译及运行性能的损耗。一般可考虑是否使用多重继承、异常、RTTI，并调节使用模版及模版元编程的程度。使用过分复杂的设计和功能，可能会令部分团队成员更难理解和维护。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;为团队建立编程规范&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;C++的编码自由度很高，容易编写风格迥异的代码，C++本身也没有定义一些标准规范。而且，C++的源文件物理构成，较许多语言复杂。因此，除了决定特性集，每个团队应建立一套编程规范，包括源文件格式(可使用文件模版)、花括号风格。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;尽量使用C++风格而非C风格&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;由于C++有对C兼容的包袱，一些功能可以使用C风格实现，但最好使用C++提供的新功能。最基本的是尽量以具名常量、内联函数和泛型取代宏，只把宏用在条件式编译及特殊情况。旧式的C要求局部变量声明在作用域开端，C++则无此限制，应把变量声明尽量置于邻近其使用的地方，for()的循环变量声明可置于for的括号内。 C++中能加强类型安全的功能应尽量使用，例如避免“万能”指针void *，而使用个别或泛型类型；用bool而非int表示布尔值；选用4种C++ cast关键字代替简单的强制转换。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;结合其他语言&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;如前文所述，C++并非适合所有应用情境，有时可以混合其他语言使用，包括用C++扩展其他语言，或在C++程序中嵌入脚本语言引擎。对于后者，除了使用各种脚本语言的专门API，还可使用&lt;a href="http://www.boost.org/doc/libs/1_42_0/libs/python/doc/index.html"&gt;Boost&lt;/a&gt;或&lt;a href="http://www.swig.org/"&gt;SWIG&lt;/a&gt;作整合。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;C++学习建议&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;C++缺点之一，是相对许多语言复杂，而且难学难精。许多人说学习C语言只需一本K&amp;R&lt;a href="http://book.douban.com/subject/1139336/"&gt;《C程序设计语言》&lt;/a&gt;即可，但C++书籍却是多不胜数。我是从C进入C++，皆是靠阅读自学。在此分享一点学习心得。个人认为，学习C++可分为4个层次：&lt;/p&gt;&lt;ul&gt;&lt;li&gt;第一层次，C++基础：挑选一本入门书籍，如&lt;a href="http://book.douban.com/subject/4262575/"&gt;《C++ Primer》&lt;/a&gt;、&lt;a href="http://book.douban.com/subject/2030264/"&gt;《C++大学教程》&lt;/a&gt;、或Stroustrup撰写的经典&lt;a href="http://book.douban.com/subject/1099889/"&gt;《C++程序设计语言》&lt;/a&gt;或他一年半前的新作&lt;a href="http://book.douban.com/subject/4875599/"&gt;《C++程序设计原理与实践》&lt;/a&gt;，而一般C++课程也止于此，另外&lt;a href="http://book.douban.com/subject/1110941/"&gt;《C++ 标准程序库》&lt;/a&gt;及&lt;a href="http://book.douban.com/subject/1868179/"&gt;《The C++ Standard Library Extensions》&lt;/a&gt;可供参考；&lt;/li&gt;&lt;li&gt;第二层次，正确高效地使用C++：此层次开始必须自修，阅读过《(&lt;a href="http://book.douban.com/subject/1241385/"&gt;More&lt;/a&gt;)&lt;a href="http://book.douban.com/subject/1842426/"&gt;Effective C++&lt;/a&gt;》、《(&lt;a href="http://book.douban.com/subject/1244943/"&gt;More&lt;/a&gt;)&lt;a href="http://book.douban.com/subject/1967356/"&gt;Exceptional C++&lt;/a&gt;》、&lt;a href="http://book.douban.com/subject/1792179/"&gt;《Effective STL》&lt;/a&gt;及&lt;a href="http://book.douban.com/subject/1480481/"&gt;《C++编程规范》&lt;/a&gt;等，才适宜踏入专业C++开发之路；&lt;/li&gt;&lt;li&gt;第三层次，深入了解C++：关于全局问题可读&lt;a href="http://book.douban.com/subject/1091086/"&gt;《深入探索C++对象模型》&lt;/a&gt;、&lt;a href="http://book.douban.com/subject/1470838/"&gt;《Imperfect C++》&lt;/a&gt;、&lt;a href="http://book.douban.com/subject/2970056/"&gt;《C++沉思录》&lt;/a&gt;、&lt;a href="http://book.douban.com/subject/1110934/"&gt;《STL源码剖析》&lt;/a&gt;，要挑战智商，可看关于模版及模版元编程的书籍如&lt;a href="http://book.douban.com/subject/2378124/"&gt;《C++ Templates》&lt;/a&gt;、&lt;a href="http://book.douban.com/subject/1119904/"&gt;《C++设计新思维》&lt;/a&gt;、&lt;a href="http://book.douban.com/subject/4136223/"&gt;《C++模版元编程》&lt;/a&gt;；&lt;/li&gt;&lt;li&gt;第四层次，研究C++：阅读&lt;a href="http://book.douban.com/subject/1096216/"&gt;《C++语言的设计和演化》&lt;/a&gt;、&lt;a href="http://book.douban.com/subject/4722718/"&gt;《编程的本质》&lt;/a&gt;(含STL设计背后的数学根基)、C++标准文件&lt;a href="http://openassist.googlecode.com/files/C%2B%2B%20Standard%20-%20ANSI%20ISO%20IEC%2014882%202003.pdf"&gt;《ISO/IEC 14882:2003》&lt;/a&gt;、&lt;a href="http://www.open-std.org/JTC1/SC22/WG21/"&gt;C++标准委员会&lt;/a&gt;的提案书和报告书&lt;/a&gt;、关于C++的学术文献。&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;由于我主要是应用C++，大约只停留于第二、三个层次。然而，C++只是软件开发的一环而已，单凭语言并不能应付业务和工程上的问题。建议读者不要强求几年内“彻底学会C++的知识”，到达第二层左右便从工作实战中汲取经验，有兴趣才慢慢继续学习更高层次的知识。虽然学习C++有难度，但也是相当有趣且有满足感的。&lt;/p&gt;&lt;p&gt;数十年来，C++虽有起伏，但她依靠其使用者而不断得到顽强的生命力，相信在我退休之前都不会与她分离，也希望更进一步了解她，与她走进未来。&lt;/p&gt;&lt;hr/&gt;&lt;p&gt;本文原于&lt;a href="http://www.programmer.com.cn/"&gt;《程序员》&lt;/a&gt;2010年8月刊揭载。&lt;/p&gt;&lt;img src="http://www.cnblogs.com/miloyip/aggbug/1828449.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/09/17/behind_cplusplus.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/09/03/idalloc_solution.html</id><title type="text">手机分配短讯id的面试题目(分析解答篇)</title><summary type="text">看过上回《厘清需求篇》，读者想到多少个解呢？本篇首先谈及一些基本分析，之后会按两种API设计(纯函数API和含状态的API)，分别描述多个解。虽然面试时或许不能进行实际测试，但本文还是给出PC上的效能测试结果。最后分析比较各解之优劣作为总结。 问题分析原来的问题是要从一个无序ids数组里分配一个id。我们可以用数学方式去更清楚地说明这个问题。设m = 256 为所有id的个数，集合U = \lef...</summary><published>2010-09-02T17:37:00Z</published><updated>2010-09-02T17:37:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/09/03/idalloc_solution.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/09/03/idalloc_solution.html"/><content type="html">&lt;p&gt;看过上回&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/08/31/idalloc_clarify.html"&gt;《厘清需求篇》&lt;/a&gt;，读者想到多少个解呢？本篇首先谈及一些基本分析，之后会按两种API设计(纯函数API和含状态的API)，分别描述多个解。虽然面试时或许不能进行实际测试，但本文还是给出PC上的效能测试结果。最后分析比较各解之优劣作为总结。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;问题分析&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;原来的问题是要从一个无序ids数组里分配一个id。我们可以用数学方式去更清楚地说明这个问题。&lt;/p&gt;&lt;p&gt;设m = 256 为所有id的个数，集合&lt;span class="math"&gt;U = \left\{ 0, 1, ..., m-1 \right\}&lt;/span&gt;为所有id的集合。那么，给定一个已分配id的集合&lt;span class="math"&gt;A\subset U&lt;/span&gt;，&lt;span class="math"&gt;A = \left\{ a_0, a_1, ..., a_{n-1} \right\}&lt;/span&gt;(即参数ids)，本题目可表示为，求一个&lt;span class="math"&gt;x&lt;/span&gt;(即传回的id)，符合条件: &lt;/p&gt;&lt;div class="math"&gt;x \in U - A&lt;/div&gt;&lt;p&gt;减号是补集的意思，即x属于U但不属于A。上回的对答已确定&lt;span class="math"&gt;U - A\ne \oslash&lt;/span&gt; ，即&lt;span class="math"&gt;x&lt;/span&gt;必然存在。此外，这个条件又可以写成: &lt;/p&gt;&lt;div class="math"&gt;x \in U \wedge x \notin A&lt;/div&gt;&lt;p&gt;以上两种表达式可说明此问题的两种解法，一种编程方向是查找U集里有没有不属于A的id，而另种是计算A的补集再取出其中一个id。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;纯函数API的解&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;实现程序之前，如果可以，应先写测试函数。笔者认为，若面试者在情况容许下，也可在解答题目之前，写下测试程序。如果有多个面试者能同样解题，或许同时写下测试程序的面试者能脱颖而出。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;测试函数&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;为了简单起见，笔者使用了assert()来检测正确性，只于Debug版本有效。而Release版本则用来测试效能。&lt;/p&gt;&lt;p&gt;由于U集合的子集合很多，&lt;span class="math"&gt;\left| P(U) \right| = 2^m=2^{256}\approx 10^{76}&lt;/span&gt; ，不可能穷举所有可能集合。所以，只能够举出随机的集合以作测试。 &lt;/p&gt;&lt;p&gt;以下是一些常数(宏)及类型声明，TEST_COUNT是测试次数，而TEST_REPEATCOUNT是为了测试效能时，重覆测试的次数(即Release版本会调用测试函数一百万次): &lt;/p&gt;#define M 256 // ID的数目，且所有ID在[0, M)的区间内&lt;br/&gt;&lt;br/&gt;#define TEST_COUNT 10000 &lt;br/&gt;#ifdef NDEBUG &lt;br/&gt;#define TEST_REPEATCOUNT 100 &lt;br/&gt;#else &lt;br/&gt;#define TEST_REPEATCOUNT 1 &lt;br/&gt;#endif &lt;br/&gt;&lt;br/&gt;typedef unsigned char byte; &lt;br/&gt;typedef unsigned long dword; &lt;br/&gt;&lt;br/&gt;typedef byte (*idalloc_func)(byte*, size_t); &lt;br/&gt;&lt;p&gt;首先，写一个帮助函数测试某id是否在ids集合之内(不熟C++的读者可参考C版本):&lt;/p&gt;// 检测ids里是否含id (C++ 版本) &lt;br/&gt;inline bool contain(byte* ids, size_t n, byte id) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;&lt;br/&gt;return find(ids, ids + n, id) != ids + n; &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;// 检测ids里是否含id (C 版本) &lt;br/&gt;inline bool contain(byte* ids, size_t n, byte id) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;&lt;br/&gt;for (size_t i = 0; i &lt; n; i++) &lt;br/&gt;if (ids[i] == id) &lt;br/&gt;return true; &lt;br/&gt;return false; &lt;br/&gt;}&lt;br/&gt;&lt;p&gt;笔者首先写了一个测试平均情况的测试平台函数:&lt;/p&gt;// 测试平均情况 &lt;br/&gt;void test_average(idalloc_func idalloc) { &lt;br/&gt;assert(idalloc != NULL); &lt;br/&gt;&lt;br/&gt;byte ids[M]; &lt;br/&gt;&lt;br/&gt;for (size_t i = 0 ; i &lt; M; i++) &lt;br/&gt;ids[i] = (byte)i; &lt;br/&gt;&lt;br/&gt;srand(0); // 使每次测试的伪随机数相同 &lt;br/&gt;&lt;br/&gt;size_t n = 0; &lt;br/&gt;for (int test = 0; test &lt; TEST_COUNT; test++) { &lt;br/&gt;random_shuffle(ids, ids + M); // 把整个数组洗牌 &lt;br/&gt;&lt;br/&gt;for (int repeat = 0; repeat &lt; TEST_REPEATCOUNT; repeat++) { &lt;br/&gt;byte id = idalloc(ids, n); &lt;br/&gt;(void)id; &lt;br/&gt;assert(!contain(ids, n, id)); &lt;br/&gt;&lt;br/&gt;// 测试是否最小的id &lt;br/&gt;for (size_t i = 0; i &lt; id; i++) &lt;br/&gt;assert(contain(ids, n, (byte)i)); &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;n = (n + 1) % M; &lt;br/&gt;} &lt;br/&gt;} &lt;br/&gt;&lt;p&gt;简单解释。首先，把ids数组填入所有id值。利用random_shuffle()把把整个ids数组洗牌，而n则是在[0, M)区间里循环递增。&lt;/p&gt;&lt;p&gt;由于笔者给出的解，都能传回最小的id，所以也会测试这条件。而最坏情况，就是ids含无序的{0, 1, ... M - 2}，分配到的id为M-1，笔者也为此编了一个最坏情况的效能测试函数。 &lt;/p&gt;// 测试最坏情况(ids为无序的[0, M - 2], 结果必然是id = M - 1) &lt;br/&gt;void test_worst(idalloc_func idalloc) { &lt;br/&gt;assert(idalloc != NULL); &lt;br/&gt;&lt;br/&gt;const size_t n = M - 1; &lt;br/&gt;byte ids[n]; &lt;br/&gt;&lt;br/&gt;srand(0); // 使每次测试的伪随机数相同 &lt;br/&gt;&lt;br/&gt;for (size_t i = 0 ; i &lt; n; i++) &lt;br/&gt;ids[i] = (byte)i; &lt;br/&gt;&lt;br/&gt;for (int test = 0; test &lt; TEST_COUNT; test++) { &lt;br/&gt;random_shuffle(ids, ids + n); &lt;br/&gt;&lt;br/&gt;for (int repeat = 0; repeat &lt; TEST_REPEATCOUNT; repeat++) { &lt;br/&gt;byte id = idalloc(ids, n); &lt;br/&gt;(void)id; &lt;br/&gt;assert(id == M - 1); &lt;br/&gt;} &lt;br/&gt;} &lt;br/&gt;} &lt;br/&gt;&lt;p&gt;&lt;strong&gt;线性查找&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;最简单的想法，可能是遍历所整个U集合(即0至M-1)，并使用contain()函数检测该id是否不包含在ids数组里。&lt;/p&gt; // 线性查找 (总是传回最小id) &lt;br/&gt;// 时间复杂度: O(n^2) &lt;br/&gt;// 临时内存大小: 0 字节 &lt;br/&gt;// 注: 因为n &lt; M，无论ids内的值为何(甚至有重复元素)，必然可找到一个id，所以id的for不用边界检查。 &lt;br/&gt;byte linear_search(byte* ids, size_t n) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;assert(n &lt; M); &lt;br/&gt;&lt;br/&gt;// 逐个id检查是否存在于[ids, ids + n) &lt;br/&gt;for (byte id = 0; ; id++) &lt;br/&gt;if (!contain(ids, n, id)) &lt;br/&gt;return id; &lt;br/&gt;} &lt;br/&gt;&lt;p&gt;&lt;strong&gt;二分查找&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;网友Doyle在TL里提出了用二分查找的主意。笔者实现了两种形式，以下这个是不需额外内存。原理是把U集合分割为两个各占一半的区间，分别数算两个区间内的已分配元素数目，若元素数目少于区间大小，即代表该区间内有未分配的id。再继续分割该区间，直至区间内都是可分配的id(即找到的元素是零)。&lt;/p&gt;// 数ids内有多少个id在[min, max)的区间内 &lt;br/&gt;inline size_t count_interval(byte* ids, size_t n, size_t min, size_t max) { &lt;br/&gt;size_t count = 0; &lt;br/&gt;&lt;br/&gt;for (size_t i = 0; i &lt; n; i++) &lt;br/&gt;if (ids[i] &gt;= min &amp;&amp; ids[i] &lt; max) &lt;br/&gt;count++; &lt;br/&gt;&lt;br/&gt;return count; &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;// 二分查找 (总是传回最小id) &lt;br/&gt;// 时间复杂度: O(n lg n) &lt;br/&gt;// 临时内存大小: 0 字节 &lt;br/&gt;byte binary_search(byte* ids, size_t n) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;assert(n &lt; M); &lt;br/&gt;&lt;br/&gt;size_t l = 0, r = M; &lt;br/&gt;&lt;br/&gt;for(;;) { &lt;br/&gt;size_t c = (l + r) / 2; // 把id范围从[l, r)分割为[l, c), [c, r)两个区间 &lt;br/&gt;size_t count; &lt;br/&gt;&lt;br/&gt;// 以下的条件测试次序保证了传回最小id &lt;br/&gt;if ((count = count_interval(ids, n, l, c)) &lt; c - l) { &lt;br/&gt;if (count == 0) &lt;br/&gt;return (byte)l; &lt;br/&gt;r = c; &lt;br/&gt;} &lt;br/&gt;else if ((count = count_interval(ids, n, c, r)) &lt; r - c) { &lt;br/&gt;if (count == 0) &lt;br/&gt;return (byte)c; &lt;br/&gt;l = c; &lt;br/&gt;} &lt;br/&gt;else &lt;br/&gt;assert(false); // 因为n &lt; M，不可能找不到任何id &lt;br/&gt;} &lt;br/&gt;}&lt;p&gt;这算法在最坏情况比线性查找快，但平均情况下却不一定。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;排序&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;以上两个解，都是查找的方式，毋需改动数据。相反，另一类解用的算法需改动ids数组内的元素，或是把ids复制到另一个临时数组里进行更改型的算法。 &lt;/p&gt;&lt;p&gt;最简单的算法，是把无序的ids排序。之后就可以从头开始扫描未分配的id。 &lt;/p&gt;// 排序 (总是传回最小id) &lt;br/&gt;// 时间复杂度: O(n lg n) &lt;br/&gt;// 临时内存大小: M 字节(如果可改变ids则是0) &lt;br/&gt;byte sort_stl(byte* ids, size_t n) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;assert(n &lt; M); &lt;br/&gt;&lt;br/&gt;byte buffer[M]; &lt;br/&gt;memcpy(buffer, ids, n); &lt;br/&gt;&lt;br/&gt;sort(buffer, buffer + n); // 平均 O(n lg n) &lt;br/&gt;&lt;br/&gt;for (size_t i = 0; i &lt; n; i++) &lt;br/&gt;if (buffer[i] != i) &lt;br/&gt;return (byte)i; &lt;br/&gt;&lt;br/&gt;return (byte)n; &lt;br/&gt;}&lt;p&gt;但读者可能会想到，把整个数组排序可能会做了很多无用工。而且，快速排序(quicksort)的最坏时间复杂度是O(n^2)。因此，就有了下一个解。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;堆&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;笔者想到的另一个解是使用堆(heap)数据结构。堆可保证第一个元素是最小的元素(通常是最大的，但这题目里我们希望取得最小的)，而每次弹出这个元素，取出第二小的元素只需要O(lg n)的时间。 sort_stl()需要完整排序，而使用堆则是逐步进行的，中途找到没用到的id就可以停下来，所以平均来说会省下很多时间。 &lt;/p&gt;// 堆 (总是传回最小id) &lt;br/&gt;// 时间复杂度: O(n lg n) &lt;br/&gt;// 临时内存大小: M 字节(如果可改变ids则是0) &lt;br/&gt;byte heap_stl(byte* ids, size_t n) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;assert(n &lt; M); &lt;br/&gt;&lt;br/&gt;byte buffer[M]; &lt;br/&gt;memcpy(buffer, ids, n); &lt;br/&gt;&lt;br/&gt;byte* end = buffer + n; &lt;br/&gt;make_heap(buffer, end, greater&lt;byte&gt;()); // O(n) &lt;br/&gt;&lt;br/&gt;for (byte id = 0; buffer != end; id++, end--) { &lt;br/&gt;if (buffer[0] != id) &lt;br/&gt;return id; &lt;br/&gt;pop_heap(buffer, end, greater&lt;byte&gt;()); // O(lg n) &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;return (byte)n; &lt;br/&gt;}&lt;p&gt;最坏的情况，是要把最小的M-1个元素最弹出，才能求得id=M-1。这情况其实等价于堆排序(heapsort)。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;剖分&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;另一个方法和二分查找相似，就是把数组剖分(partition)为两部分，这应该是Doyle提出的原意。原理是，设一个中间c=M/2，用它把无序ids集合剖分为两个无序集合，前一个集合的元素小于c，后一个的元素大于或等于c。那么，应该有一个集合的元素数量少于id区间的大小，再把该集合继续剖分，直至变成空集。 &lt;/p&gt;// 剖分 (总是传回最小id) &lt;br/&gt;// 时间复杂度: O(n) &lt;br/&gt;// 临时内存大小: M 字节(如果可改变ids则是0) &lt;br/&gt;byte partition_stl(byte* ids, size_t n) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;assert(n &lt; M); &lt;br/&gt;&lt;br/&gt;byte buffer[M]; &lt;br/&gt;memcpy(buffer, ids, n); &lt;br/&gt;&lt;br/&gt;byte *first = buffer, *last = buffer + n; &lt;br/&gt;size_t l = 0, r = M; &lt;br/&gt;&lt;br/&gt;for (;;) { &lt;br/&gt;size_t c = (l + r) / 2; &lt;br/&gt;byte* middle = partition(first, last, bind2nd(less&lt;byte&gt;(), c)); // O(n) &lt;br/&gt;// 后置条件: l &lt;= [first, middle)内元素 &lt; c 及 c &lt;= [middle, last)内元素 &lt; r&lt;br/&gt;&lt;br/&gt;// 以下的条件测试次序保证了传回最小id &lt;br/&gt;if (first == middle) &lt;br/&gt;return (byte)l; &lt;br/&gt;else if ((size_t)distance(first, middle) &lt; c - l) { &lt;br/&gt;last = middle; &lt;br/&gt;r = c; &lt;br/&gt;} &lt;br/&gt;else if (middle == last) &lt;br/&gt;return (byte)c; &lt;br/&gt;else if ((size_t)distance(middle, last) &lt; r - c) { &lt;br/&gt;first = middle; &lt;br/&gt;l = c; &lt;br/&gt;} &lt;br/&gt;else &lt;br/&gt;assert(false); &lt;br/&gt;} &lt;br/&gt;}&lt;p&gt;此算法的妙处在于，时间复杂度仅为O(n)！为什么呢？因为partition()的时间复杂度是O(n)，而此算法中每个迭代需处理的元素是n, n/2, n/4, ...，把这个几何数列求和，得出2n，所以此算法为线性时间。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;布尔集合&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;也许，最多网友都想到的解，就是把ids无序数组变换为另一个集合表示方式，能更快地测试A是否不含某id。这种表达方式是使用一个布尔数组(boolean array)，储存某id是否在ids无序数组里。用数学方式，可以称这个数组为一个函数&lt;span class="math"&gt;f:U\rightarrow \{0,1\}&lt;/span&gt;: &lt;/p&gt;&lt;div class="math"&gt;f(i)=\left\{\begin{matrix} 1 &amp;amp; \text{if } i \in A\\ 0 &amp;amp; \text{if } i \notin A \end{matrix}\right.&lt;/div&gt;&lt;p&gt;建立这个数组之后，再扫描一次，找出没使用到的id。 &lt;/p&gt;// 布尔集合 (总是传回最小id) &lt;br/&gt;// 时间复杂度: O(n) &lt;br/&gt;// 临时内存大小: M 字节 &lt;br/&gt;byte boolset(byte* ids, size_t n) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;assert(n &lt; M); &lt;br/&gt;&lt;br/&gt;bool id_used[M] = { false }; &lt;br/&gt;&lt;br/&gt;// 填充 id_used &lt;br/&gt;for (size_t i = 0; i &lt; n; i++) { &lt;br/&gt;assert(!id_used[ids[i]]); // 此处断言失败代表ids有重复元素 &lt;br/&gt;id_used[ids[i]] = true; &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;// 扫描id_used去找出最小未用id &lt;br/&gt;for (size_t i = 0; i &lt; M; i++) &lt;br/&gt;if (!id_used[i]) &lt;br/&gt;return (byte)i; &lt;br/&gt;&lt;br/&gt;assert(false); &lt;br/&gt;return 0; &lt;br/&gt;}&lt;p&gt;这类解法在纯函数API中是最快的，但必须使用额外内存。&lt;/p&gt; &lt;p&gt;&lt;strong&gt;位集合&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;上述的解，每个数组元素由于只需储存1个位(bit)，可以把8个布尔值置于字节里，减少额外内存。这种集合称为位集合(bit set)或位图(bitmap)。此外，在32位CPU上，可一次检查32位是否全0或全1，这可是一个优化。这次，我们直接储存补集A，即是那些分配了的id会把位设为0，那么在扫描时就不需做一个not位元运算。 &lt;/p&gt;// 位集合 (总是传回最小id) &lt;br/&gt;// 时间复杂度: O(n) &lt;br/&gt;// 临时内存大小: floor((M + 31) / 32) * 4 字节 &lt;br/&gt;byte bitset_standard(byte* ids, size_t n) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;assert(n &lt; M); &lt;br/&gt;&lt;br/&gt;const size_t dword_count = (M + 31) / 32; &lt;br/&gt;dword id_unused_bits[dword_count]; &lt;br/&gt;&lt;br/&gt;// 开始时设全部id为未用(即设位为1) &lt;br/&gt;memset(id_unused_bits, ~0, sizeof(id_unused_bits)); &lt;br/&gt;&lt;br/&gt;// 填充id_unused_bits (ids内的位清为0) &lt;br/&gt;for (size_t i = 0; i &lt; n; i++) { &lt;br/&gt;size_t index = ids[i] / 32; &lt;br/&gt;dword bitIndex = ids[i] % 32; &lt;br/&gt;assert(id_unused_bits[index] &amp; (1 &lt;&lt; bitIndex)); &lt;br/&gt;id_unused_bits[index] ^= (1 &lt;&lt; bitIndex); &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;// 扫描id_unused_bits，找出最小未用id &lt;br/&gt;for (size_t index = 0; index &lt; dword_count; index++) { &lt;br/&gt;if (dword bits = id_unused_bits[index]) { &lt;br/&gt;for (dword bitIndex = 0; bitIndex &lt; 32; bitIndex++) &lt;br/&gt;if (bits &amp; (1 &lt;&lt; bitIndex)) { &lt;br/&gt;dword id = index * 32 + bitIndex; &lt;br/&gt;assert(id &lt; M); &lt;br/&gt;return (byte)id; &lt;br/&gt;} &lt;br/&gt;} &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;assert(false); &lt;br/&gt;return 0; &lt;br/&gt;}&lt;p&gt;在某些CPU上，还会支持一个汇编指令bsf(bit scan forward)，可扫描一个32位值里，第一个为1的位索引(从LSB至MSB)。这正正是我们想要的。以下使用了Visual C++的内部函数(intrinsic)去使用此指令。 &lt;/p&gt;// 位集合(使用内部函数(intrinsic)) &lt;br/&gt;byte bitset_intrinsic(byte* ids, size_t n) { &lt;br/&gt;assert(ids != NULL); &lt;br/&gt;assert(n &lt; M); &lt;br/&gt;&lt;br/&gt;const size_t dword_count = (M + 31) / 32; &lt;br/&gt;dword id_unused_bits[dword_count]; &lt;br/&gt;&lt;br/&gt;// 开始时设全部id为未用(即设位为1) &lt;br/&gt;memset(id_unused_bits, ~0, sizeof(id_unused_bits)); &lt;br/&gt;&lt;br/&gt;// 填充id_unused_bits (ids内的位清为0) &lt;br/&gt;for (size_t i = 0; i &lt; n; i++) { &lt;br/&gt;size_t index = ids[i] / 32; &lt;br/&gt;dword bitIndex = ids[i] % 32; &lt;br/&gt;assert(id_unused_bits[index] &amp; (1 &lt;&lt; bitIndex)); &lt;br/&gt;id_unused_bits[index] ^= (1 &lt;&lt; bitIndex); &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;// 扫描id_unused_bits，找出最小未用id &lt;br/&gt;for (size_t index = 0; index &lt; dword_count; index++) { &lt;br/&gt;dword bitIndex; &lt;br/&gt;if (_BitScanForward(&amp;bitIndex, id_unused_bits[index])) { &lt;br/&gt;dword id = index * 32 + bitIndex; &lt;br/&gt;assert(id &lt; M); &lt;br/&gt;return (byte)id; &lt;br/&gt;} &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;assert(false); &lt;br/&gt;return 0; &lt;br/&gt;}&lt;p&gt;由于建立位集合所需的操作较布尔集合多，扫描的优化未必能弥补，所以位集合的主要好处是减低了临时内存的大小，为布尔集合的八分之一。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;含状态API的解&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;笔者对此题目提出另一种API的设计，就是保存一些状态: &lt;/p&gt;struct manager {&lt;br/&gt;    // 这里有一些状态变量(暂未决定)&lt;br/&gt;&lt;br/&gt;    byte alloc();&lt;br/&gt;    void dealloc(byte id);&lt;br/&gt;};&lt;p&gt;而在工程上，我们都可以估计到，传给纯函数API的ids数组，其内容实际上是以某方式储存在系统内的。若能改善它们储存的方式，就能加速id的分配过程。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;测试函数&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;同样，笔者为此API设计编写了测试函数。纯函数API的测试函数每次都是独立调用，但本测试的对象是有状态的。因此，此函数设计为随机分配为释放id(各概率约为50%)。&lt;/p&gt;template &amp;lt;class T&amp;gt;&lt;br/&gt;void test_manager() { &lt;br/&gt;T manager; &lt;br/&gt;bool id_allocated[M] = { false }; &lt;br/&gt;byte allocated_ids[M]; // allocated_ids[0]至allocated_ids[id_used_count - 1]储存无序的已分配id &lt;br/&gt;size_t allocated_id_count = 0; &lt;br/&gt;&lt;br/&gt;srand(0); // 使每次测试的伪随机数相同 &lt;br/&gt;&lt;br/&gt;for (int test = 0; test &lt; TEST_COUNT * TEST_REPEATCOUNT; test++) { &lt;br/&gt;// id集为空时必须进行分配，否则若id集未满时，有一半概率进行分配 &lt;br/&gt;if (allocated_id_count == 0 || (rand() &gt; RAND_MAX / 2 &amp;&amp; allocated_id_count &lt; M)) { &lt;br/&gt;byte id = manager.alloc(); &lt;br/&gt;assert(!id_allocated[id]); &lt;br/&gt;id_allocated[id] = true; &lt;br/&gt;allocated_ids[allocated_id_count++] = id; &lt;br/&gt;} &lt;br/&gt;else { &lt;br/&gt;// 其他情况，随机抽一个已分配id进行释放 &lt;br/&gt;assert(allocated_id_count &gt; 0); &lt;br/&gt;size_t index = rand() % allocated_id_count; &lt;br/&gt;byte id = allocated_ids[index]; &lt;br/&gt;assert(id_allocated[id]); &lt;br/&gt;manager.dealloc(id); &lt;br/&gt;id_allocated[id] = false; &lt;br/&gt;allocated_ids[index] = allocated_ids[--allocated_id_count]; // 用列表末的id取代已释放的id &lt;br/&gt;} &lt;br/&gt;} &lt;br/&gt;}&lt;p&gt;此外，这个测试函数不使用O(n)的contain()，所有操作都是O(1)的，测试的开销比较少。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;布尔集合(含状态)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;首先的解是把之前的布尔集合储存为状态，那么就不用每次重新建立该集合。 &lt;/p&gt;// 布尔集合 (总是传回最小id) &lt;br/&gt;// 分配的时间复杂度: O(n) &lt;br/&gt;// 释放的时间复杂度: O(1) &lt;br/&gt;// 状态所需内存: M 字节 &lt;br/&gt;struct boolset_manager { &lt;br/&gt;bool id_used[M]; &lt;br/&gt;&lt;br/&gt;boolset_manager() { &lt;br/&gt;for (size_t i = 0; i &lt; M; i++) &lt;br/&gt;id_used[i] = false; &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;byte alloc() { &lt;br/&gt;for (size_t i = 0; i &lt; M; i++) { &lt;br/&gt;if (!id_used[i]) { &lt;br/&gt;id_used[i] = true; &lt;br/&gt;return (byte)i; &lt;br/&gt;} &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;assert(0); &lt;br/&gt;return false; &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;void dealloc(byte id) { &lt;br/&gt;assert(id_used[id]); &lt;br/&gt;id_used[id] = false; &lt;br/&gt;} &lt;br/&gt;};&lt;p&gt;当然，亦可以用位集合减少内存。此处就不再详述了。 &lt;/p&gt;&lt;p&gt;这个解可以传回最小id，但若是没此需要，则有更快的解。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;栈&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;笔者认为，以下这个采用栈(stack)的解可能是本文最简单的一个解，同时，它的分配和释放时间复杂度皆是O(1)，而且系数应为最低，所以是本文最高效的解。&lt;/p&gt;&lt;p&gt;其原理很简单，把整个U集合压入栈，分配的时候弹出一个id，释放的时候压回去。&lt;/p&gt;// 栈 &lt;br/&gt;// 分配的时间复杂度: O(1) &lt;br/&gt;// 释放的时间复杂度: O(1) &lt;br/&gt;// 状态所需内存: M + 4 字节(使用short top会是M + 2 字节) &lt;br/&gt;struct stack_manager { &lt;br/&gt;byte ids[M]; &lt;br/&gt;size_t top; &lt;br/&gt;&lt;br/&gt;stack_manager() : top(M) { &lt;br/&gt;for (size_t i = 0; i &lt; M; i++) &lt;br/&gt;ids[i] = (byte)i; &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;byte alloc() { &lt;br/&gt;assert(top &gt; 0); &lt;br/&gt;return ids[--top]; // 弹出 &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;void dealloc(byte id) { &lt;br/&gt;assert(top &lt; M); &lt;br/&gt;ids[top++] = id; // 压入 &lt;br/&gt;} &lt;br/&gt;};&lt;p&gt;&lt;strong&gt;数组链表&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;而另一个接近高效的解是&lt;a href="http://home.cnblogs.com/Hybird3D/"&gt;Qiaojie&lt;/a&gt;提出的，把数组当作链表。这个解的分配和释放时间复杂度亦是O(1)。&lt;/p&gt;// 数组链表 (来自qiaojie) &lt;br/&gt;// 分配的时间复杂度: O(1) &lt;br/&gt;// 释放的时间复杂度: O(1) &lt;br/&gt;// 状态所需内存: M + 1 字节(若以freelist形式储存，则所需额外内存只是1字节) &lt;br/&gt;struct arraylinkedlist_manager { &lt;br/&gt;byte next[M]; &lt;br/&gt;byte head; &lt;br/&gt;&lt;br/&gt;arraylinkedlist_manager() : head(0) { &lt;br/&gt;// 填入完整的环 &lt;br/&gt;for(int i = 0; i &lt; M; ++i) &lt;br/&gt;next[i] = (byte)(i + 1); &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;byte alloc() { &lt;br/&gt;byte id = head; &lt;br/&gt;head = next[head]; &lt;br/&gt;&lt;br/&gt;// next[id]在这里已经不需要，可以用来放短讯或其他数据，这里放置0作为测试。实际上这步是可有可无的。 &lt;br/&gt;next[id] = 0; &lt;br/&gt;&lt;br/&gt;return id; &lt;br/&gt;} &lt;br/&gt;&lt;br/&gt;void dealloc(byte id) { &lt;br/&gt;next[id] = head; &lt;br/&gt;head = id; &lt;br/&gt;} &lt;br/&gt;};&lt;p&gt;这个解其实可称为free list，其优点是，next数组的元素若被分配，则本身可以储存其他数据。所以实际上会占用的额外内存只是1个字节！例如，可以把短讯的结构定义为: &lt;/p&gt;// 用于数组链表的freelist的结构例子 &lt;br/&gt;union sms { &lt;br/&gt;byte next; &lt;br/&gt;char message[160]; &lt;br/&gt;};&lt;p&gt;此数据结构其实最适合做对象池(object pool)。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;效能测试&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;以下是在i7 920、Windows 7、Visual C++ 2008 x86模式下的结果(单位为秒): &lt;/p&gt;  0.068476 test_average(dummy)&lt;br/&gt;  0.545491 test_average(linear_search)&lt;br/&gt;  3.030943 test_average(binary_search)&lt;br/&gt;  4.209131 test_average(sort_stl)&lt;br/&gt;  0.966749 test_average(heap_stl)&lt;br/&gt;  0.424917 test_average(partition_stl)&lt;br/&gt;  0.208690 test_average(boolset)&lt;br/&gt;  0.272523 test_average(bitset_standard)&lt;br/&gt;  0.271665 test_average(bitset_intrinsic)&lt;br/&gt;&lt;br/&gt;  0.068385 test_worst(dummy)&lt;br/&gt; 27.025864 test_worst(linear_search)&lt;br/&gt; 11.407150 test_worst(binary_search)&lt;br/&gt; 10.122118 test_worst(sort_stl)&lt;br/&gt; 13.912083 test_worst(heap_stl)&lt;br/&gt;  0.887030 test_worst(partition_stl)&lt;br/&gt;  0.498429 test_worst(boolset)&lt;br/&gt;  0.570213 test_worst(bitset_standard)&lt;br/&gt;  0.458865 test_worst(bitset_intrinsic)&lt;br/&gt;&lt;br/&gt;  0.042507 test_manager&lt;dummy_manager&gt;()&lt;br/&gt;  0.073745 test_manager&lt;boolset_manager&gt;()&lt;br/&gt;  0.042462 test_manager&lt;stack_manager&gt;()&lt;br/&gt;  0.042526 test_manager&lt;arraylinkedlist_manager&gt;()&lt;p&gt;当中，dummy/dummy_manager为没有实际计算的测试对象，用以量度测试本身的开销。读者比较时可把测试的时间减去相对的开销。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;讨论&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;以下的表简单总括各个解的特性: &lt;/p&gt;&lt;table border="1" bordercolor="#000000" cellpadding="3" cellspacing="0" width="100%"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td width="25%"&gt;解&lt;/td&gt;&lt;td width="25%"&gt;传回最小id &lt;/td&gt;&lt;td width="25%"&gt;平均时间复杂度&lt;/td&gt;&lt;td width="25%"&gt;额外内存(字节) &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;线性查找&lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n^2) &lt;/td&gt;&lt;td width="25%"&gt; 0 &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;二分查找&lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n lg n) &lt;/td&gt;&lt;td width="25%"&gt; 0 &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;排序&lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n lg n) (最坏O(n^2)) &lt;/td&gt;&lt;td width="25%"&gt; m 或0(可改动ids) &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;堆&lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n lg n) &lt;/td&gt;&lt;td width="25%"&gt; m 或0(可改动ids) &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;剖分&lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n) &lt;/td&gt;&lt;td width="25%"&gt; m 或0(可改动ids) &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;布尔集合&lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n) &lt;/td&gt;&lt;td width="25%"&gt; m &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;位集合&lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n)&lt;/td&gt;&lt;td width="25%"&gt; floor((m+31)/32)*4 &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;布尔集合(含状态) &lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n), O(1)&lt;/td&gt;&lt;td width="25%"&gt; m &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;位集合(含状态)&lt;/td&gt;&lt;td width="25%"&gt;是&lt;/td&gt;&lt;td width="25%"&gt; O(n), O(1)&lt;/td&gt;&lt;td width="25%"&gt; floor((m+31)/32)*4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;栈&lt;/td&gt;&lt;td width="25%"&gt;否&lt;/td&gt;&lt;td width="25%"&gt; O(1), O(1) &lt;/td&gt;&lt;td width="25%"&gt; m + 4 或m + 2 &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="25%"&gt;数组链表&lt;/td&gt;&lt;td width="25%"&gt;否&lt;/td&gt;&lt;td width="25%"&gt; O(1), O(1)&lt;/td&gt;&lt;td width="25%"&gt; m + 1 或1&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;原题目中的需求中谈及「……我要求你的程序尽量快，并少用内存。」但时间和空间是两个互相竞争的需求，通常难以同时满足。而在上文中，也把问题的API需求细分为: &lt;/p&gt;&lt;ol&gt;&lt;li&gt;纯函数API &lt;/li&gt;&lt;li&gt;可改动ids的函数API &lt;/li&gt;&lt;li&gt;含状态API &lt;/li&gt;&lt;/ol&gt;&lt;p&gt;本文列出的解并没有各方面都完美的解。例如，在无需额外内存的纯函数解里，二分查找在最坏情况下比线性查找的性能好，但平均来说却是相反。 &lt;/p&gt;&lt;p&gt;在变动数组(或复制数组)的纯函数解里，剖分在平均和最坏情况下，性能都比排序和堆好。剖分的优点是可以不占内存(当能改动ids时)，性能又比查找好。 &lt;/p&gt;&lt;p&gt;布尔集合和位集合的性能在纯函数解里是最好的，但必须占一些内存(虽然当m=256，位集合只需32字节)。 &lt;/p&gt;&lt;p&gt;含状态的解中，若需要传回最小id，可使用布尔集合和位集合。不然，可采用栈和数组链表。若在数组链表中以free list使用，当然是最理想，因为这只占1字节。但栈的性能会好一点点。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;结语&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;个人认为，本题是一个不错的面试题目，因为它并没有一个各方面都完美的解。这样，更可以考验应试者对算法的基础知识和编程能力。当然，笔者在编写这些程序也花了多个小时，在有限的面试时间中不太可能写这么多。但也可以用简单文字描述，或在交流中讲解一些思考方向。个人认为，理想的工程人员不但能解决问题，还会知道有其他解的存在，并去实验、分析、选择最适合某场合的解。&lt;/p&gt;&lt;p&gt;如果读者也想到其他的解，或对上述解的改善，希望不吝告之，本人也会尽量整理于此。 &lt;/p&gt;&lt;p&gt;&lt;a href="http://files.cnblogs.com/miloyip/idalloc20100903.zip"&gt;下载源文件&lt;/a&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/miloyip/aggbug/1816614.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/09/03/idalloc_solution.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/08/31/idalloc_clarify.html</id><title type="text">手机分配短讯id的面试题目(厘清需求篇)</title><summary type="text">前阵子，笔者在TopLanguage论坛里参与讨论了一个不错的面试题目，在此和大家分享，也当作个人的讨论总结。本文列出该问题，并模拟应试者向面试官的对话，以厘清问题需求。题目原文事缘Dbger发起的帖子中，liuxinyu举了一个面试题目，原文如下:有个老的手机短信程序，由于当时的手机CPU，内存都很烂。所以这个短信程序只能记住256条短信，多了就删了。 每个短信有个唯一的ID，在0到255之间。...</summary><published>2010-08-30T17:42:00Z</published><updated>2010-08-30T17:42:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/08/31/idalloc_clarify.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/08/31/idalloc_clarify.html"/><content type="html">&lt;p&gt;前阵子，笔者在TopLanguage论坛里参与讨论了一个不错的面试题目，在此和大家分享，也当作个人的讨论总结。本文列出该问题，并模拟应试者向面试官的对话，以厘清问题需求。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;题目原文&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;事缘&lt;a href="http://www.cnblogs.com/baiyanhuang/"&gt;Dbger&lt;/a&gt;发起的帖子中，&lt;a href="https://sites.google.com/site/algoxy"&gt;liuxinyu&lt;/a&gt;举了一个面试题目，原文如下:&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;&lt;p&gt;有个老的手机短信程序，由于当时的手机CPU，内存都很烂。所以这个短信程序只能记住256条短信，多了就删了。 &lt;/p&gt;&lt;p&gt;每个短信有个唯一的ID，在0到255之间。当然用户可能自己删短信，现在我们收到了一个新短信，请分配给它一个唯一的ID。说白了，就是请你写一个函数: &lt;/p&gt;byte function(byte* ids);&lt;p&gt;该函数接受一个ids数组，返回一个可用的ID，由于手机很破，我要求你的程序尽量快，并少用内存。注意：ids是无序的。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;strong&gt;厘清需求&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;笔者认为，面试时，并不一定要急着解答问题。面试(interview)并非考试(examination)，面试中双方可透过交谈去评估对方是否合适的雇主/雇员。若对面试题目有疑问，应尽量沟通厘清。这也反映实际软件开发时，程序员须具备足够的问题需求分析和沟通能力。笔者相信，大部分负责任的面试官，也喜见应试者提出切合实际的问题。&lt;/p&gt;&lt;p&gt;以下笔者尝试以对话形式，虚构一个厘清需求的情境。 (甲=面试官，乙=应试者)&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;&lt;p&gt;甲微笑着说：你对纸上的题目清楚么？&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;乙认真地看着试题，犹豫片刻。&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;&lt;p&gt;乙突然发问：在这个函数原型中，ids应该是一个非固定长度的数组，代表现时已分配的ID集合。那么该函数怎样可以得知ids数组的长度呢？我想到，由于0-255都是id的范围，似乎不可用简单的结束符，例如像null-terminated string那样用0去表示数组的结束。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;突然而來的长问题，轮到甲犹豫了，立刻把纸上题目快读一遍，发现似乎真是个问题。&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;&lt;p&gt;甲：嗯……这是个好问题，可能是出题同事的手误。这样吧，加上一个size_t n的参数，代表ids数组的有效元素数目。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;乙轻轻点头，觉得这个修正最为简单直觉。之后更放心把想到的问题都提出来。&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;&lt;p&gt;乙：想确认一点，n是否必然少于255，换句话说，该函数必然能正常输出一个id？&lt;/p&gt;&lt;p&gt;甲：是的。本题目不要求处理其他输入错误的情况，例如ids为NULL、ids里有重复值等。可假定输入数据都是有效的。&lt;/p&gt;&lt;p&gt;乙：想确认一点，传回的id只要不存在于ids数组便可，不需传回最小的id吧？&lt;/p&gt;&lt;p&gt;甲：没错，没这个需求。&lt;/p&gt;&lt;p&gt;乙有点不好意思地说：对于函数原型，我还有个问题……&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;甲心想不是还有手误吧。但甲还是带着微笑，示意继续问。&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;&lt;p&gt;乙：其实也是关于ids数组的。由于原型没写明为const byte *，想问此函数可否改动ids数组的内容？或者再进一步说，此函数是否&lt;a href="http://en.wikipedia.org/wiki/Pure_function"&gt;纯函数(pure function)&lt;/a&gt;，没有副作用(side effect)？&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;甲松了一口气，因为这不算是问题的缺陷。总算保住公司的面子。&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;&lt;p&gt;甲充满信心地说：对于ids数组的内容是否能改变，你可分为两个情况去考虑。而除了这个参数之外，此函数不会有其他副作用。 &lt;/p&gt;&lt;p&gt;乙：哦，明白了。此外，虽然我未考虑周详，但似乎要获得更高运行效率，便需要额外的存储状态，或许要设另一个接口才行。&lt;/p&gt;&lt;p&gt;甲喜形于色地回答：是的，也许可以透过空间换取时间。那么你对接口有提议么？&lt;/p&gt;&lt;p&gt;乙边说边写：我想，或许另一种接口的形式是这样的……&lt;/p&gt;&lt;br/&gt;struct manager {&lt;br/&gt;    // 这里有一些状态变量(暂未决定)&lt;br/&gt;&lt;br/&gt;    byte alloc();&lt;br/&gt;    void dealloc(byte id);&lt;br/&gt;};&lt;br/&gt;&lt;p&gt;甲：好。这个是比原题目更抽像的情况。若时间足够，你也可在答题时对这种接口进行补充。&lt;/p&gt;&lt;p&gt;乙：最后想问，该手机的CPU是32位的吧？&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;甲没有预先对此设限，就顺着回答了。&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;&lt;p&gt;甲：就当作是32位的吧。&lt;/p&gt;&lt;p&gt;乙：我暂时没想到其他问题了。&lt;/p&gt;&lt;p&gt;甲：那好，你可以开始在纸上作答，我们一小时后再讨论。&lt;/p&gt;&lt;p&gt;乙：谢谢您。我现在对题目的需求应有充分理解了。我会尽量想出多个解。&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;甲觉得乙有细心阅读题目，发问的内容和技巧也不错。乙也认为甲能耐心地聆听，并给与合理清晰的指示，甚至给与他一些弹性、一些发挥的机会。&lt;/p&gt;&lt;p&gt;最后一句中，乙提及的「多个解」也使甲留下印象和期待。因为很多时候，某个问题并不一定有最优解，能设想多个解，并分析比较每个解的优缺点，能体现程序员的基础能力。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;预告&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;解答篇应于本周五发表(&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/09/03/idalloc_solution.html"&gt;已如期发表&lt;/a&gt;)。有兴趣的读者不妨自己试试，多想几个不同的解。若不熟悉C/C++，也可用诸如C#、Java等语言作答。其实本题并不难，适合一般编程职位的面试。笔者将会提供的解，有些来自笔者，有些来自网友，也非什么神奇特别的解，只是作为讨论的总结，和大家分享。也许读者会想到更好的解呢。 &lt;/p&gt;&lt;img src="http://www.cnblogs.com/miloyip/aggbug/1813256.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/08/31/idalloc_clarify.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/07/07/languages_brawl_GI.html</id><title type="text">C++/C#/F#/Java/JS/Lua/Python/Ruby渲染比试</title><summary type="text">前篇博文把一个C++全局光照渲染器移植至C#，比较C++和C#之性能。……本人陆续移植了C++代码至Java、JavaScript、Lua、Python和Ruby，赵姐夫亦尝试了F#。本文提供源代码、测试结果、简单分析、以及个人体会。</summary><published>2010-07-06T16:20:00Z</published><updated>2010-07-06T16:20:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/07/07/languages_brawl_GI.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/07/07/languages_brawl_GI.html"/><content type="html">&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_smallpt_icc_openmp_10K_1116s.jpg" width=512" height="512" border="0"/&gt;&lt;br /&gt;&lt;small&gt;512x512像素，每像素10000个采样，Intel C++ OpenMP版本渲染时间为18分36秒。估计Ruby版本約需351天。&lt;/small&gt;&lt;/p&gt;&lt;style type="text/css"&gt;&lt;!--canvas { border-style:solid;border-width:1px;}--&gt;&lt;/style&gt; &lt;script type="text/javascript" src="http://www.google.com/jsapi"&gt;&lt;/script&gt;&lt;script type="text/javascript"&gt;    google.load("visualization", "1", { packages: ["corechart", 'table'] });    google.setOnLoadCallback(drawChart);    function drawChart() {        var data = new google.visualization.DataTable();        data.addColumn('string', 'Test');        data.addColumn('number', 'Time(sec)');        data.addRow(['IC++', 14.761]);        data.addRow(['VC++', 17.632]);        data.addRow(['IC++_OpenMP', 2.861]);        data.addRow(['VC++_OpenMP', 3.140]);        data.addRow(['C++/CLI_OpenMP', 5.147]);        data.addRow(['GCC', 19.500]);        data.addRow(['GCC_OpenMP', 3.359]);        data.addRow(['Java', 30.527]);        data.addRow(['C++/CLI', 27.634]);        data.addRow(['C#', 48.194]);        data.addRow(['C#_outref', 44.220]);        data.addRow(['F#', 47.172]);        data.addRow(['JsChrome', 237.88]);        data.addRow(['JsFirefox', 3588.778]);        data.addRow(['LuaJIT', 829.777]);        data.addRow(['Lua', 1227.656]);        data.addRow(['Python', 3920.556]);        data.addRow(['IronPython', 2921.573]);        data.addRow(['Jython', 6211.550]);        data.addRow(['Ruby', 77859.653]);        data.addColumn('number', 'Relative time');        data.sort([{ column: 1}]);        var formatter1 = new google.visualization.NumberFormat({ fractionDigits: 3 });        formatter1.format(data, 1);        var table = new google.visualization.Table(document.getElementById('table_div'));        for (var rowIndex = 0; rowIndex &lt; data.getNumberOfRows(); rowIndex++)            if (data.getValue(rowIndex, 0) == 'IC++') {            redrawTable(rowIndex);            table.setSelection([{ row: rowIndex, column: null}]);            break;        }        function redrawTable(selectedRow) {            // Compute relative time using the first row as basis            var basis = data.getValue(selectedRow, 1);            for (var rowIndex = 0; rowIndex &lt; data.getNumberOfRows(); rowIndex++)                data.setValue(rowIndex, 2, data.getValue(rowIndex, 1) / basis);            var formatter = new google.visualization.NumberFormat(            { suffix: 'x' });            formatter.format(data, 2); // Apply formatter to second column            table.draw(data, { width: 400 });        }        google.visualization.events.addListener(table, 'select',        function() {            var selection = table.getSelection();            if (selection.length &gt; 0) {                var item = selection[0];                if (item.row != null)                    redrawTable(item.row);            }        });        var view = new google.visualization.DataView(data)        view.setRows(view.getFilteredRows([{ column: 1, maxValue: 4000}]));        view.hideColumns([2]);        var chart = new google.visualization.BarChart(document.getElementById('chart_div'));        chart.draw(view, { width: 640, height: 480, title: 'Smallpt Rendering Time (&lt;4000sec)', legend: 'none',            hAxis: { title: 'Time (sec)' }        });        var chart2 = new google.visualization.BarChart(document.getElementById('chart2_div'));        var view2 = new google.visualization.DataView(data)        view2.setRows(view.getFilteredRows([{ column: 1, maxValue: 60}]));        view2.hideColumns([2]);        chart2.draw(view2, { width: 640, height: 480, title: 'Smallpt Rendering Time (&lt;60sec)', legend: 'none',            hAxis: { title: 'Time (sec)' }        });    }&lt;/script&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/06/23/cpp_vs_cs_GI.html"&gt;前篇博文&lt;/a&gt;把一个C++全局光照渲染器移植至C#，比较C++和C#之性能。刊出后，园友们不吝指出箇中问题，例如&lt;a href="http://www.cnblogs.com/aoaoblogs/"&gt;嗷嗷&lt;/a&gt;发现C++实现里的随机产生器采用了比较复杂的运行时函数，造成VisualC++和Intel C++的巨大差异；&lt;a href="http://blog.zhaojie.me/"&gt;赵姐夫&lt;/a&gt;发现C#版本用class竟然比struct快等等。修改这些问题后，园友&lt;a href="http://www.cnblogs.com/Hybird3D/"&gt;QiaoJie&lt;/a&gt;亦提出，可同时测试C++/CLI，检测其所产生的IL代码，在同样的.Net平台上运行，看看是否比C#优胜。很多网友也提供了宝贵意见，未能尽录，唯有以努力撰文作为答谢。本人陆续移植了C++代码至Java、JavaScript、Lua、Python和Ruby，赵姐夫亦尝试了F#。本文提供&lt;a  href="http://files.cnblogs.com/miloyip/smallpt20100706.zip"&gt;&lt;b&gt;测试源代码&lt;/b&gt;&lt;/a&gt;、测试结果、简单分析、以及个人体会。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;声明&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;首先，为免误会，再次重申，本测试有其局限，只能测试某一应用、某一实现的结果，并不能反映编程语言及其运行时的综合性能，亦无意尝试这样做。而实验环境也只限于某机器、某操作系统上，并不全面。而且，本测试只提供运行时间的结果，不考虑、不比较语言/平台间的技术性和非技术性优缺点，也没有测试运行期内存。世界上的软件应用林林总总，性能需求也完全不同，本测试只供参考。&lt;/p&gt;&lt;p&gt;由于本人第一次使用Python和Ruby，若代码有不当之处，敬请告之。当然也非常乐见其他意见。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;测试内容&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;本文测试程序为&lt;a href="http://kevinbeason.com/smallpt/"&gt;一个全局光照渲染器&lt;/a&gt;，是一个CPU运算密集的控制台应用程序(console application)，功能详见&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/06/23/cpp_vs_cs_GI.html"&gt;前文&lt;/a&gt;。在前文刊出后，本人进行了一点profiling、优化，并把代码重新格式化。本渲染器除了有大量数学运算，亦会产生大量临时对象，并进行极多的方法调用(非虚函数)。本测试有别于人工合成的测试(synthetictests，例如个别测试运算、字串操作、输入输出等)，是一个有实际用途的程序。&lt;/p&gt;&lt;p&gt;移植时尽量维持原代码的逻辑，主要采用面向对象范式。优化方面，不进行人手内联函数(inline function)，但优化了一些不必要的重复运算。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;测试配置&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;硬件: Intel Core i7 920@2.67Ghz(4 core, HyperThread), 12GB RAM&lt;/li&gt;&lt;li&gt;操作系统: Microsoft Windows 7 64-bit&lt;/li&gt;&lt;/ul&gt;&lt;table border="1" bordercolor="#000000" cellpadding="3" cellspacing="0" id="t-d:"width="100%"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td width="20%"&gt;测试名称&lt;/td&gt;&lt;td width="40%"&gt;编译器/解译器&lt;/td&gt;&lt;td width="40%"&gt;编译/运行选项&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;VC++&lt;/td&gt;&lt;td width="40%"&gt;Visual C++ 2008 (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;/Ox /Ob2 /Oi /Ot /GL /FD /MD /GS- /Gy /arch:SSE /fp:fast&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;VC++_OpenMP&lt;/td&gt;&lt;td width="40%"&gt;Visual C++ 2008 (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;/Ox /Ob2 /Oi /Ot /GL /FD /MD /GS- /Gy /arch:SSE /fp:fast /openmp&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;IC++&lt;/td&gt;&lt;td width="40%"&gt;Intel C++ Compiler (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;/Ox /Og /Ob2 /Oi /Ot /Qipo /GA /MD /GS- /Gy /arch:SSE2 /fp:fast /Zi /QxHost&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;IC++_OpenMP&lt;/td&gt;&lt;td width="40%"&gt;Intel C++ Compiler (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;/Ox /Og /Ob2 /Oi /Ot /Qipo /GA /MD /GS- /Gy /arch:SSE2 /fp:fast /Zi /QxHost /Qopenmp&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;GCC&lt;/td&gt;&lt;td width="40%"&gt;GCC 4.3.4 in Cygwin (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;-O3 -march=native -ffast-math&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;GCC_OpenMP&lt;/td&gt;&lt;td width="40%"&gt;GCC 4.3.4 in Cygwin (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;-O3 -march=native -ffast-math -fopenmp&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;C++/CLI&lt;/td&gt;&lt;td width="40%"&gt;Visual C++ 2008 (32-bit), .Net Framework 3.5&lt;/td&gt;&lt;td width="40%"&gt;/Ox /Ob2 /Oi /Ot /GL /FD /MD /GS- /fp:fast /Zi /clr /TP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;C++/CLI_OpenMP&lt;/td&gt;&lt;td width="40%"&gt;Visual C++ 2008 (32-bit), .Net Framework 3.5&lt;/td&gt;&lt;td width="40%"&gt;/Ox /Ob2 /Oi /Ot /GL /FD /MD /GS- /fp:fast /Zi /clr /TP /openmp&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;C#&lt;/td&gt;&lt;td width="40%"&gt;Visual C# 2008 (32-bit), .Net Framework 3.5&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;*C#_outref&lt;/td&gt;&lt;td width="40%"&gt;Visual C# 2008 (32-bit), .Net Framework 3.5&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;F#&lt;/td&gt;&lt;td width="40%"&gt;&lt;p&gt;F# 2.0 (32-bit), .Net Framework 3.5&lt;/p&gt;&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;Java&lt;/td&gt;&lt;td width="40%"&gt;Java SE 1.6.0_17&lt;/td&gt;&lt;td style="text-align: justify" width="40%"&gt;-server&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;JsChrome&lt;/td&gt;&lt;td width="40%"&gt;Chrome 5.0.375.86&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;JsFirefox&lt;/td&gt;&lt;td width="40%"&gt;Firefox 3.6&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;LuaJIT&lt;/td&gt;&lt;td width="40%"&gt;LuaJIT 2.0.0-beta4 (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;Lua&lt;/td&gt;&lt;td width="40%"&gt;LuaJIT (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;-joff&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;Python&lt;/td&gt;&lt;td width="40%"&gt;Python 3.1.2 (32-bit)&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;*IronPython&lt;/td&gt;&lt;td width="40%"&gt;IronPython 2.6 for .Net 4&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;*Jython&lt;/td&gt;&lt;td width="40%"&gt;Jython 2.5.1&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td width="20%"&gt;Ruby&lt;/td&gt;&lt;td width="40%"&gt;Ruby 1.9.1p378&lt;/td&gt;&lt;td width="40%"&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;* 见本文最后的"7.更新"一节&lt;/p&gt;&lt;p&gt;渲染的解像度为256x256，每象素作100次采样。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;结果及分析&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;下表中预设的相对时间以最快的单线程测试(IC++)作基准，用鼠标按列可改变基准。由于Ruby运行时间太长，只每象素作4次采样，把时间乘上25。另外，因为各测试的渲染时间相差很远，所以用了两个棒形图去显示数据，分别显示时间少于4000秒和少于60秒的测试(Ruby是4000秒以外，不予显示)。&lt;/p&gt;&lt;div id='table_div'&gt;&lt;/div&gt;&lt;div id="chart_div"&gt;&lt;/div&gt;&lt;div id="chart2_div"&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;C++/.Net/Java组别&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;静态语言和动态语言在此测试下的性能不在同一数量级。先比较静态语言。&lt;/p&gt;&lt;p&gt;C++和.Net的测试结果和上一篇博文相若，而C#和F#无显著区别。但是，C++/CLI虽然同样产生IL，于括管的.Net平台上执行，其渲染时间却只是C#/F#的55%左右。为什么呢？使用ildasm去反汇编C++/CLI和C#的可执行文件后，可以发现，程序的热点函数Sphere.Intersect()在两个版本中，C++/CLI版本的代码大小(codesize)为201字节， C#则为125字节！ C++/CLI版本在编译时，已把函数内所有Vec类的方法调用全部内联，而C#版本则使用callvirt调用Vec的方法。估计JIT没有把这函数进行内联，做成这个性能差异。另外，C++/CLI版本使用了值类型，并使用指针(代码中为引用)作参数传送。若把C#的版本的Vec方法改写为://class Vec&lt;br/&gt;//{&lt;br/&gt;    //public static Vec operator +(Vec a, Vec b)&lt;br/&gt;//}&lt;br/&gt;&lt;br/&gt;struct Vec&lt;br/&gt;{&lt;br/&gt;    void Add(ref Vec a, ref Vec b, out Vec c);&lt;br/&gt;}&lt;br/&gt;&lt;p&gt;那么，struct不用GC，同时ref/out不用复制，其性能会比较高。但是代码会变得很难看:&lt;/p&gt;// 原来用运算符重载(operator overloading):&lt;br/&gt;a = b * c + d;&lt;br/&gt;&lt;br/&gt;// 改用ref/out&lt;br/&gt;Vec e;&lt;br/&gt;Vec.Mul(ref b, ref, c, out e);&lt;br/&gt;Vec.Add(ref e, ref d, out a);&lt;br/&gt;&lt;p&gt;为了维持让语言"正常"的使用方法，本实验不采用这种API风格(更新:加入了C#_outref测试，詳見文末)。&lt;/p&gt;&lt;p&gt;然而，托管代码(C++/CLI)的渲染时间，仅为原生非括管代码(IC++)的1.91倍，个人觉得.Net的JIT已经非常不错。&lt;/p&gt;&lt;p&gt;另一方面，Java的性能表现非常突出，只比C++/CLI稍慢一点，Java版本的渲染时间为C#/F#的65%左右。以前一直认为，C#不少设计会使其性能高于Java，例如C#的方法预设为非虚，Java则预设为虚；又例如C#支持struct作值类型(valuetype)，Java则只有class引用类型(reference type)，后者必须使用GC。但是，这个测试显示，Java VM应该在JIT中做了大量优化，估计也应用了内联，才能使其性能逼近C++/CLI。&lt;/p&gt;&lt;p&gt;纯C++方面，Intel C++编译器最快，Visual C++慢一点点(1.19x)，GCC再慢一点点(1.32x)。这结果符合本人预期。 Intel C++的OpenMP版本和单线程比较，达5.16加速比(speedup)，对于4核HyperThreading来说算是不错的结果。读者若有兴趣，也可以自行测试C# 4.0的并行新特性。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;动态语言组别&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;首先，要说一句，Google太强了，难以想像JsChome的渲染时间仅是IC++的16.12倍，C#的4.94倍。我有信心用JavaScript继续写图形、物理方面的博文了。&lt;/p&gt;&lt;p&gt;以下比较各动态语言的相对时间，以JsChrome为基准。 Chrome的V8 JavaScript引擎(1.00x)大幅抛离Firefox的SpiderMonkey引擎(15.09x)。而LuaJIT(3.49x)和Lua(5.16x)则排第二和第三名。Lua的JIT版本是没有JIT的68%，并没有想像中的快，但是也比Python(16.48x)快得多。曾听说过Ruby有效能问题，没想到问题竟然如此严重(327.31x)，其渲染时间差不多是Python的20倍。&lt;/p&gt;&lt;p&gt;我认为，本实验中，不同语言的性能差异，并非在于数值运算，而是对象生成及函数调用。我使用Python内建的profiling功能:&lt;/p&gt;&lt;br/&gt;python -m profile smallpt.py&lt;br/&gt;&lt;p&gt;从结果发现，Vec类共产生约15亿个实例，Vec的方法调用约17.5亿次，intersect()共调用5.7亿次，产生随机数5.7亿个，radiance()调用(即追踪的路径线段)6.5百万次。这些庞大数字，放大了对象生成和函数调用的常数开销(overhead)。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;结语&lt;/strong&gt;&lt;/p&gt;也许本博文的意义不大(yet-another-unfair-biased-performance-comparison-among-programming-languages)，但对本人而言，此次实验加深了对各种语言性能的了解，或应该是消除了一些误解。简单总括运行性能方面的体验和感想:&lt;ol&gt;&lt;li&gt;C++和VM类静态语言可以大约只差2~4倍，JVM和CLR差异不大。 &lt;/li&gt;&lt;li&gt;C++和动态语言之比，则可以是15~5000倍，不同动态语言的差异很大。 &lt;/li&gt;&lt;li&gt;一直以为Lua(JIT)会是最快的通用脚本语言，没想到此测试中败给JavaScript(V8)，或许应该多点研究嵌入V8引擎(SWIG能支持就最理想了)。&lt;/li&gt;&lt;li&gt;以为Python和Ruby的性能相差不远，但测试结果两者大相径庭。暂时不太了解Ruby的特长，或许之后再研究其优点是否能盖过其性能问题。 &lt;/li&gt;&lt;/ol&gt;&lt;p&gt;最后建议读者，若要为某应用挑选语言，又要顾及性能，那么应该自己做实验去比较。不要盲目相信一些流言或评测(包括本文)。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;附录: JavaScript版本测试&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;警告: 建议使用Chrome。Firefox可能会慢得无法响应。&lt;/p&gt;&lt;script type="text/javascript" src="http://files.cnblogs.com/miloyip/smallpt.js"&gt;&lt;/script&gt;&lt;button onclick="render(document.getElementById('renderCanvas'), document.getElementById('status'))" type="button"&gt;Run&lt;/button&gt;&lt;button onclick="stop()" type="button"&gt;Stop&lt;/button&gt; &lt;br /&gt;&lt;canvas id="renderCanvas" width="256" height="256"&gt;&lt;/canvas&gt;&lt;div id="status"&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;更新&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;2010/7/7: 新增的C#_outref测试，按&lt;a href ="http://www.cnblogs.com/noremorse/"&gt;noremorse&lt;/a&gt;的&lt;a href="http: //www.cnblogs.com/miloyip/archive/2010/07/07/languages_brawl_GI.html#1865842"&gt;建议&lt;/a&gt;，把Vec和Ray变作struct，所有函数传送这两种对象改为ref/ out。 &lt;a href="http://files.cnblogs.com/miloyip/smallpt_csref20100707.zip"&gt;源代码&lt;/a&gt;。&lt;/li&gt;&lt;li&gt;2010/7/8: 新增IronPython和Jython。&lt;/li&gt;&lt;li&gt;2010/7/8: 园友猫粮撰文&lt;a href="http://bbs.9ria.com/thread-58209-1-1.html"&gt;《AS3的光线跟踪极限测试》&lt;/ a&gt;，看来AS3性能不太好。 &lt;/li&gt;&lt;li&gt;2010/7/10: 园友Domslab撰文&lt;a http://www.cnblogs.com/domslab/archive/2010/07/10/1774686.html"&gt;《对《C++/C#/F#/Java /JS/Lua/Python/Ruby渲染比试》一文的补充——增加Mono测试》&lt;/a&gt;，比较了gcc/mono C#/Java在Windows/Linux的性能。&lt;/li&gt;&lt;li&gt;2010/7/11: 园友noremorse撰文&lt;a href="http://www.cnblogs.com/noremorse/archive/2010/07/10/inline_or_not.html"&gt;《Swifter C#之inline还是不inline，这是个问题》&lt;/a&gt;，以本例研究.Net Runtime的内联机制。 &lt;/li&gt;&lt;/ul&gt;&lt;img src="http://www.cnblogs.com/miloyip/aggbug/1772514.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/07/07/languages_brawl_GI.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/06/23/cpp_vs_cs_GI.html</id><title type="text">C# vs C++ 全局照明渲染性能比试</title><summary type="text">最近有多篇讨论程序语言趋势的博文，其中谈及到C#的性能问题。本人之前未做过相关测试，自己的回覆流于理论猜测，所以花了点时间做个简单实验，比较C#和C++的性能。</summary><published>2010-06-23T01:48:00Z</published><updated>2010-06-23T01:48:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/06/23/cpp_vs_cs_GI.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/06/23/cpp_vs_cs_GI.html"/><content type="html">&lt;p&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_smallpt512_1000.jpg" width=512" height="512" border="0"/&gt;&lt;br /&gt;&lt;small&gt;512x512像素，每像素1000采样，C#版本渲染时间为40分47秒&lt;/small&gt;&lt;/p&gt;&lt;p&gt;最近有多篇讨论程序语言趋势的博文，其中谈及到C#的性能问题。本人之前未做过相关测试，自己的回覆流于理论猜测，所以花了点时间做个简单实验，比较C#和C++的性能。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;实验内容&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;a href="http://blog.zhaojie.me/" id="n-e." title="赵姐夫"&gt;赵姐夫&lt;/a&gt;在&lt;a href="http://www.cnblogs.com/sumtec/archive/2010/06/22/1762564.html#1853492" id="lbxh" title="此回覆"&gt;此回覆&lt;/a&gt;认为，C#比C/C++慢，主要在于.Net平台的垃圾回收(garbage collection, GC)机制。若是计算密集型应用，C#和C++产生的原生代码，速度应该相差不大。我对此半信半疑。想到之前看过一个用99行C++代码实现的全局照明(global illumination, GI)渲染程序&lt;a href="http://kevinbeason.com/smallpt/" id="ry3i" title="smallpt"&gt;smallpt&lt;/a&gt; ，是纯计算密集的。而且在运算期间，若用C#实现，基本上连GC都可以不用。因此，就把该99行代码移植至C#。 &lt;/p&gt;&lt;p&gt;此渲染器的一些特点如下: &lt;/p&gt;&lt;ul&gt;&lt;li&gt;使用蒙地卡罗路径追踪(Monte Carlo path-tracing)来产生全局照明效果&lt;/li&gt;&lt;li&gt;支持三种双向反射分布函数(bidirectional reflectance distribution function, BRDF): 镜射(specular)、漫射(diffuse)和玻璃(即纯折射的介质) &lt;/li&gt;&lt;li&gt;从漫射光源产生柔和阴影(soft shadow) &lt;/li&gt;&lt;li&gt;使用2x2超采样(super-sampling)去实现反锯齿&lt;/li&gt;&lt;li&gt;使用OpenMP作并行运算，充份利用多核性能&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;当中的术语及技术，之后可能会于&lt;a href="http://www.cnblogs.com/miloyip/category/241646.html" id="wz0:" title="图形学博文系列"&gt;图形学博文系列&lt;/a&gt;里探讨。本文主要以性能为题。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt; C++版本&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;以下是C++版本代码，作了些许修改。修改地方加上了MILO注译。 &lt;/p&gt;&lt;br/&gt;#include &amp;lt;math.h&amp;gt;   // smallpt, a Path Tracer by Kevin Beason, 2008&lt;br/&gt;#include &amp;lt;stdlib.h&amp;gt; // Make : g++ -O3 -fopenmp smallpt.cpp -o smallpt&lt;br/&gt;#include &amp;lt;stdio.h&amp;gt;  //        Remove "-fopenmp" for g++ version &amp;lt; 4.2&lt;br/&gt;#include &amp;lt;time.h&amp;gt;// MILO&lt;br/&gt;#include "erand48.inc"// MILO&lt;br/&gt;#define M_PI 3.141592653589793238462643// MILO&lt;br/&gt;struct Vec {        // Usage: time ./smallpt 5000 &amp;&amp; xv image.ppm&lt;br/&gt;  double x, y, z;                  // position, also color (r,g,b)&lt;br/&gt;  Vec(double x_=0, double y_=0, double z_=0){ x=x_; y=y_; z=z_; }&lt;br/&gt;  Vec operator+(const Vec &amp;b) const { return Vec(x+b.x,y+b.y,z+b.z); }&lt;br/&gt;  Vec operator-(const Vec &amp;b) const { return Vec(x-b.x,y-b.y,z-b.z); }&lt;br/&gt;  Vec operator*(double b) const { return Vec(x*b,y*b,z*b); }&lt;br/&gt;  Vec mult(const Vec &amp;b) const { return Vec(x*b.x,y*b.y,z*b.z); }&lt;br/&gt;  Vec&amp; norm(){ return *this = *this * (1/sqrt(x*x+y*y+z*z)); }&lt;br/&gt;  double dot(const Vec &amp;b) const { return x*b.x+y*b.y+z*b.z; } // cross:&lt;br/&gt;  Vec operator%(const Vec &amp;b){return Vec(y*b.z-z*b.y,z*b.x-x*b.z,x*b.y-y*b.x);}&lt;br/&gt;};&lt;br/&gt;struct Ray { Vec o, d; Ray(const Vec &amp;o_, const Vec &amp;d_) : o(o_), d(d_) {} };&lt;br/&gt;enum Refl_t { DIFF, SPEC, REFR };  // material types, used in radiance()&lt;br/&gt;struct Sphere {&lt;br/&gt;  double rad;       // radius&lt;br/&gt;  Vec p, e, c;      // position, emission, color&lt;br/&gt;  Refl_t refl;      // reflection type (DIFFuse, SPECular, REFRactive)&lt;br/&gt;  Sphere(double rad_, Vec p_, Vec e_, Vec c_, Refl_t refl_):&lt;br/&gt;    rad(rad_), p(p_), e(e_), c(c_), refl(refl_) {}&lt;br/&gt;  double intersect(const Ray &amp;r) const { // returns distance, 0 if nohit&lt;br/&gt;    Vec op = p-r.o; // Solve t^2*d.d + 2*t*(o-p).d + (o-p).(o-p)-R^2 = 0&lt;br/&gt;    double t, eps=1e-4, b=op.dot(r.d), det=b*b-op.dot(op)+rad*rad;&lt;br/&gt;    if (det&amp;lt;0) return 0; else det=sqrt(det);&lt;br/&gt;    return (t=b-det)&amp;gt;eps ? t : ((t=b+det)&amp;gt;eps ? t : 0);&lt;br/&gt;  }&lt;br/&gt;};&lt;br/&gt;Sphere spheres[] = {//Scene: radius, position, emission, color, material&lt;br/&gt;  Sphere(1e5, Vec( 1e5+1,40.8,81.6), Vec(),Vec(.75,.25,.25),DIFF),//Left&lt;br/&gt;  Sphere(1e5, Vec(-1e5+99,40.8,81.6),Vec(),Vec(.25,.25,.75),DIFF),//Rght&lt;br/&gt;  Sphere(1e5, Vec(50,40.8, 1e5),     Vec(),Vec(.75,.75,.75),DIFF),//Back&lt;br/&gt;  Sphere(1e5, Vec(50,40.8,-1e5+170), Vec(),Vec(),           DIFF),//Frnt&lt;br/&gt;  Sphere(1e5, Vec(50, 1e5, 81.6),    Vec(),Vec(.75,.75,.75),DIFF),//Botm&lt;br/&gt;  Sphere(1e5, Vec(50,-1e5+81.6,81.6),Vec(),Vec(.75,.75,.75),DIFF),//Top&lt;br/&gt;  Sphere(16.5,Vec(27,16.5,47),       Vec(),Vec(1,1,1)*.999, SPEC),//Mirr&lt;br/&gt;  Sphere(16.5,Vec(73,16.5,78),       Vec(),Vec(1,1,1)*.999, REFR),//Glas&lt;br/&gt;  Sphere(600, Vec(50,681.6-.27,81.6),Vec(12,12,12),  Vec(), DIFF) //Lite&lt;br/&gt;};&lt;br/&gt;inline double clamp(double x){ return x&amp;lt;0 ? 0 : x&amp;gt;1 ? 1 : x; }&lt;br/&gt;inline int toInt(double x){ return int(pow(clamp(x),1/2.2)*255+.5); }&lt;br/&gt;inline bool intersect(const Ray &amp;r, double &amp;t, int &amp;id){&lt;br/&gt;  double n=sizeof(spheres)/sizeof(Sphere), d, inf=t=1e20;&lt;br/&gt;  for(int i=int(n);i--;) if((d=spheres[i].intersect(r))&amp;&amp;d&amp;lt;t){t=d;id=i;}&lt;br/&gt;  return t&amp;lt;inf;&lt;br/&gt;}&lt;br/&gt;Vec radiance(const Ray &amp;r, int depth, unsigned short *Xi){&lt;br/&gt;  double t;                               // distance to intersection&lt;br/&gt;  int id=0;                               // id of intersected object&lt;br/&gt;  if (!intersect(r, t, id)) return Vec(); // if miss, return black&lt;br/&gt;  const Sphere &amp;obj = spheres[id];        // the hit object&lt;br/&gt;  Vec x=r.o+r.d*t, n=(x-obj.p).norm(), nl=n.dot(r.d)&amp;lt;0?n:n*-1, f=obj.c;&lt;br/&gt;  double p = f.x&amp;gt;f.y &amp;&amp; f.x&amp;gt;f.z ? f.x : f.y&amp;gt;f.z ? f.y : f.z; // max refl&lt;br/&gt;  if (++depth&amp;gt;5) if (erand48(Xi)&amp;lt;p) f=f*(1/p); else return obj.e; //R.R.&lt;br/&gt;  if (depth &amp;gt; 100) return obj.e; // MILO&lt;br/&gt;  if (obj.refl == DIFF){                  // Ideal DIFFUSE reflection&lt;br/&gt;    double r1=2*M_PI*erand48(Xi), r2=erand48(Xi), r2s=sqrt(r2);&lt;br/&gt;    Vec w=nl, u=((fabs(w.x)&amp;gt;.1?Vec(0,1):Vec(1))%w).norm(), v=w%u;&lt;br/&gt;    Vec d = (u*cos(r1)*r2s + v*sin(r1)*r2s + w*sqrt(1-r2)).norm();&lt;br/&gt;    return obj.e + f.mult(radiance(Ray(x,d),depth,Xi));&lt;br/&gt;  } else if (obj.refl == SPEC)            // Ideal SPECULAR reflection&lt;br/&gt;    return obj.e + f.mult(radiance(Ray(x,r.d-n*2*n.dot(r.d)),depth,Xi));&lt;br/&gt;  Ray reflRay(x, r.d-n*2*n.dot(r.d));     // Ideal dielectric REFRACTION&lt;br/&gt;  bool into = n.dot(nl)&amp;gt;0;                // Ray from outside going in?&lt;br/&gt;  double nc=1, nt=1.5, nnt=into?nc/nt:nt/nc, ddn=r.d.dot(nl), cos2t;&lt;br/&gt;  if ((cos2t=1-nnt*nnt*(1-ddn*ddn))&amp;lt;0)    // Total internal reflection&lt;br/&gt;    return obj.e + f.mult(radiance(reflRay,depth,Xi));&lt;br/&gt;  Vec tdir = (r.d*nnt - n*((into?1:-1)*(ddn*nnt+sqrt(cos2t)))).norm();&lt;br/&gt;  double a=nt-nc, b=nt+nc, R0=a*a/(b*b), c = 1-(into?-ddn:tdir.dot(n));&lt;br/&gt;  double Re=R0+(1-R0)*c*c*c*c*c,Tr=1-Re,P=.25+.5*Re,RP=Re/P,TP=Tr/(1-P);&lt;br/&gt;  return obj.e + f.mult(depth&amp;gt;2 ? (erand48(Xi)&amp;lt;P ?   // Russian roulette&lt;br/&gt;    radiance(reflRay,depth,Xi)*RP:radiance(Ray(x,tdir),depth,Xi)*TP) :&lt;br/&gt;    radiance(reflRay,depth,Xi)*Re+radiance(Ray(x,tdir),depth,Xi)*Tr);&lt;br/&gt;}&lt;br/&gt;int main(int argc, char *argv[]){&lt;br/&gt;  clock_t start = clock(); // MILO&lt;br/&gt;  int w=512, h=512, samps = argc==2 ? atoi(argv[1])/4 : 250; // # samples&lt;br/&gt;  Ray cam(Vec(50,52,295.6), Vec(0,-0.042612,-1).norm()); // cam pos, dir&lt;br/&gt;  Vec cx=Vec(w*.5135/h), cy=(cx%cam.d).norm()*.5135, r, *c=new Vec[w*h];&lt;br/&gt;#pragma omp parallel for schedule(dynamic, 1) private(r)       // OpenMP&lt;br/&gt;  for (int y=0; y&amp;lt;h; y++){                       // Loop over image rows&lt;br/&gt;    fprintf(stderr,"\rRendering (%d spp) %5.2f%%",samps*4,100.*y/(h-1));&lt;br/&gt;unsigned short Xi[3]={0,0,y*y*y}; // MILO&lt;br/&gt;    for (unsigned short x=0; x&amp;lt;w; x++)   // Loop cols&lt;br/&gt;      for (int sy=0, i=(h-y-1)*w+x; sy&amp;lt;2; sy++)     // 2x2 subpixel rows&lt;br/&gt;        for (int sx=0; sx&amp;lt;2; sx++, r=Vec()){        // 2x2 subpixel cols&lt;br/&gt;          for (int s=0; s&amp;lt;samps; s++){&lt;br/&gt;            double r1=2*erand48(Xi), dx=r1&amp;lt;1 ? sqrt(r1)-1: 1-sqrt(2-r1);&lt;br/&gt;            double r2=2*erand48(Xi), dy=r2&amp;lt;1 ? sqrt(r2)-1: 1-sqrt(2-r2);&lt;br/&gt;            Vec d = cx*( ( (sx+.5 + dx)/2 + x)/w - .5) +&lt;br/&gt;                    cy*( ( (sy+.5 + dy)/2 + y)/h - .5) + cam.d;&lt;br/&gt;            r = r + radiance(Ray(cam.o+d*140,d.norm()),0,Xi)*(1./samps);&lt;br/&gt;          } // Camera rays are pushed ^^^^^ forward to start in interior&lt;br/&gt;          c[i] = c[i] + Vec(clamp(r.x),clamp(r.y),clamp(r.z))*.25;&lt;br/&gt;        }&lt;br/&gt;  }&lt;br/&gt;  printf("\n%f sec\n", (float)(clock() - start)/CLOCKS_PER_SEC); // MILO&lt;br/&gt;  FILE *f = fopen("image.ppm", "w");         // Write image to PPM file.&lt;br/&gt;  fprintf(f, "P3\n%d %d\n%d\n", w, h, 255);&lt;br/&gt;  for (int i=0; i&amp;lt;w*h; i++)&lt;br/&gt;    fprintf(f,"%d %d %d ", toInt(c[i].x), toInt(c[i].y), toInt(c[i].z));&lt;br/&gt;}&lt;br/&gt;&lt;p&gt;由于Visual C++没有erand48()函数，便在网上找到一个&lt;a href="http://doxygen.postgresql.org/erand48_8c-source.html" id="ngln" title="PostgreSQL的实现"&gt;PostreSQL的实现&lt;/a&gt; 。此外，为了比较公平，分别测试使用和禁用OpenMP情况下的性能。 &lt;/p&gt;&lt;p&gt;本人亦为了显示C++特有的能力，另外作一个版本，采用微软DirectX SDK中的&lt;a href="http://msdn.microsoft.com/en-us/library/ee415574(VS.85).aspx" id="f.ns" title="XNA数学库"&gt;C++ XNA数学库&lt;/a&gt;进行SIMD矢量加速(XNA Game Studio也有.Net用的XNA数学库，但本文并没有使用)。由于XNA数学库采用单精度浮点数，对这个特别场景(6面墙壁其实是由6个巨大的球体组成)有超出精度范围的问题。因此，我对这版本里的场境稍作修改。又因为erand48()函数是传回双精度的随机数，多次转换比较慢，此版本就换了&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/05/27/reply_discrete.html" id="ibtz" title="之前博文"&gt;之前博文&lt;/a&gt;使用的LCG实现。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt; C#版本&lt;/strong&gt;&lt;/p&gt;&lt;br/&gt;using System;&lt;br/&gt;using System.IO;&lt;br/&gt;namespace smallpt_cs {&lt;br/&gt;struct Vec {        // Usage: time ./smallpt 5000 &amp;&amp; xv image.ppm&lt;br/&gt;   public double x,y,z;                 // position,also color (r,g,b)&lt;br/&gt;   public Vec(double x_,double y_,double z_) {x=x_;y=y_;z=z_;}&lt;br/&gt;   public static Vec operator +(Vec a,Vec b) {return new Vec(a.x+b.x,a.y+b.y,a.z+b.z);}&lt;br/&gt;   public static Vec operator -(Vec a,Vec b) {return new Vec(a.x-b.x,a.y-b.y,a.z-b.z);}&lt;br/&gt;   public static Vec operator *(Vec a,double b) {return new Vec(a.x*b,a.y*b,a.z*b);}&lt;br/&gt;   public Vec mult(Vec b) { return new Vec(x*b.x,y*b.y,z*b.z);}&lt;br/&gt;   public Vec norm() { return this=this*(1/Math.Sqrt(x*x+y*y+z*z));}&lt;br/&gt;   public double dot(Vec b) { return x*b.x+y*b.y+z*b.z;}//cross:&lt;br/&gt;   public static Vec operator %(Vec a,Vec b) { return new Vec(a.y*b.z-a.z*b.y,a.z*b.x-a.x*b.z,a.x*b.y-a.y*b.x);}&lt;br/&gt;}&lt;br/&gt;enum Refl_t { DIFF,SPEC,REFR }; // material types,used in radiance()&lt;br/&gt;struct Ray { public Vec o,d;public Ray(Vec o_,Vec d_) { o=o_;d=d_;} }&lt;br/&gt;class Sphere {&lt;br/&gt;  public double rad;      // radius&lt;br/&gt;  public Vec p,e,c;     // position,emission,color&lt;br/&gt;  public Refl_t refl;     // reflection type (DIFFuse,SPECular,REFRactive)&lt;br/&gt;  public Sphere(double rad_,Vec p_,Vec e_,Vec c_,Refl_t refl_) {&lt;br/&gt;    rad=rad_;p=p_;e=e_;c=c_;refl=refl_;&lt;br/&gt;  }&lt;br/&gt;  public double intersect(Ray r)&lt;br/&gt;  { // returns distance,0 if nohit&lt;br/&gt;    Vec op=p-r.o;// Solve t^2*d.d+2*t*(o-p).d+(o-p).(o-p)-R^2=0&lt;br/&gt;    double t,eps=1e-4,b=op.dot(r.d),det=b*b-op.dot(op)+rad*rad;&lt;br/&gt;    if (det&amp;lt;0) return 0;else det=Math.Sqrt(det);&lt;br/&gt;    return (t=b-det) &amp;gt; eps?t : ((t=b+det) &amp;gt; eps?t : 0);&lt;br/&gt;  }&lt;br/&gt;};&lt;br/&gt;class smallpt {&lt;br/&gt;  static Random random=new Random();&lt;br/&gt;  static double erand48() { return random.NextDouble();}&lt;br/&gt;  static Sphere[] spheres={//Scene: radius,position,emission,color,material&lt;br/&gt;    new Sphere(1e5,new Vec( 1e5+1,40.8,81.6), new Vec(),new Vec(.75,.25,.25),Refl_t.DIFF),//Left&lt;br/&gt;    new Sphere(1e5,new Vec(-1e5+99,40.8,81.6),new Vec(),new Vec(.25,.25,.75),Refl_t.DIFF),//Rght&lt;br/&gt;    new Sphere(1e5,new Vec(50,40.8,1e5),      new Vec(),new Vec(.75,.75,.75),Refl_t.DIFF),//Back&lt;br/&gt;    new Sphere(1e5,new Vec(50,40.8,-1e5+170), new Vec(),new Vec(),           Refl_t.DIFF),//Frnt&lt;br/&gt;    new Sphere(1e5,new Vec(50,1e5,81.6),      new Vec(),new Vec(.75,.75,.75),Refl_t.DIFF),//Botm&lt;br/&gt;    new Sphere(1e5,new Vec(50,-1e5+81.6,81.6),new Vec(),new Vec(.75,.75,.75),Refl_t.DIFF),//Top&lt;br/&gt;    new Sphere(16.5,new Vec(27,16.5,47),      new Vec(),new Vec(1,1,1)*.999, Refl_t.SPEC),//Mirr&lt;br/&gt;    new Sphere(16.5,new Vec(73,16.5,78),      new Vec(),new Vec(1,1,1)*.999, Refl_t.REFR),//Glas&lt;br/&gt;    new Sphere(600,new Vec(50,681.6-.27,81.6),new Vec(12,12,12), new Vec(),  Refl_t.DIFF) //Lite&lt;br/&gt;  };&lt;br/&gt;  static double clamp(double x) { return x&amp;lt;0?0 : x &amp;gt; 1?1 : x;}&lt;br/&gt;  static int toInt(double x) { return (int)(Math.Pow(clamp(x),1 / 2.2)*255+.5);}&lt;br/&gt;  static bool intersect(Ray r,ref double t,ref int id) {&lt;br/&gt;    double d,inf=t=1e20;&lt;br/&gt;    for (int i=spheres.Length-1;i &amp;gt;= 0;i--)&lt;br/&gt;      if ((d=spheres[i].intersect(r)) != 0 &amp;&amp; d&amp;lt;t) { t=d;id=i;}&lt;br/&gt;    return t&amp;lt;inf;&lt;br/&gt;  }&lt;br/&gt;  static Vec radiance(Ray r,int depth) {&lt;br/&gt;    double t=0;                              // distance to intersection&lt;br/&gt;    int id=0;                              // id of intersected object&lt;br/&gt;    if (!intersect(r,ref t,ref id)) return new Vec();// if miss,return black&lt;br/&gt;    Sphere obj=spheres[id];       // the hit object&lt;br/&gt;    Vec x=r.o+r.d*t,n=(x-obj.p).norm(),nl=n.dot(r.d)&amp;lt;0?n:n*-1,f=obj.c;&lt;br/&gt;    double p=f.x&amp;gt;f.y&amp;&amp;f.x&amp;gt;f.z?f.x:f.y&amp;gt;f.z?f.y:f.z;//max refl&lt;br/&gt;    if (++depth &amp;gt; 5) if (erand48()&amp;lt;p) f=f*(1 / p);else return obj.e;//R.R.&lt;br/&gt;    if (depth &amp;gt; 100) return obj.e;&lt;br/&gt;    if (obj.refl == Refl_t.DIFF) {                  // Ideal DIFFUSE reflection&lt;br/&gt;      double r1=2*Math.PI*erand48(),r2=erand48(),r2s=Math.Sqrt(r2);&lt;br/&gt;      Vec w=nl,u=((Math.Abs(w.x)&amp;gt;.1?new Vec(0,1,0):new Vec(1,0,0))%w).norm(),v=w%u;&lt;br/&gt;      Vec d=(u*Math.Cos(r1)*r2s+v*Math.Sin(r1)*r2s+w*Math.Sqrt(1-r2)).norm();&lt;br/&gt;      return obj.e+f.mult(radiance(new Ray(x,d),depth));&lt;br/&gt;    }&lt;br/&gt;    else if (obj.refl == Refl_t.SPEC)            // Ideal SPECULAR reflection&lt;br/&gt;      return obj.e+f.mult(radiance(new Ray(x,r.d-n*2*n.dot(r.d)),depth));&lt;br/&gt;    Ray reflRay=new Ray(x,r.d-n*2*n.dot(r.d));//IdealdielectricREFRACTION&lt;br/&gt;    bool into=n.dot(nl) &amp;gt; 0;               // Ray from outside going in?&lt;br/&gt;    double nc=1,nt=1.5,nnt=into?nc / nt : nt / nc,ddn=r.d.dot(nl),cos2t;&lt;br/&gt;    if ((cos2t=1-nnt*nnt*(1-ddn*ddn))&amp;lt;0)    //Total internal reflection&lt;br/&gt;      return obj.e+f.mult(radiance(reflRay,depth));&lt;br/&gt;    Vec tdir=(r.d*nnt-n*((into?1:-1)*(ddn*nnt+Math.Sqrt(cos2t)))).norm();&lt;br/&gt;    double a=nt-nc,b=nt+nc,R0=a*a/(b*b),c=1-(into?-ddn:tdir.dot(n));&lt;br/&gt;    double Re=R0+(1-R0)*c*c*c*c*c,Tr=1-Re,P=.25+.5*Re,RP=Re/P,TP=Tr/(1-P);&lt;br/&gt;    return obj.e+f.mult(depth &amp;gt; 2?(erand48()&amp;lt;P?  // Russian roulette&lt;br/&gt;      radiance(reflRay,depth)*RP:radiance(new Ray(x,tdir),depth)*TP):&lt;br/&gt;      radiance(reflRay,depth)*Re+radiance(new Ray(x,tdir),depth)*Tr);&lt;br/&gt;  }&lt;br/&gt;  public static void Main(string[] args) {&lt;br/&gt;    DateTime start=DateTime.Now;&lt;br/&gt;    int w=256,h=256,samps=args.Length==2?int.Parse(args[1])/4:25;// # samples&lt;br/&gt;    Ray cam=new Ray(new Vec(50,52,295.6),new Vec(0,-0.042612,-1).norm());//cam pos,dir&lt;br/&gt;    Vec cx=new Vec(w*.5135/h,0,0),cy=(cx%cam.d).norm()*.5135,r;Vec[] c=new Vec[w*h];&lt;br/&gt;    for (int y=0;y&amp;lt;h;y++) {                        // Loop over image rows&lt;br/&gt;      Console.Write("\rRendering ({0}spp) {1:F2}%",samps*4,100.0*y/(h-1));&lt;br/&gt;      for (int x=0;x&amp;lt;w;x++)   // Loop cols&lt;br/&gt;        for (int sy=0,i=(h-y-1)*w+x;sy&amp;lt;2;sy++)     // 2x2 subpixel rows&lt;br/&gt;          for (int sx=0;sx&amp;lt;2;sx++) {               // 2x2 subpixel cols&lt;br/&gt;            r=new Vec();&lt;br/&gt;            for (int s=0;s&amp;lt;samps;s++) {&lt;br/&gt;              double r1=2*erand48(),dx=r1&amp;lt;1?Math.Sqrt(r1)-1:1-Math.Sqrt(2-r1);&lt;br/&gt;              double r2=2*erand48(),dy=r2&amp;lt;1?Math.Sqrt(r2)-1:1-Math.Sqrt(2-r2);&lt;br/&gt;              Vec d=cx*(((sx+.5+dx)/2+x)/w-.5)+&lt;br/&gt;                    cy*(((sy+.5+dy)/2+y)/h-.5)+cam.d;&lt;br/&gt;              r=r+radiance(new Ray(cam.o+d*140,d.norm()),0)*(1.0/samps);&lt;br/&gt;            } // Camera rays are pushed ^^^^^ forward to start in interior&lt;br/&gt;            c[i]=c[i]+new Vec(clamp(r.x),clamp(r.y),clamp(r.z))*.25;&lt;br/&gt;          }&lt;br/&gt;    }&lt;br/&gt;    Console.WriteLine("\n{0} sec",(DateTime.Now-start).TotalSeconds);&lt;br/&gt;    using (StreamWriter sw=new StreamWriter("image.ppm")) {&lt;br/&gt;      sw.Write("P3\r\n{0} {1}\r\n{2}\r\n",w,h,255);&lt;br/&gt;      for (int i=0;i&amp;lt;w*h;i++)&lt;br/&gt;        sw.Write("{0} {1} {2}\r\n",toInt(c[i].x),toInt(c[i].y),toInt(c[i].z));&lt;br/&gt;      sw.Close();&lt;br/&gt;    }&lt;br/&gt;  }&lt;br/&gt;}&lt;br/&gt;}&lt;br/&gt;&lt;p&gt; Vec和Ray需要不断在计算中产生实例，所以设它们为struct，struct在C#代表值类型(value type)，ibpp在堆栈上高效分配内存的，不需使用GC。渲染时，Sphere是只读对象，因此用class作为引用类型(reference type)去避免不必要的复制。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;实验结果和分析&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;实验环境是Visual Studio 2008/.Net Framework 3.5编译，Intel I7 920 (4核、超线程)。渲染512x512解像度，每像素100个采样。结果如下: &lt;/p&gt;&lt;table border="1" bordercolor="#000000" cellpadding="3" cellspacing="0" id="pmkk"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;测试版本&lt;/td&gt;&lt;td&gt;需时(秒) &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;(a) &lt;/td&gt;&lt;td&gt;C++&lt;/td&gt;&lt;td&gt;45.548&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;(b) &lt;/td&gt;&lt;td&gt;C#&lt;/td&gt;&lt;td&gt;61.044 &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;(c) &lt;/td&gt;&lt;td&gt;C++ SIMD&lt;/td&gt;&lt;td&gt;20.500&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;(d) &lt;/td&gt;&lt;td&gt;C++(OpenMP)&lt;/td&gt;&lt;td&gt;7.397&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt; (e) &lt;/td&gt;&lt;td&gt; C++ SIMD(OpenMP)&lt;/td&gt;&lt;td&gt; 3.470 &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;(f)* &lt;/td&gt;&lt;td&gt; C++ LCG&lt;/td&gt;&lt;td&gt; 17.365 &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;(g)* &lt;/td&gt;&lt;td&gt; C# LCG&lt;/td&gt;&lt;td&gt; 59.623 &lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;(h)* &lt;/td&gt;&lt;td&gt; C++ LCG (OpenMP)&lt;/td&gt;&lt;td&gt; 3.427 &lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;strong&gt;*2010/6/23 加入(f)(g)(h)，見更新1&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;最基本，应比较(a)和(b)。两者皆使用单线程。 C++版本性能比C#版本快大约34%。这其实已远远超出我对C#/.Net的期望，没想到用JIT代码的运行速度，能这么接近传统的编译方式。 &lt;/p&gt;&lt;p&gt;采用SIMD的C++版本(c)，虽然仍未大量优化，但性能比没有SIMD的版本高122%，比C#版本高接近两倍。不过，采用SIMD后，数值运算的精确度变低，所以这比较只能作为参考。 &lt;/p&gt;&lt;p&gt;采用OpenMP能活用i7的8个逻辑核心。使用OpenMP的非SIMD(d)和SIMD(e)版本，分别比没使用OpenMP的版本(a)和(c)，性能各为6.16倍和5.9倍。这已经很接近理想值8，说明这应用能充分利用CPU并行性。而OpenMP强大的地方，在于只需加入1句编译器#pragma指令就能自动并行。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;结语&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;虽然本文的实验只能反映个别情况。但实验可以说明，在某些应用上，C#/.Net的性能可以非常贴近C++，差别小于一个数量级。 &lt;/p&gt;&lt;p&gt;本文实验所用的程序代码，有不少进一步优化的空间，源代码可于&lt;a href="http://files.cnblogs.com/miloyip/smallpt20100623.zip"&gt;这里下载&lt;/a&gt;。有兴趣的朋友也可把代码移植至Java及其他语言。 &lt;/p&gt;&lt;p&gt;最后，本人认为，各种平台和语言，都有其适用时机。作为程序员，最理想是认识各种技术，以及认清每个技术的特长、短处，以便为应用找到最好的配撘。 &lt;/p&gt;&lt;p&gt;&lt;strong&gt;更新&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;2010/6/23: 园友&lt;a href="http://www.cnblogs.com/aoaoblogs/"&gt;嗷嗷&lt;/a&gt;在本文&lt;a href="http://www.cnblogs. com/miloyip/archive/2010/06/23/cpp_vs_cs_GI.html#1855140"&gt;#78楼&lt;/a&gt;回覆中指出，C++版本的很大部分开销在于erand48()函数里调用Runtime库函数。所以，决定用简单的LCG随机数实现，取代原来的库函数(包括C++和C#)，再测试三个版本(f)(g)(h)。结果: &lt;strong&gt;C++版本(f)比C#版本(g)快2.43倍。 &lt;/strong&gt;使用OpenMP(h)是没用OpenMP(f)版本的5.06倍。此LCG版本代码可于&lt;a href="http://files.cnblogs.com/miloyip/smallpt20100623_LCG.zip"&gt;此下载&lt;/a&gt;。再次感谢这位园友。&lt;/li&gt;&lt;/ol&gt;&lt;img src="http://www.cnblogs.com/miloyip/aggbug/1763267.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/06/23/cpp_vs_cs_GI.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/06/14/Kinematics_ParticleSystem.html</id><title type="text">用JavaScript玩转游戏物理(一)运动学模拟与粒子系统</title><summary type="text">系列简介也许，三百年前的艾萨克·牛顿爵士(Sir Issac Newton, 1643-1727)并没幻想过，物理学广泛地应用在今天许多游戏、动画中。为什么在这些应用中要使用物理学？笔者认为，自我们出生以来，一直感受着物理世界的规律，意识到物体在这世界是如何"正常移动"，例如射球时球为抛物线(自旋的球可能会做成弧线球) 、石子系在一根线的末端会以固定频率摆动等等。要让游戏或动画中的物体有真实感，其...</summary><published>2010-06-14T08:19:00Z</published><updated>2010-06-14T08:19:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/06/14/Kinematics_ParticleSystem.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/06/14/Kinematics_ParticleSystem.html"/><content type="html">&lt;style type="text/css"&gt;&lt;!--canvas { border-style:solid;border-width:1px;}--&gt;&lt;/style&gt;&lt;script language="javascript"&gt;// Framework.jsvar canvas, ctx, isContinue, timeoutID;function start(canvasName, func) {    if (timeoutID)        stop();    canvas = document.getElementById(canvasName);    ctx = canvas.getContext("2d");    isContinue = true;    var loop = function() {        func();        if (isContinue)            timeoutID = setTimeout(loop, 10);    }    loop();}function stop() {    clearTimeout(timeoutID);    isContinue = false;}function clearCanvas() {    if (ctx != null)        ctx.clearRect(0, 0, canvas.width, canvas.height);}// Vector2.jsVector2 = function(x, y) { this.x = x; this.y = y; };Vector2.prototype = {    copy: function() { return new Vector2(this.x, this.y); },    length: function() { return Math.sqrt(this.x * this.x + this.y * this.y); },    sqrLength: function() { return this.x * this.x + this.y * this.y; },    normalize: function() { var inv = 1 / this.length(); return new Vector2(this.x * inv, this.y * inv); },    negate: function() { return new Vector2(-this.x, -this.y); },    add: function(v) { return new Vector2(this.x + v.x, this.y + v.y); },    subtract: function(v) { return new Vector2(this.x - v.x, this.y - v.y); },    multiply: function(f) { return new Vector2(this.x * f, this.y * f); },    divide: function(f) { var invf = 1 / f; return new Vector2(this.x * invf, this.y * invf); },    dot: function(v) { return this.x * v.x + this.y * v.y; }};Vector2.zero = new Vector2(0, 0);// Color.jsColor = function(r, g, b) { this.r = r; this.g = g; this.b = b };Color.prototype = {    copy: function() { return new Color(this.r, this.g, this.b); },    add: function(c) { return new Color(this.r + c.r, this.g + c.g, this.b + c.b); },    multiply: function(s) { return new Color(this.r * s, this.g * s, this.b * s); },    modulate: function(c) { return new Color(this.r * c.r, this.g * c.g, this.b * c.b); },    saturate: function() { this.r = Math.min(this.r, 1); this.g = Math.min(this.g, 1); this.b = Math.min(this.b, 1); }};Color.black = new Color(0, 0, 0);Color.white = new Color(1, 1, 1);Color.red = new Color(1, 0, 0);Color.green = new Color(0, 1, 0);Color.blue = new Color(0, 0, 1);Color.yellow = new Color(1, 1, 0);Color.cyan = new Color(0, 1, 1);Color.purple = new Color(1, 0, 1);// Particle.jsParticle = function(position, velocity, life, color, size) {    this.position = position;    this.velocity = velocity;    this.acceleration = Vector2.zero;    this.age = 0;    this.life = life;    this.color = color;    this.size = size;};// ParticleSystem.jsfunction ParticleSystem() {    // Private fields    var that = this;    var particles = new Array();    // Public fields    this.gravity = new Vector2(0, 100);    this.effectors = new Array();    // Public methods    this.emit = function(particle) {        particles.push(particle);    };    this.simulate = function(dt) {        aging(dt);        applyGravity();        applyEffectors();        kinematics(dt);    };    this.render = function(ctx) {        for (var i in particles) {            var p = particles[i];            var alpha = 1 - p.age / p.life;            ctx.fillStyle = "rgba("                + Math.floor(p.color.r * 255) + ","                + Math.floor(p.color.g * 255) + ","                + Math.floor(p.color.b * 255) + ","                + alpha.toFixed(2) + ")";            ctx.beginPath();            ctx.arc(p.position.x, p.position.y, p.size, 0, Math.PI * 2, true);            ctx.closePath();            ctx.fill();        }    }    // Private methods    function aging(dt) {        for (var i = 0; i &lt; particles.length; ) {            var p = particles[i];            p.age += dt;            if (p.age &gt;= p.life)                kill(i);            else                i++;        }    }    function kill(index) {        if (particles.length &gt; 1)            particles[index] = particles[particles.length - 1];        particles.pop();    }    function applyGravity() {        for (var i in particles)            particles[i].acceleration = that.gravity;    }    function applyEffectors() {        for (var j in that.effectors) {            var apply = that.effectors[j].apply;            for (var i in particles)                apply(particles[i]);        }    }    function kinematics(dt) {        for (var i in particles) {            var p = particles[i];            p.position = p.position.add(p.velocity.multiply(dt));            p.velocity = p.velocity.add(p.acceleration.multiply(dt));        }    }}// ChamberBox.js/* * @requires Vector2, Particle*/function ChamberBox(x1, y1, x2, y2) {    this.apply = function(particle) {        if (particle.position.x - particle.size &lt; x1 || particle.position.x + particle.size &gt; x2)            particle.velocity.x = -particle.velocity.x;        if (particle.position.y - particle.size &lt; y1 || particle.position.y + particle.size &gt; y2)            particle.velocity.y = -particle.velocity.y;    };}&lt;/script&gt;&lt;img src="http://images.cnblogs.com/cnblogs_com/miloyip/250166/r_particlesystem1.jpg" width="512" height="512"/&gt;&lt;p&gt;&lt;strong&gt;系列简介&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;也许，三百年前的艾萨克·牛顿爵士(Sir Issac Newton, 1643-1727)并没幻想过，物理学广泛地应用在今天许多游戏、动画中。为什么在这些应用中要使用物理学？笔者认为，自我们出生以来，一直感受着物理世界的规律，意识到物体在这世界是如何"正常移动"，例如射球时球为抛物线(自旋的球可能会做成弧线球) 、石子系在一根线的末端会以固定频率摆动等等。要让游戏或动画中的物体有真实感，其移动方式就要符合我们对"正常移动"的预期。&lt;/p&gt;&lt;p&gt;今天的游戏动画应用了多种物理模拟技术，例如运动学模拟(kinematics simulation)、刚体动力学模拟(rigid body dynamics simulation)、绳子/布料模拟(string/cloth simulation)、柔体动力学模拟(soft body dynamics simulation)、流体动力学模拟(fluid dynamics simulation)等等。另外碰撞侦测(collision detection)是许多模拟系统里所需的。&lt;/p&gt;&lt;p&gt;本系列希望能介绍一些这方面最基础的知识，继续使用JavaScript做例子，以即时互动方式体验。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;本文简介&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;作为系列第一篇，本文介绍最简单的运动学模拟，只有两条非常简单的公式。运动学模拟可以用来模拟很多物体运动(例如马里奥的跳跃、炮弹等)，本文将会配合粒子系统做出一些视觉特效(粒子系统其实也可以用来做游戏的玩法，而不单是视觉特效)。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;运动学模拟&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;运动学(kinematics)研究物体的移动，和动力学(dynamics)不同之处，在于运动学不考虑物体的质量(mass)/转动惯量(moment of inertia)，以及不考虑加之于物体的力(force )和力矩(torque)。&lt;/p&gt;&lt;p&gt;我们先回忆牛顿第一运动定律:&lt;/p&gt;&lt;blockquote style='border:2px solid #EFEFEF;color:#333333;padding:5px 10px;'&gt;当物体不受外力作用，或所受合力为零时，原先静止者恒静止，原先运动者恒沿着直线作等速度运动。该定律又称为「惯性定律」。&lt;/blockquote&gt;&lt;p&gt;此定律指出，每个物体除了其位置(position)外，还有一个线性速度(linear velocity)的状态。然而，只模拟不受力影响的物体并不有趣。撇开力的概念，我们可以用线性加速度(linear acceleration)去影响物体的运动。例如，要计算一个自由落体在任意时间t的y轴座标，可以使用以下的分析解(analytical solution):&lt;/p&gt;&lt;div class="math"&gt;y(t)=\frac{1}{2}gt^2+v_0t+y_0&lt;/div&gt;&lt;p&gt;当中，&lt;span class="math"&gt;y_0&lt;/span&gt;和&lt;span class="math"&gt;v_0&lt;/span&gt;分别是t=0时的y轴起始座标和速度，而g则是重力加速度(gravitational acceleration)。&lt;/p&gt;&lt;p&gt;这分析解虽然简单，但是有一些缺点，例如g是常数，在模拟过程中不能改变；另外，当物体遇到障碍物，产生碰撞时，这公式也很难处理这种不连续性(discontinuity) 。&lt;/p&gt;&lt;p&gt;在计算机模拟中，通常需要计算连续的物体状态。用游戏的用语，就是计算第一帧的状态、第二帧的状态等等。设物体在任意时间t的状态：位置矢量为&lt;span class="math"&gt;\mathbf{r}(t)&lt;/span&gt;、速度矢量为&lt;span class="math"&gt;\mathbf{v}(t)&lt;/span&gt;、加速度矢量为&lt;span class="math"&gt;\mathbf{a}(t)&lt;/span&gt;。我们希望从时间&lt;span class="math"&gt;t&lt;/span&gt;的状态，计算下一个模拟时间&lt;span class="math"&gt;t+\Delta t&lt;/span&gt;的状态。最简单的方法，是采用欧拉方法(Euler method)作数值积分(numerical integration):&lt;/p&gt;&lt;div class="math"&gt;\begin{align*} \mathbf{v}(t+\Delta t) &amp;= \mathbf{v}(t)+\mathbf{a}(t)\Delta t \\  \mathbf{r}(t+\Delta t) &amp;= \mathbf{r}(t)+\mathbf{v}(t)\Delta t\end{align*}&lt;/div&gt;&lt;p&gt;欧拉方法非常简单，但有准确度和稳定性问题，本文会先忽略这些问题。本文的例子采用二维空间，我们先实现一个JavaScript二维矢量类:&lt;/p&gt;// Vector2.js&lt;br/&gt;Vector2 = function(x, y) { this.x = x; this.y = y; };&lt;br/&gt;&lt;br/&gt;Vector2.prototype = {&lt;br/&gt;    copy : function() { return new Vector2(this.x, this.y); },&lt;br/&gt;    length : function() { return Math.sqrt(this.x * this.x + this.y * this.y); },&lt;br/&gt;    sqrLength : function() { return this.x * this.x + this.y * this.y; },&lt;br/&gt;    normalize : function() { var inv = 1/this.length(); return new Vector2(this.x * inv, this.y * inv); },&lt;br/&gt;    negate : function() { return new Vector2(-this.x, -this.y); },&lt;br/&gt;    add : function(v) { return new Vector2(this.x + v.x, this.y + v.y); },&lt;br/&gt;    subtract : function(v) { return new Vector2(this.x - v.x, this.y - v.y); },&lt;br/&gt;    multiply : function(f) { return new Vector2(this.x * f, this.y * f); },&lt;br/&gt;    divide : function(f) { var invf = 1/f; return new Vector2(this.x * invf, this.y * invf); },&lt;br/&gt;    dot : function(v) { return this.x * v.x + this.y * v.y; }&lt;br/&gt;};&lt;br/&gt;&lt;br/&gt;Vector2.zero = new Vector2(0, 0);&lt;br/&gt;&lt;p&gt;然后，就可以用HTML5 Canvas去描绘模拟的过程:&lt;/p&gt;&lt;textarea id="kinematicsCode" rows="20" cols="100"&gt;var position = new Vector2(10, 200);var velocity = new Vector2(50, -50);var acceleration = new Vector2(0, 10);var dt = 0.1;function step() {    position = position.add(velocity.multiply(dt));    velocity = velocity.add(acceleration.multiply(dt));    ctx.strokeStyle = "#000000";    ctx.fillStyle = "#FFFFFF";    ctx.beginPath();    ctx.arc(position.x, position.y, 5, 0, Math.PI*2, true);     ctx.closePath();    ctx.fill();    ctx.stroke();}    start("kinematicsCancas", step);&lt;/textarea&gt;&lt;br /&gt;&lt;button onclick="eval(document.getElementById('kinematicsCode').value)" type="button"&gt;Run&lt;/button&gt;&lt;button onclick="stop();" type="button"&gt;Stop&lt;/button&gt;&lt;button onclick="clearCanvas();" type="button"&gt;Clear&lt;/button&gt;&lt;br /&gt;&lt;table border="0" style="width: 100%;"&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;&lt;canvas id="kinematicsCancas" width="400" height="400"&gt;&lt;/canvas&gt;&lt;/td&gt; &lt;td width="10"&gt;&amp;nbsp;&lt;/td&gt; &lt;td width="100%" valign="top"&gt; &lt;p&gt;&lt;strong&gt;修改代码试试看&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;改变起始位置&lt;/li&gt;&lt;li&gt;改变起始速度(包括方向) &lt;/li&gt;&lt;li&gt;改变加速度&lt;/li&gt;&lt;/ol&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;这程序的核心就是step()函数头两行代码。很简单吧？&lt;/p&gt;&lt;p&gt;&lt;strong&gt;粒子系统&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;粒子系统(particle system)是图形里常用的特效。粒子系统可应用运动学模拟来做到很多不同的效果。粒子系统在游戏和动画中，常常会用来做雨点、火花、烟、爆炸等等不同的视觉效果。有时候，也会做出一些游戏性相关的功能，例如敌人被打败后会发出一些闪光，主角可以把它们吸收。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;粒子的定义&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;粒子系统模拟大量的粒子，并通常用某些方法把粒子渲染。粒子通常有以下特性:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;粒子是独立的，粒子之间互不影响(不碰撞、没有力) &lt;/li&gt;&lt;li&gt;粒子有生命周期，生命结束后会消失&lt;/li&gt;&lt;li&gt;粒子可以理解为空间的一个点，有时候也可以设定半径作为球体和环境碰撞&lt;/li&gt;&lt;li&gt;粒子带有运动状态，也有其他外观状态(例如颜色、影像等) &lt;/li&gt;&lt;li&gt;粒子可以只有线性运动，而不考虑旋转运动(也有例外) &lt;/li&gt;&lt;/ol&gt;&lt;p&gt;以下是本文例子里实现的粒子类:&lt;/p&gt;// Particle.js&lt;br/&gt;Particle = function(position, velocity, life, color, size) {&lt;br/&gt;    this.position = position;&lt;br/&gt;    this.velocity = velocity;&lt;br/&gt;    this.acceleration = Vector2.zero;&lt;br/&gt;    this.age = 0;&lt;br/&gt;    this.life = life;&lt;br/&gt;    this.color = color;&lt;br/&gt;    this.size = size;&lt;br/&gt;};&lt;br/&gt;&lt;p&gt;&lt;strong&gt;游戏循环&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;粒子系统通常可分为三个周期:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;发射粒子&lt;/li&gt;&lt;li&gt;模拟粒子(粒子老化、碰撞、运动学模拟等等) &lt;/li&gt;&lt;li&gt;渲染粒子&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;在游戏循环(game loop)中，需要对每个粒子系统执行以上的三个步骤。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;生与死&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;在本文的例子里，用一个JavaScript数组particles储存所有活的粒子。产生一个粒子只是把它加到数组末端。代码片段如下:&lt;/p&gt;//ParticleSystem.js&lt;br/&gt;function ParticleSystem() {&lt;br/&gt;    // Private fields&lt;br/&gt;    var that = this;&lt;br/&gt;    var particles = new Array();&lt;br/&gt;&lt;br/&gt;    // Public fields&lt;br/&gt;    this.gravity = new Vector2(0, 100);&lt;br/&gt;    this.effectors = new Array();&lt;br/&gt;&lt;br/&gt;    // Public methods&lt;br/&gt;        &lt;br/&gt;    this.emit = function(particle) {&lt;br/&gt;        particles.push(particle);&lt;br/&gt;    };&lt;br/&gt;&lt;br/&gt;    // ...&lt;br/&gt;}&lt;br/&gt;&lt;p&gt;粒子在初始化时，年龄(age)设为零，生命(life)则是固定的。年龄和生命的单位都是秒。每个模拟步，都会把粒子老化，即是把年龄增加&lt;span class="math"&gt;\Delta t&lt;/span&gt;，年龄超过生命，就会死亡。代码片段如下:&lt;/p&gt;function ParticleSystem() {&lt;br/&gt;    // ... &lt;br/&gt;    this.simulate = function(dt) {&lt;br/&gt;        aging(dt);&lt;br/&gt;        applyGravity();&lt;br/&gt;        applyEffectors();&lt;br/&gt;        kinematics(dt);&lt;br/&gt;    };&lt;br/&gt;    &lt;br/&gt;    // ...&lt;br/&gt;&lt;br/&gt;    // Private methods&lt;br/&gt;    &lt;br/&gt;    function aging(dt) {&lt;br/&gt;        for (var i = 0; i &lt; particles.length; ) {&lt;br/&gt;            var p = particles[i];&lt;br/&gt;            p.age += dt;&lt;br/&gt;            if (p.age &gt;= p.life)&lt;br/&gt;                kill(i);&lt;br/&gt;            else&lt;br/&gt;                i++;&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    function kill(index) {&lt;br/&gt;        if (particles.length &gt; 1)&lt;br/&gt;            particles[index] = particles[particles.length - 1];&lt;br/&gt;        particles.pop();&lt;br/&gt;    }&lt;br/&gt;    // ...&lt;br/&gt;}&lt;br/&gt;&lt;p&gt;在函数kill()里，用了一个技巧。因为粒子在数组里的次序并不重要，要删除中间一个粒子，只需要复制最末的粒子到那个元素，并用pop()移除最末的粒子就可以。这通常比直接删除数组中间的元素快(在C++中使用数组或std::vector亦是)。&lt;/p&gt;&lt;p&gt;&lt;strong&gt;运动学模拟&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;把本文最重要的两句运动学模拟代码套用至所有粒子就可以。另外，每次模拟会先把引力加速度写入粒子的加速度。这样做是为了将来可以每次改变加速度(续篇会谈这方面)。&lt;/p&gt;function ParticleSystem() {&lt;br/&gt;    // ...&lt;br/&gt;    function applyGravity() {&lt;br/&gt;        for (var i in particles)&lt;br/&gt;            particles[i].acceleration = that.gravity;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    function kinematics(dt) {&lt;br/&gt;        for (var i in particles) {&lt;br/&gt;            var p = particles[i];&lt;br/&gt;            p.position = p.position.add(p.velocity.multiply(dt));&lt;br/&gt;            p.velocity = p.velocity.add(p.acceleration.multiply(dt));&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;    // ...&lt;br/&gt;}&lt;br/&gt;&lt;p&gt;&lt;strong&gt;渲染&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;粒子可以用很多不同方式渲染，例如用圆形、线段(当前位置和之前位置)、影像、精灵等等。本文采用圆形，并按年龄生命比来控制圆形的透明度，代码片段如下:&lt;/p&gt;function ParticleSystem() {&lt;br/&gt;    // ...&lt;br/&gt;    this.render = function(ctx) {&lt;br/&gt;        for (var i in particles) {&lt;br/&gt;            var p = particles[i];&lt;br/&gt;            var alpha = 1 - p.age / p.life;&lt;br/&gt;            ctx.fillStyle = "rgba("&lt;br/&gt;                + Math.floor(p.color.r * 255) + ","&lt;br/&gt;                + Math.floor(p.color.g * 255) + ","&lt;br/&gt;                + Math.floor(p.color.b * 255) + ","&lt;br/&gt;                + alpha.toFixed(2) + ")";&lt;br/&gt;            ctx.beginPath();&lt;br/&gt;            ctx.arc(p.position.x, p.position.y, p.size, 0, Math.PI * 2, true);&lt;br/&gt;            ctx.closePath();&lt;br/&gt;            ctx.fill();&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;    // ...&lt;br/&gt;}&lt;br/&gt;&lt;p&gt;&lt;strong&gt;基本粒子系统完成&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;以下的例子里，每帧会发射一个粒子，其位置在画布中间(200,200)，发射方向是360度，速率为100，生命为1秒，红色、半径为5象素。&lt;/p&gt;&lt;textarea id="basicParticleSystemCode" rows="20" cols="100"&gt;var ps = new ParticleSystem();var dt = 0.01;function sampleDirection() {    var theta = Math.random() * 2 * Math.PI;    return new Vector2(Math.cos(theta), Math.sin(theta));}function step() {    ps.emit(new Particle(new Vector2(200, 200), sampleDirection().multiply(100), 1, Color.red, 5));    ps.simulate(dt);    clearCanvas();    ps.render(ctx);}start("basicParticleSystemCanvas", step);&lt;/textarea&gt;&lt;br /&gt;&lt;button onclick="eval(document.getElementById('basicParticleSystemCode').value)" type="button"&gt;Run&lt;/button&gt;&lt;button onclick="stop();" type="button"&gt;Stop&lt;/button&gt;&lt;br /&gt;&lt;table border="0" style="width: 100%;"&gt; &lt;tbody&gt; &lt;tr&gt; &lt;td&gt;&lt;canvas id="basicParticleSystemCanvas" width="400" height="400"&gt;&lt;/canvas&gt;&lt;/td&gt; &lt;td width="10"&gt;&amp;nbsp;&lt;/td&gt; &lt;td width="100%" valign="top"&gt; &lt;p&gt;&lt;strong&gt;修改代码试试看&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;改变发射位置&lt;/li&gt;&lt;li&gt;向上发射，发射范围在90度内&lt;/li&gt;&lt;li&gt;改变生命&lt;/li&gt;&lt;li&gt;改变半径&lt;/li&gt;&lt;li&gt;每帧发射5个粒子&lt;/li&gt;&lt;/ol&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;&lt;strong&gt;简单碰撞&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;为了说明用数值积分相对于分析解的优点，本文在粒子系统上加简单的碰撞。我们想加入一个需求，当粒子碰到长方形室(可设为整个Canvas大小)的内壁，就会碰撞反弹，碰撞是完全弹性的(perfectly elastic collision)。&lt;/p&gt;&lt;p&gt;在程序设计上，我把这功能用回调方式进行。 ParticleSystem类有一个effectors数组，在进行运动学模拟之前，先执行每个effectors对象的apply()函数:&lt;/p&gt;&lt;p&gt;而长方形室就这样实现:&lt;/p&gt;// ChamberBox.js&lt;br/&gt;function ChamberBox(x1, y1, x2, y2) {&lt;br/&gt;    this.apply = function(particle) {&lt;br/&gt;        if (particle.position.x - particle.size &lt; x1 || particle.position.x + particle.size &gt; x2)&lt;br/&gt;            particle.velocity.x = -particle.velocity.x;&lt;br/&gt;&lt;br/&gt;        if (particle.position.y - particle.size &lt; y1 || particle.position.y + particle.size &gt; y2)&lt;br/&gt;            particle.velocity.y = -particle.velocity.y;&lt;br/&gt;    };&lt;br/&gt;}&lt;br/&gt;&lt;p&gt;这其实就是当侦测到粒子超出内壁的范围，就反转该方向的速度分量。&lt;/p&gt;&lt;p&gt;此外，这例子的主循环不再每次把整个Canvas清空，而是每帧画一个半透明的黑色长方形，就可以模拟动态模糊(motion blur)的效果。粒子的颜色也是随机从两个颜色中取样。&lt;/p&gt;&lt;textarea id="collisionChamberCode" rows="20" cols="100"&gt;var ps = new ParticleSystem();ps.effectors.push(new ChamberBox(0, 0, 400, 400)); // 最重要是多了这语句var dt = 0.01;function sampleDirection(angle1, angle2) {    var t = Math.random();    var theta = angle1 * t + angle2 * (1 - t);    return new Vector2(Math.cos(theta), Math.sin(theta));}function sampleColor(color1, color2) {    var t = Math.random();    return color1.multiply(t).add(color2.multiply(1 - t));}function step() {    ps.emit(new Particle(new Vector2(200, 200), sampleDirection(Math.PI * 1.75, Math.PI * 2).multiply(250), 3, sampleColor(Color.blue, Color.purple), 5));    ps.simulate(dt);    ctx.fillStyle="rgba(0, 0, 0, 0.1)";    ctx.fillRect(0,0,canvas.width,canvas.height);    ps.render(ctx);}start("collisionChamberCanvas", step);&lt;/textarea&gt;&lt;br /&gt;&lt;button onclick="eval(document.getElementById('collisionChamberCode').value)" type="button"&gt;Run&lt;/button&gt;&lt;button onclick="stop();" type="button"&gt;Stop&lt;/button&gt;&lt;br /&gt;&lt;canvas id="collisionChamberCanvas" width="400" height="400"&gt;&lt;/canvas&gt;&lt;p&gt;&lt;strong&gt;互动发射&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;最后一个例子加入互动功能，在鼠标位置发射粒子，粒子方向是按鼠标移动速度再加上一点噪音(noise)。粒子的大小和生命都加入了随机性。&lt;/p&gt;&lt;textarea id="interactiveEmitCode" rows="20" cols="100"&gt;var ps = new ParticleSystem();ps.effectors.push(new ChamberBox(0, 0, 400, 400));var dt = 0.01;var oldMousePosition = Vector2.zero, newMousePosition = Vector2.zero;function sampleDirection(angle1, angle2) {    var t = Math.random();    var theta = angle1 * t + angle2 * (1 - t);    return new Vector2(Math.cos(theta), Math.sin(theta));}function sampleColor(color1, color2) {    var t = Math.random();    return color1.multiply(t).add(color2.multiply(1 - t));}function sampleNumber(value1, value2) {    var t = Math.random();    return value1 * t + value2 * (1 - t);}function step() {    var velocity = newMousePosition.subtract(oldMousePosition).multiply(10);    velocity = velocity.add(sampleDirection(0, Math.PI * 2).multiply(20));        var color = sampleColor(Color.red, Color.yellow);    var life = sampleNumber(1, 2);        var size = sampleNumber(2, 4);    ps.emit(new Particle(newMousePosition, velocity, life, color, size));    oldMousePosition = newMousePosition;        ps.simulate(dt);    ctx.fillStyle="rgba(0, 0, 0, 0.1)";    ctx.fillRect(0,0,canvas.width,canvas.height);    ps.render(ctx);}start("interactiveEmitCanvas", step);canvas.onmousemove = function(e) {    if (e.layerX || e.layerX == 0) { // Firefox         e.target.style.position='relative';        newMousePosition = new Vector2(e.layerX, e.layerY);    }    else        newMousePosition = new Vector2(e.offsetX, e.offsetY);};&lt;/textarea&gt;&lt;br /&gt;&lt;button onclick="eval(document.getElementById('interactiveEmitCode').value)" type="button"&gt;Run&lt;/button&gt;&lt;button onclick="stop();" type="button"&gt;Stop&lt;/button&gt;&lt;br /&gt;&lt;canvas id="interactiveEmitCanvas" width="400" height="400"&gt;&lt;/canvas&gt;&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;本文介绍了最简单的运动学模拟，使用欧拉方法作数值积分，并以此法去实现一个有简单碰撞的粒子系统。本文的精华其实只有两条简单公式(只有两个加数和两个乘数)，希望让读者明白，其实物理模拟可以很简单。虽然本文的例子是在二维空间，但这例子能扩展至三维空间，只须把Vector2换成Vector3。本文完整源代码可&lt;a href="http://files.cnblogs.com/miloyip/particle20100614.zip"&gt;下载&lt;/a&gt;。&lt;/p&gt;&lt;p&gt;续篇会谈及在此基础上加入其他物理现象，有机会再加入其他物理模拟课题。希望各位支持，并给本人更多意见。&lt;/p&gt;&lt;img src="http://www.cnblogs.com/miloyip/aggbug/1758272.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/miloyip/archive/2010/06/14/Kinematics_ParticleSystem.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/05/27/reply_discrete.html</id><title type="text">回应CSDN肖舸《做程序，要“专注”和“客观”》，实验比较各离散采样算法</title><summary type="text">自从肖舸在其CSDN博客上说“拒绝回答博客园等网站网友的问题”，实质上不单是拒绝回答，而且还删去包括一些网友及本人对于纯粹技术探讨的评论。当然每位博主都有自由这么做，但个人认为这对于社区的交流发展有负面影响。为了探讨这个技术问题，本人唯有把回应发表于博客园内。本文会阐述之前的论点，评论肖舸的实现，并进行了兩个实验比较不同算法、实现的优劣之处。 之前的“交流&amp;rd...</summary><published>2010-05-27T15:57:00Z</published><updated>2010-05-27T15:57:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/05/27/reply_discrete.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/05/27/reply_discrete.html"/><content type="text">自从肖舸在其CSDN博客上说“拒绝回答博客园等网站网友的问题”，实质上不单是拒绝回答，而且还删去包括一些网友及本人对于纯粹技术探讨的评论。当然每位博主都有自由这么做，但个人认为这对于社区的交流发展有负面影响。为了探讨这个技术问题，本人唯有把回应发表于博客园内。本文会阐述之前的论点，评论肖舸的实现，并进行了兩个实验比较不同算法、实现的优劣之处。 之前的“交流&amp;rd...</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/05/12/binary_tree_traversal.html</id><title type="text">《编程之美：分层遍历二叉树》的另外两个实现</title><summary type="text">之前重温本书写书评时，也尝试找寻更好的编程解法。今天把另一个问题的实现和大家分享。问题定义给定一棵二叉树，要求按分层遍历该二叉树，即从上到下按层次访问该二叉树(每一层将单独输出一行)，每一层要求访问的顺序为从左到右，并将节点依次编号。下面是一个例子: 输出:节点的定义:书上的解法书上举出两个解法。第一个解法是用递归方式，搜寻并打印某一层的节点，再打印下一层的节点。这方法简单但时间效率不高(但不需要...</summary><published>2010-05-11T16:10:00Z</published><updated>2010-05-11T16:10:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/05/12/binary_tree_traversal.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/05/12/binary_tree_traversal.html"/><content type="text">之前重温本书写书评时，也尝试找寻更好的编程解法。今天把另一个问题的实现和大家分享。问题定义给定一棵二叉树，要求按分层遍历该二叉树，即从上到下按层次访问该二叉树(每一层将单独输出一行)，每一层要求访问的顺序为从左到右，并将节点依次编号。下面是一个例子: 输出:节点的定义:书上的解法书上举出两个解法。第一个解法是用递归方式，搜寻并打印某一层的节点，再打印下一层的节点。这方法简单但时间效率不高(但不需要...</content></entry><entry><id>http://www.cnblogs.com/miloyip/archive/2010/04/26/1720877.html</id><title type="text">在博客里轻松使用LaTeX数学公式</title><summary type="text">笔者最近的博文有不少数学相关内容，发现利用一些网上服务、jQuery和CSS，可以更轻松地在博客里使用\LaTeX语法排版方程式。\LaTeX是基于\TeX的排版系统。而\TeX就是美国著明计算机教授高德纳(Donald E. Knuth)，为了编写他的巨著《计算机程序设计艺术(The Art of Computer Programming)》而设计的系统，对于数学公式的排版支援十分强大。线上La...</summary><published>2010-04-25T17:32:00Z</published><updated>2010-04-25T17:32:00Z</updated><author><name>Milo Yip</name><uri>http://www.cnblogs.com/miloyip/</uri></author><link rel="alternate" href="http://www.cnblogs.com/miloyip/archive/2010/04/26/1720877.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/miloyip/archive/2010/04/26/1720877.html"/><content type="text">笔者最近的博文有不少数学相关内容，发现利用一些网上服务、jQuery和CSS，可以更轻松地在博客里使用\LaTeX语法排版方程式。\LaTeX是基于\TeX的排版系统。而\TeX就是美国著明计算机教授高德纳(Donald E. Knuth)，为了编写他的巨著《计算机程序设计艺术(The Art of Computer Programming)》而设计的系统，对于数学公式的排版支援十分强大。线上La...</content></entry></feed>
