在ActionScript 3中,扩展类会带来哪些负面影响?

8
在我的游戏引擎中,我使用Box2D来实现物理效果。Box2D的命名规范和不良注释破坏了我引擎的一致性和文档完整性,这令人有些沮丧,也会影响到使用者。
我考虑过创建一组Box2D包装类,即扩展每个常见的Box2D对象并重新编写它们的函数以遵循我的引擎的命名约定,并使它们的注释更加清晰、一致。我甚至考虑在某些类之上建立一些内容并添加一些功能(例如b2Vec2类中的像素度量的getters)。
这样做是可以的,但我不确定负面影响是什么,以及这些影响对我的应用程序和游戏的程度。我不确定编译器是否在一定程度上缓解了我的担忧,或者我是否需要考虑添加一些看似不必要的类以提高可读性和一致性。
我有一些猜测:
  • 需要额外的类结构占用更多内存。
  • 由于初始化额外的成员导致创建新对象时的性能影响?
我特别询问运行时的影响。

根据这篇文章,超级调用比普通函数更快。http://jacksondunstan.com/articles/413 - Pier
不是针对你的问题的答案,但你可以考虑编写一个使用组合而非继承的适配器模式版本。在这种情况下,适配器将包含一个它所包装的类的实例,而不是扩展它。 - net.uk.sweet
@net.uk.sweet 这是真的。老实说,我开始考虑重写现有类的部分代码了。 - Marty
5个回答

6

在集成第三方库时,特别是像Box2DAS3这样的端口库,它们保留了父语言的编码和命名约定,而没有完全与目标语言集成,这是一个相当普遍的问题(例如:Box2DAS3使用getFoo()setFoo()而不是.foo getter/setter)。

简单回答你的问题,使用包装类不会对性能产生重大影响;除了在项目中看到的类层次结构外,不会有更多的影响。当然,如果你计时5百万次迭代的循环,你可能会看到一两毫秒的差异,但在正常使用中,你不会注意到它。

"内存消耗增加以容纳额外级别的类结构。" 像任何具有类继承的语言一样,后台将使用vtable,因此你将会有一些内存/性能上的小幅增加,但这是可以忽略的。

"由于初始化额外级别的成员而创建新对象时的性能影响?" 不比正常实例化多,所以除非你正在创建大量的对象,否则不需要担心。

就性能而言,通常情况下你应该没有问题(优先考虑可读性和可用性,除非你确实有问题),但我更愿意将其视为架构问题,并考虑在这方面扩展/修改外部库的类时可能会产生的负面影响,通常分为以下3个方面,具体取决于你要做什么:

  • 修改库
  • 扩展类
  • 与自己的类组合

修改库

由于Box2DAS3是开源的,所以没有任何阻止你随心所欲地重构所有类/函数名称。有时我也会认真考虑这样做。

优点:

  • 你可以修改想要修改的任何内容 - 函数、类等等
  • 你可以添加任何需要的缺失部分(例如:像素-米转换帮助程序)
  • 你可以修复任何潜在的性能问题(我注意到一些东西如果按照“AS3”的方式完成,可以更好、更快)

缺点:

  • 如果你计划保持版本最新,则需要手动合并/转换任何更新和更改。对于流行的库或那些经常变化的库,这可能是一个巨大的痛苦
  • 这非常耗时 - 除了修改之外,你需要很好地理解正在发生的事情,以便在不破坏功能的情况下进行任何更改
  • 如果有多个人使用它,则不能像使用外部文档/示例那样依赖于它,因为内部可能已经发生了更改

扩展类

