Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are the negative impacts of extending classes in ActionScript 3?

In my game engine I use Box2D for physics. Box2D's naming conventions and poor commenting ruin the consistent and well documented remainder of my engine which is a little frustrating and presents poorly when you're using it.

I've considered making a set of wrapper classes for Box2D. That is, classes which extend each of the common Box2D objects and have their functions rewritten to follow the naming conventions of the rest of my engine, and to have them more clearly and consistently commented. I have even considered building ontop of some of the classes and adding some bits and pieces (like getters for pixel-based measurements in the b2Vec2 class).

This is fine but I am not 100% sure what the negative impacts of this would be and the degree to which those would affect my applications and games. I'm not sure if the compiler alleviates some of my concerns to a degree or whether I do need to be considerate when adding somewhat unnecessary classes for the sake of readability and consistency.

I have some suspicions:

  • More memory consumption to accommodate the extra level of class structure.
  • Performance impact when creating new objects due to initializing an extra level of members?

I am asking specifically about runtime impacts.

like image 998
Marty Avatar asked Sep 13 '13 00:09

Marty


1 Answers

This is a pretty common problem when it comes to integrating third party libraries, especially libraries that are ports (as Box2DAS3 is), where they keep the coding and naming conventions of the parent language rather than fully integrating with the destination language (case in point: Box2DAS3 using getFoo() and setFoo() instead of a .foo getter/setter).

To answer your question quickly, no, there will be no significant performance impact with making wrapper classes; no more than you'll see in the class hierarchy in your own project. Sure, if you time a loop of 5 million iterations, you might see a millisecond or two of difference, but in normal usage, you won't notice it.

"More memory consumption to accommodate the extra level of class structure." Like any language that has class inheritence, a vtable will be used behind the scenes, so you will have a small increase in memory/perf, but it's negligible.

"Performance impact when creating new objects due to initializing an extra level of members?" No more than normal instantiation, so not something to worry about unless you're creating a huge amount of objects.

Performance wise, you should generally have no problem (favour readability and usability over performance unless you actually have a problem with it), but I'd look at it more as an architectural problem and, with that in mind, what I would consider to be a negative impact of extending/modifying classes of an external library generally fall into 3 areas, depending on what you want to do:

  • Modify the library
  • Extend the classes
  • Composition with your own classes

Modify the libary

As Box2DAS3 is open source, there's nothing stopping you jumping in and refactoring all the class/function names to your hearts content. I've seriously considered doing this at times.