在这里,你可以简单地创建自己的包装类,它们继承了基本的Box2D类。你可以按照需要添加属性和功能,包括实现将转换为基础类的自己的命名方案(例如MyBox2DClass.foo() 可以简单地作为 Box2DClass.bar() 的一个包装器)。
优点:
- 你只需实现所需的类,并进行必要的更改 - 你的包装类仍然可以在基本的Box2D引擎中使用 - 也就是说,你可以将MyBox2DClass对象传递给接受Box2DClass 的内部方法,你知道它会起作用 - 它是三种方法中最少的工作量
缺点:
- 同样,如果你打算保持版本更新,你需要检查任何更改不会破坏你的类。但通常不是什么大问题 - 如果你创建调用其Box2D等效函数的自己的函数(例如“我应该使用setPos()还是SetPosition()?”),可能会导致类别混乱。即使你正在自己的项目上工作,当你在6个月后回到你的类时,你会忘记的 - 你的类将缺乏一致性(例如,有些函数使用你的命名方法(setPos()),而其他函数使用Box2D的方法(SetPosition())) - 你将被限制在Box2D中;如果你不打算更改,那么没有多少开发量。但是,如果你的类在整个项目中使用,则可能需要大量的开发
通过自己的类进行组合:
你可以创建自己的类,它们内部持有一个Box2D属性(例如,MyPhysicalClass将具有一个b2Body属性)。你可以按照自己的方式自由地实现接口,并且只暴露必要的内容。
优点:
- 你的类更干净,与你的引擎很好地融合在一起。只公开你感兴趣的功能 - 你不会受到Box2D引擎的约束;如果你想切换到Nape等其他引擎,你只需要修改自定义类;你的引擎和游戏的其余部分都不知情。其他开发人员也不需要学习Box2D引擎才能使用它 - 当你在那里时,你甚至可以实现多个引擎,并使用切换或接口在它们之间进行切换。同样,你的引擎和游戏的其余部分都不知情 - 与基于组件的引擎很好地配合使用 - 例如,你可以有一个包含b2Body属性的Box2D组件
缺点:
  • 与仅扩展类不同,你实际上是在你的引擎和Box2D之间创建了一个中间层。理想情况下,在你的自定义类之外,不应该有对Box2D的引用。工作量取决于你在类中需要什么。
  • 额外的间接层;通常不应该成为问题,因为Box2D将直接使用你的Box2D属性,但如果你的引擎经常调用你的函数,这是一种额外的性能损失。

在这三种方法中,我更喜欢采用组合方式,因为它提供了最大的灵活性并保持了你的引擎模块化的本质,即你有你的核心引擎类,并且你可以使用外部库来扩展功能。你还可以轻松切换库,这也是一个巨大的优势。这也是我在我的引擎中采用的技术,并将其扩展到其他类型的库中——例如广告。我有我的引擎Ad类,可以根据需要与Mochi、Kongregate等集成——我的游戏不关心我正在使用什么,这让我在整个引擎中保持了我的编码风格和一致性,同时仍然具有灵活性和可扩展性。

----- 更新于20/9/2013 -----

大型更新时间!所以我回去做了一些关于大小和速度的测试。我使用的类太大了,不能在这里粘贴,所以你可以在http://divillysausages.com/files/TestExtendClass.as下载它。

在其中,我测试了一些类:

  • 一个Empty实例;一个只是继承了Object并实现了一个空的getPostion()函数的类。这将是我们的基准。
  • 一个b2Body实例
  • 一个Box2DExtends实例;一个继承了b2Body并实现了一个仅返回GetPosition()b2Body函数)的getPosition()函数的类
  • 一个Box2DExtendsOverrides实例;一个继承了b2Body并重写了GetPosition()函数(它只是返回super.GetPosition())的类
  • 一个Box2DComposition实例;一个具有b2Body属性和一个返回b2Body'sGetPosition()getPosition()函数的类
  • 一个Box2DExtendsProperty实例;一个继承了b2Body并添加了一个新的Point属性的类
  • 一个Box2DCompositionProperty实例;一个既有b2Body属性又有Point属性的类

所有测试都在独立播放器中进行,在Windows 7上的FP v11.7.700.224版本,使用一台不太好的笔记本电脑。

测试1:大小

AS3有点烦人,如果你调用getSize(),它会给你对象本身的大小,但任何内部属性也是Object,只会导致4字节的增加,因为它们只计算指针。我可以理解为什么它们这样做,只是让得到正确的大小有些棘手。

因此,我转向了flash.sampler包。如果我们对创建对象进行采样,并将NewObjectSample对象中的所有大小相加,我们将得到对象的完整大小(注意:如果您想看到被创建的内容和大小,请在测试文件中注释掉log调用)。

  • Empty的大小为56 // 扩展自Object
  • b2Body的大小为568
  • Box2DExtends的大小为568 // 扩展自b2Body
  • Box2DExtendsOverrides的大小为568 // 扩展自b2Body
  • Box2DComposition的大小为588 // 具有b2Body属性
  • Box2DExtendsProperty的大小为604 // 扩展自b2Body并添加Point属性
  • Box2DCompositionProperty的大小为624 // 具有b2Body和Point属性
这些大小都是以字节为单位的。以下是一些值得注意的点:
- 基本Object大小为40字节,因此仅类而没有其他内容为16字节。 - 添加方法不会增加对象的大小(它们基于类实现),而属性显然会增加大小。 - 仅扩展类并未添加任何内容。 - Box2DComposition的额外20字节来自于16个类和4个指向b2Body属性的指针。 - 对于Box2DExtendsProperty等,您有16个Point类本身,4个指向Point属性的指针,以及每个x和y属性Number的8个字节= 36个字节之间的差异Box2DExtends。
因此,显然大小的差异取决于您添加的属性,但总的来说,相当微不足道。
测试2:创建速度
对于这个,我只是使用了getTimer()和一个循环10000次的循环,它本身循环了10次(因此为100k)以获得平均值。在每组之间调用System.gc()以最小化垃圾收集时间。
- 创建Empty的时间为3.9毫秒(平均值) - 创建b2Body的时间为65.5毫秒(平均值) - 创建Box2DExtends的时间为69.9毫秒(平均值) - 创建Box2DExtendsOverrides的时间为68.8毫秒(平均值) - 创建Box2DComposition的时间为72.6毫秒(平均值) - 创建Box2DExtendsProperty的时间为76.5毫秒(平均值) - 创建Box2DCompositionProperty的时间为77.2毫秒(平均值)
这里没有太多要注意的。扩展/组成类需要稍微更长的时间,但像0.000007ms这样的时间(这是100,000个对象的创建时间)并不值得考虑。
测试3:调用速度
为此,我再次使用了getTimer(),循环执行1000000次,每次循环执行10次(即10m)以获得平均值。在每个集合之间调用System.gc()以最小化由于垃圾收集而产生的时间。所有对象都调用了它们的getPosition()/GetPosition()函数,以查看重载和重定向之间的差异。
  • Empty的getPosition()时间为83.4ms(平均值)//空
  • b2Body的GetPosition()时间为88.3ms(平均值)//正常
  • Box2DExtends的getPosition()时间为158.7ms(平均值)//getPosition()调用GetPosition()
  • Box2DExtendsOverrides的GetPosition()时间为161ms(平均值)//重载调用super.GetPosition()
  • Box2DComposition的getPosition()时间为160.3ms(平均值)//调用this.body.GetPosition()
  • Box2DExtendsProperty的GetPosition()时间为89ms(平均值)//隐式super(即未被重载)
  • Box2DCompositionProperty的getPosition()时间为155.2ms(平均值)//调用this.body.GetPosition()

这个结果有点让我惊讶,因为时间差异大约是两倍(尽管每次调用仍然只有0.000007ms)。延迟似乎完全取决于类继承 - 例如,Box2DExtendsOverrides只是调用了super.GetPosition(),但它的速度却是Box2DExtendsProperty的两倍,后者从其基类中继承了GetPosition()

我想这与函数查找和调用的开销有关,虽然我使用FlexSDK中的swfdump查看了生成的字节码,并且它们是相同的,所以要么它在欺骗我(或没有包含它),要么有些东西我还不知道 :)虽然步骤可能相同,但它们之间的时间可能不同(例如,在内存中,它会跳转到您的类vtable,然后跳转到基类vtable等)

var v:b2Vec2 = b2Body.GetPosition()的字节码非常简单:

getlocal        4
callproperty    :GetPosition (0)
coerce          Box2D.Common.Math:b2Vec2
setlocal3

var v:b2Vec2 = Box2DExtends.getPosition()getPosition()返回GetPosition())时:

getlocal        5
callproperty    :getPosition (0)
coerce          Box2D.Common.Math:b2Vec2
setlocal3

对于第二个例子,它没有显示对GetPosition()的调用,因此我不确定他们是如何解决这个问题的。如果有人想尝试解释一下,测试文件可供下载。
需要注意以下几点:
  • GetPosition()实际上并没有做任何事情;它本质上是一个伪装成函数的getter,这也是为什么“额外类步骤惩罚”看起来如此之大的原因之一
  • 这是在10m的循环中进行的,你不太可能在游戏中这样做。每次调用的惩罚并不值得担心
  • 即使你真的担心这个惩罚,也要记住这是你的代码和Box2D之间的接口;Box2D内部将不受此影响,只有对你的接口的调用会受到影响
总之,我期望从扩展我的一个类中获得相同的结果,所以我不会真的担心它。实现最适合你解决方案的架构即可。