Pros:

  • You can modify what you want - functions, classes, you name it
  • You can add any missing pieces that you need (e.g. pixel-meters conversion helpers)
  • You can fix any potential performance issues (I've noticed a few things that could be done better and faster if they were done in an "AS3" way)

Cons:

  • If you plan to keep your version up to date, you'll need to manually merge/convert any updates and changes. For popular libraries, or those that change a lot, this can be a huge pain
  • It's very time-consuiming - aside from modifications, you'll need a good understanding on what's going on so you can make any changes without breaking functionality
  • If there's multiple people working with it, they can't rely as much on external documentation/examples, as the internals might have changed

Extend the classes

Here, you simply make your own wrapper classes, which extend the base Box2D classes. You can add properties and functions as you want, including implementing your own naming scheme which translates to the base class (e.g. MyBox2DClass.foo() could simply be a wrapper for Box2DClass.bar())

Pros:

  • You implement just the classes you need, and make just the changes necessary
  • Your wrapper classes can still be used in the base Box2D engine - i.e. you can pass a MyBox2DClass object to an internal method that takes a Box2DClass and you know it'll work
  • It's the least amount of work, out of all three methods

Cons:

  • Again, if you plan to keep your version up to date, you'll need to check that any changes don't break your classes. Normally not much of a problem, though
  • Can introduce confusion into the class, if you create your own functions that call their Box2D equivalent (e.g. "Should I use setPos() or SetPosition()?). Even if you're working on your own, when you come back to your class in 6 months, you'll have forgotten
  • Your classes will lack coherence and consistency (e.g. some functions using your naming methodology (setPos()) while others use that of Box2D (SetPosition()))
  • You're stuck with Box2D; you can't change physics engines without a lot of dev, depending on how your classes are used throughout the project. This might not be such a big deal if you don't plan on switching

Composition with your own classes

You make your own classes, which internally hold a Box2D property (e.g. MyPhysicalClass will have a property b2Body). You're free to implement your own interface as you wish, and only what's necessary.

Pros:

  • Your classes are cleaner and fit in nicely with your engine. Only functions that you're interested in are exposed
  • You're not tied to the Box2D engine; if you want to switch to Nape, for example, you only need to modify your custom classes; the rest of your engine and games are oblivious. Other developers also don't need to learn the Box2D engine to be able to use it
  • While you're there, you can even implement multiple engines, and switch between them using a toggle or interfaces. Again, the rest of your engine and games are oblivious
  • Works nicely with component based engines - e.g. you can have a Box2DComponent that holds a b2Body property

Cons:

  • More work than just extending the classes, as you're essentially creating an intermediary layer between your engine and Box2D. Ideally, outside of your custom classes, there shouldn't be a reference to Box2D. The amount of work depends on what you need in your class
  • Extra level of indirection; normally it shouldn't be a problem, as Box2D will use your Box2D properties directly, but if your engine is calling your functions a lot, it's an extra step along the way, performance wise

Out of the three, I prefer to go with composition, as it gives the most flexibility and keeps the modular nature of your engine intact, i.e. you have your core engine classes, and you extend functionality with external libraries. The fact that you can switch out libraries with minimal effort is a huge plus as well. This is the technique that I've employed in my own engine, and I've also extended it to other types of libraries - e.g. Ads - I have my engine Ad class, that can integrate with Mochi, Kongregate, etc as needed - the rest of my game doesn't care what I'm using, which lets me keep my coding style and consistency throughout the engine, whilst still being flexible and modular.

----- Update 20/9/2013 -----

Big update time! So I went back to do some testing on size and speed. The class I used is too big to paste here, so you can download it at http://divillysausages.com/files/TestExtendClass.as

In it, I test a number of classes:

  • An Empty instance; a Class that just extends Object and implements an empty getPostion() function. This will be our benchmark
  • A b2Body instance
  • A Box2DExtends instance; a Class that extends b2Body and implements a function getPosition() that just returns GetPosition() (the b2Body function)
  • A Box2DExtendsOverrides instance; a Class that extends b2Body and overrides the GetPosition() function (it simply returns super.GetPosition())
  • A Box2DComposition instance; a Class that has a b2Body property and a getPosition() function that returns the b2Body's GetPosition()
  • A Box2DExtendsProperty instance; a Class that extends b2Body and adds a new Point property
  • A Box2DCompositionProperty instance; a Class that has both a b2Body property and a Point property

All tests were done in the standalone player, FP v11.7.700.224, Windows 7, on a not-great laptop.

Test1: Size

AS3 is a bit annoying in that if you call getSize(), it'll give you the size of the object itself, but any internal properties that are also Objects will just result in a 4 byte increase as they're only counting the pointer. I can see why they do this, it just makes it a bit awkward to get the right size.

Thus I turned to the flash.sampler package. If we sample the creation of our objects, and add up all the sizes in the NewObjectSample objects, we'll get the full size of our object (NOTE: if you want to see what's created and the size, comment in the log calls in the test file).

  • Empty's size is 56 // extends Object
  • b2Body's size is 568
  • Box2DExtends's size is 568 // extends b2Body
  • Box2DExtendsOverrides's size is 568 // extends b2Body
  • Box2DComposition's size is 588 // has b2Body property
  • Box2DExtendsProperty's size is 604 // extends b2Body and adds Point property
  • Box2DCompositionProperty's size is 624 // has b2Body and Point properties

These sizes are all in bytes. Some points worth noting:

  • The base Object size is 40 bytes, so just the class and nothing else is 16 bytes.
  • Adding methods doesn't increase the size of the object (they're implemented on a class basis anyway), while properties obviously do
  • Just extending the class didn't add anything to it
  • The extra 20 bytes for Box2DComposition come from 16 for the class and 4 for the pointer to the b2Body property
  • For Box2DExtendsProperty etc, you have 16 for the Point class itself, 4 for the pointer to the Point property, and 8 for each of the x and y property Numbers = 36 bytes difference between that and Box2DExtends