1
这个回答朝着正确的方向发展;你能否再详细阐述一下第三和第四段?那是我最感兴趣的地方,但目前你只是提出了观点,没有任何证明。 - Marty
这是一个足够大的扩展吗? :) - divillysausages
是的,非常好,感谢您抽出时间进行这些测试。 - Marty

1

我知道这个答案不会符合悬赏的要求,因为我太懒了,不想写基准测试。但是,由于我曾经在Flash代码库上工作过,所以也许可以给出一些提示:

avm2是一种动态语言,因此编译器在这种情况下不会进行任何优化。将调用封装为子类调用将产生成本。然而,这个成本将是恒定的时间和很小的空间。

对象创建成本最多也只会受到恒定量的时间和内存的影响。此外,与基本成本相比,时间和数量可能微不足道。

但是,像许多事情一样,魔鬼在细节中。我从未使用过box2d,但如果它执行任何类型的对象池操作,那么事情可能就不太顺利了。通常游戏应该尽量避免在播放时分配对象。因此,请非常小心,不要添加只是为了更漂亮而分配对象的函数。

function addvectors(a:vec,b:vec,dest:vec):void

可能看起来不太好看,但比起其他方式要快得多。
function addvectors(a:vec,b:vec):vec

我希望我理解AS3的语法是正确的。甚至更有用但更丑陋的可能是:
function addvectors(a:Vector.<vec>, b:Vector.<vec>, dest:Vector.<vec>, offset:int, count:int):void

所以我的答案是,如果您只是为了可读性而进行包装,请继续。这是一个小但不断的成本。但是非常非常小心地更改函数的工作方式。

0

我认为扩展不会对性能产生太大的影响。是的,有一些成本,但只要您不使用组合,成本就不会太高。也就是说,您不直接扩展Box2d类,而是创建该类的实例并在类内部使用它。例如:

public class Child extends b2Body {
    public function Child() {
        // do some stuff here
    }
}

用这个代替

public class Child  {
    private var _body:b2Body;
    public function Child() {
        ...
        _body = _world.CreateBody(...);
        ...
    }
}

我想您知道,创建的对象越少,性能就越好。只要保持创建实例的数量不变,您就会拥有相同的性能。

从另一个角度来看: a) 添加一层抽象可能会对Box2d产生很大影响。如果您在团队中工作,这可能是一个问题,因为其他开发人员应该了解您的命名方式。 b) 谨防“中间人”代码异味。通常当您开始包装已经存在的功能时,最终会得到一些仅仅是委托者的类。


0

这里有一些很好的答案,但我也想发表我的看法。

你必须认识到两个不同的概念:当你扩展一个类和当你实现一个类时。

这是一个扩展MovieClip的例子。

public class TrickedOutClip extends MovieClip {
   private var rims = 'extra large'
   public function TrickedOutClip() { 
     super();
   }
}

这是一个实现MovieClip的例子

public class pimpMyClip {
   private var rims = 'extra large';
   private var pimpedMovieClip:MovieClip;
   public function pimpMyClip() { 
     pimpedMovieClip = new MovieClip();
     pimpedMovieClip.bling = rims;
   }

 public function getPimpedClip() { 
     return pimpedMovieClip;
   }
}

我认为你可能不想扩展这些box2D类,而是要实现它们。以下是一个大致的概述:

 public class myBox2DHelper {
   private var box2d = new Box2D(...);

  public function MyBox2DHelper(stage) {

  }

   public function makeBox2DDoSomeTrickyThing(varA:String, varB:Number) { 
       // write your custom code here
     }

   public function makeBox2DDoSomethingElse(varC:MovieClip) { 
       // write your custom code here
     }
  }

祝你好运。


我认为 Marty 很清楚你在说什么。他关心的是性能而不是实现细节。 - Pier

0

我不知道实例化时间是否会有很大的影响,但我会用不同的方式回答你的问题:你有其他选项吗?它们似乎会做得更好吗?

Jackson Dunstan 做了一个关于函数性能的漂亮基准测试:http://jacksondunstan.com/articles/1820

总结一下:

因此,如果您不想使用继承,也许您将不得不用静态调用来替换它,这对性能来说是不好的。 个人建议是扩展这些类并在运行时添加所有需要的对象的急切实例化:如果是大量的话,则可以制作一个漂亮的加载屏幕...

此外,还要查看应用程序后字节代码优化,如 apparat:http://code.google.com/p/apparat/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接