So obviously the difference in size depends on the properties that you add, but all in all, pretty negligible.

Test 2: Creation Speed

For this, I simply used getTimer(), with a loop of 10000, itself looped 10 (so 100k) times to get the average. System.gc() was called between each set to minimise time due to garbage collection.

  • Empty's time for creation is 3.9ms (av.)
  • b2Body's time for creation is 65.5ms (av.)
  • Box2DExtends's time for creation is 69.9ms (av.)
  • Box2DExtendsOverrides's time for creation is 68.8ms (av.)
  • Box2DComposition's time for creation is 72.6ms (av.)
  • Box2DExtendsProperty's time for creation is 76.5ms (av.)
  • Box2DCompositionProperty's time for creation is 77.2ms (av.)

There's not a whole pile to note here. The extending/composition classes take slightly longer, but it's like 0.000007ms (this is the creation time for 100,000 objects), so it's not really worth considering.

Test 3: Call Speed

For this, I used getTimer() again, with a loop of 1000000, itself looped 10 (so 10m) times to get the average. System.gc() was called between each set to minimise time due to garbage collection. All the objects had their getPosition()/GetPosition() functions called, to see the difference between overriding and redirecting.

  • Empty's time for getPosition() is 83.4ms (av.) // empty
  • b2Body's time for GetPosition() is 88.3ms (av.) // normal
  • Box2DExtends's time for getPosition() is 158.7ms (av.) // getPosition() calls GetPosition()
  • Box2DExtendsOverrides's time for GetPosition() is 161ms (av.) // override calls super.GetPosition()
  • Box2DComposition's time for getPosition() is 160.3ms (av.) // calls this.body.GetPosition()
  • Box2DExtendsProperty's time for GetPosition() is 89ms (av.) // implicit super (i.e. not overridden)
  • Box2DCompositionProperty's time for getPosition() is 155.2ms (av.) // calls this.body.GetPosition()

This one surprised me a bit, with the difference between the times being ~2x (though that's still 0.000007ms per call). The delay seems entirely down to the class inheritence - e.g. Box2DExtendsOverrides simply calls super.GetPosition(), yet is twice as slow as Box2DExtendsProperty, which inherits GetPosition() from its base class.

I guess it has to do with the overhead of function lookups and calling, though I took a look at the generated bytecode using swfdump in the FlexSDK, and they're identical, so either it's lying to me (or doesn't include it), or there's something I'm missing :) While the steps might be the same, the time between them probably isn't (e.g. in memory, it's jumping to your class vtable, then jumping to the base class vtable, etc)

The bytecode for var v:b2Vec2 = b2Body.GetPosition() is simply:

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

whilst var v:b2Vec2 = Box2DExtends.getPosition() (getPosition() returns GetPosition()) is:

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

For the second example, it doesn't show the call to GetPosition(), so I'm not sure how they're resolving that. The test file is available for download if someone wants to take a crack at explaining it.

Some points to keep in mind:

  • GetPosition() doesn't really do anything; it's essentially a getter disguised as a function, which is one reason why the "extra class step penalty" appears so big
  • This was on a loop of 10m, which you're unlikely to doing in your game. The per-call penalty isn't really worth worrying about
  • Even if you do worry about the penalty, remember that this is the interface between your code and Box2D; the Box2D internals will be unaffected by this, only the calls to your interface

All-in-all, I'd expect the same results from extending one of my own classes, so I wouldn't really worry about it. Implement the architecture that works the best for your solution.

like image 88
divillysausages Avatar answered Oct 02 '22 19:10

divillysausages