  
Welcome to Ventspace! Most posts here are delayed copies of posts from the real Ventspace.
| Tuesday, November 27, 2007 |
 A Case for Trivial Setters and Getters |
Posted - 11/27/2007 2:17:12 PM | I'll probably get a photo of my office up soon. I have gotten some new hardware, and the place is quite impressive. It's unfortunate that PS3s don't stack, though. It would've saved me a lot of space.
A Case for Trivial Setters and Getters
I've historically been strongly opposed to writing trivial getters and setters. Trivial means that there's no data validation in either direction, nor is there any kind of intermediate work (computation, locks, whatever). It's just a direct read or write. The usual reason that people do this is because they've been told it's encapsulation and makes good object oriented code. I've already talked about this type of behavior once, when discussing information hiding. I've advised people against doing this sort of thing before, as have other people whose design intuition I tend to trust. As a result, I'm rather uncomfortable in breaking from this viewpoint, but I think there's a strong argument from the other side which shouldn't be ignored.
A typical piece of software nowadays involves a lot of state from a lot of objects being read and modified all the time. Games suffer from this particular problem quite a bit, due to the sheer amount of objects involved in your typical game world. Large inheritance hierarchies (especially deep ones) exacerbate the situation even further, by seriously obscuring all of the different available state and locations where state can be changed. So then, how do you tackle a data corruption bug that is caused by legal code? (That is, not caused by a buffer overrun, bad pointer, or other serious safety failure.) If somebody is handing you a technically legal but completely undesired value, how do you find it?
This is a very real problem, that comes up quite a bit. If your game is in meters and set on earth, you probably don't want gravity to be set to 1.10. In fact, you probably want it around 9.81. So if you find that your gravity HAS been set to 1.10, perhaps by accident while trying to set gravity scaling, you need to track that down. Since gravity is a fairly universal parameter, you can probably get away with a memory breakpoint. It'll be hideously slow, since gravity is probably accessed pretty often and you're adding a page fault to every read, but it will single out that bad write eventually. That won't work if you're not sure of the exact memory location of the desired member, though. In a situation with objects being created and destroyed, you may not know more than the class of the object you're after (and maybe its name, if you're lucky). If you're modifying that variable directly from all over the place, and you don't know exactly where it is, tracking down the point where the corruption occurs will probably be a hellish exercise.
Then major advantage of a trivial setter or getter, then, is that you can inject a breakpoint, or even other code to help catch the bogus value you're looking for. You can do it at source level instead of messing with memory, and it won't drastically affect application performance the way a memory breakpoint does. The optimizer should inline the functions in your final build, so you lose nothing when speed actually matters. It's now a lot easier to debug where things are going awry, and all it takes is a couple extra minor lines of code per member. Compared to the overhead introduced by your typical C++ reflection framework, that's nothing at all.
C# makes this a lot easier by allowing transparent refactoring from direct access to properties, which give you all of the benefits without affecting client code. Unfortunately C++ doesn't, and since the people behind C++ have generally come down against properties, it probably never will. (This paper has some rationale. It's not unreasonable, but it is blind to several advantages of properties and the presented solution is a gigantic pain.) If you want to switch from direct access to a setter/getter pair in C++, the refactoring effort involved is potentially quite large, and equally so in the reverse direction if you wanted to make the change temporarily.
I'm still not fully sold on the trivial setters and getters, but the argument for them is very compelling as soon as you start dealing with any important data object that is used a lot. At work, we're supposed to favor the setters and getters for everything for just this reason, and there are frequently asserts in both functions. Those asserts and trivial functions are really rather useful in day-to-day bug fixing, and so I'm nearly ready to change my tune on this one.
| |
| Sunday, November 11, 2007 |
 A Follow Up on the Preprocessor |
Posted - 11/11/2007 12:12:19 PM | SlimDX is supposed to be a stupidly simple, thin wrapper around DX objects and functions. It's supposed to do damn near no work and as a result it's supposed to be easy to get it right. It turns out that those things don't hold up all the time in practice. Sometimes it's true, but sometimes working on SlimDX involves quite a lot of subtleties, details and generally irritating problems. This entry details two of those irritating problems, but there are quite a few more, and some of those continue to create bugs. I have to give props to Tom Miller for doing this the first time around, because it's just amazing how touchy some of this stuff is.
A Follow Up on the Preprocessor
Some of the guys on reddit picked this up and were discussing a couple points that I wanted to clarify. (Although the GameDev commenters here didn't harass me at all. Do you guys just assume I'm always right?) I skipped over a lot of details in the SlimDX code sample, and didn't explain exactly why macros had been the solution. There are a number of subtleties involved that complicate the situation quite a bit.
In the example, I essentially used macros to create fifty nearly identical types with basically the same code. Thing is, we already have facilitities to do exactly that -- templates and generics. Unfortunately, neither one is actually a workable substitute in this case. Generics in .NET can't be parameterized on values, only on types. Making a generic exception class that is specialized for each return code would require us to define a unique dummy type for each exception code. Once we've done that, we're basically back where we started. Not only that, we're back but with much longer and more unwieldy names, ones that hide the actual error at first glance. You're forced to skip to the end of an absurdly long type name to see what happened, and since .NET can't deal with typedefs, there's no way around it. Complicating the external interface of a library just to make it easier to write the library itself is not generally a good idea.
Okay, fine, generics are out. What about templates? They can be parameterized on the actual return code values. The problem here is that templates are not really a familiar thing to .NET code. They're expanded out by the C++ compiler and emitted as entirely separate classes. What you'll see in the client code is exceptions with names of the form GraphicsException<2933548>. Now, I don't know if it's possible to handle an exception with a name like that from C#. Even if it is, good luck debugging when an unexpected exception of that form pops up. Templates in .NET are a very problematic construct, and what makes it even worse is that Visual Studio is completely unable to understand what is going on when templates have been exposed to C# code. I know because we already used templates once in SlimDX. It took three months to fix the problems caused by it.
SlimDX contains several dozen classes which wrap COM objects. These wrappers all share some very similar code; the only difference is the exact type of the internal pointer. What we did was to write a class called DirectXObject, which takes the native COM type as a template parameter. (Generics weren't an option because generics can't be parameterized on native types.) A typical class that wraps a COM object will look like:
public ref class Device : DirectXObject<IDirect3DDevice9>
In some sense, it's actually a mutated version of CRTP. Anyway, the base class provides some useful functions, like the implementations for Dispose() and IsDisposed, a virtual destructor, and the actual COM pointer as well as a public property to expose it to clients. It worked out quite well, until we started to hear some confusion from people working with it early on. Apparently none of the classes had Dispose as a member, which was really quite bizarre.
The samples were using Dispose. I knew it was there and worked fine. The problem was that VC# had completely tripped over the template definitions, and neither Intellisense nor Object Browser could understand what was going. Even the type disassembler was broken. All of the functions exposed from DirectXObject were simply missing from the reflection tools. And since people working with C# tend to assume those tools are completely correct, it was a serious problem. I tried probably at least a half dozen hacks to try and force VC# into seeing what was going on. I injected extra classes into the hierarchy, I played with generic/template combinations, I tried multiple inheritance hacks, and I did every other crazy thing I could think of. In the end, only one thing worked. The functions only showed up in VC# if they were also present as an explicit part of the derived class.
So now I had to make a copy of the functions inside every COM wrapper class in SlimDX. You've probably already figured out that I patched it up with a macro. These SlimDX classes all have a line that says DXOBJECT_FUNCTIONS;. This line is a macro that declares and defines a number of functions that override functions in DirectXObject. It means a few functions are virtual that don't need to be, and a bit of duplicated code across every class. I'm still fairly annoyed about the whole thing, but it was necessary in order to make the library work, and without macros we would've had some very serious problems -- I don't think we could've come up with a reasonable solution at all.
The preprocessor is a last ditch solution with a lot of irritating limitations, but sometimes you need a last ditch solution, because you've exhausted all your other alternatives. It's those times when I appreciate having that last option available in my toolbox.
| |
 I Like the C Preprocessor |
Posted - 11/9/2007 3:08:00 PM | I have a message for any of you who get the bright idea to bring up Lisp macros in response to this entry:
Shut the hell up.
Lisp macros are an entirely different game and really only work because of the way Lisp is set up in the first place. They're not relevant and certainly aren't supportable in mainstream C-based languages.
I Like the C Preprocessor
The C preprocessor gets a lot of hate. And I mean a lot of hate. Just look at Java; their reactionary horror of the whole thing was so exaggerated that they entirely forgot that conditional compilation is actually handy, and simply refused to provide anything that seemed like a preprocessor. C# was a little less blind, giving us proper conditional compilation and a few other tricks at least. I'm not talking about how much I like conditional compilation, though. Any bonehead can figure out that it's a useful thing to have. When I say I like the C preprocessor, I mean it. Textual replacement, includes, macros -- the whole deal. Are they perfect? Of course not? Do they get abused? Hell yes. Despite all that, though, I keep coming back to just how useful the damn things are. Just take a look at the heart of the internal exceptions architecture in SlimDX:
#pragma once
using namespace System;
using namespace System::Runtime::Serialization;
#include "../Exceptions.h"
#include <dxerr.h>
namespace SlimDX
{
namespace Direct3D9
{
[Serializable]
public ref class GraphicsException : public SlimDX::DirectXException
{
private:
static GraphicsException()
{
LastError = S_OK;
EnableForDeviceState = true;
EnableForStillDrawing = true;
}
protected:
GraphicsException(SerializationInfo^ info, StreamingContext context) : DirectXException(info, context)
{ }
public:
GraphicsException() : DirectXException(E_FAIL, "A Direct3D exception occurred.")
{ }
GraphicsException(String^ message) : DirectXException(E_FAIL, message)
{ }
GraphicsException(int errorCode ) : DirectXException( errorCode, gcnew String( DXGetErrorDescription( errorCode ) ) )
{ }
GraphicsException(int errorCode, String^ message) : DirectXException( errorCode, message )
{ }
GraphicsException(String^ message, Exception^ innerException) : DirectXException( message, innerException )
{ }
static property int LastError;
static property bool EnableForDeviceState;
static property bool EnableForStillDrawing;
static GraphicsException^ GetExceptionFromHResult( HRESULT hr );
static void CheckHResult( HRESULT hr );
static void CheckHResult( HRESULT hr, String^ dataKey, Object^ dataValue );
};
#define DEFINE_GRAPHICS_EXCEPTION( ExName, ErrorCode ) \
[Serializable] \
public ref class ExName ## Exception : public GraphicsException \
{ \
protected: \
ExName ## Exception (SerializationInfo^ info, StreamingContext context) : GraphicsException(info, context) { }\
public: \
ExName ## Exception () : GraphicsException( ErrorCode ) { } \
ExName ## Exception ( String^ message ) : GraphicsException( ErrorCode, message ) { } \
ExName ## Exception ( String^ message, Exception^ innerException ) : GraphicsException( message, innerException ) { } \
}
#define DEFINE_CUSTOM_GRAPHICS_EXCEPTION( ExName, ErrorCode, Message ) \
[Serializable] \
public ref class ExName ## Exception : public GraphicsException \
{ \
protected: \
ExName ## Exception (SerializationInfo^ info, StreamingContext context) : GraphicsException(info, context) { }\
public: \
ExName ## Exception () : GraphicsException( ErrorCode, Message ) { } \
ExName ## Exception ( String^ message ) : GraphicsException( ErrorCode, message ) { } \
ExName ## Exception ( String^ message, Exception^ innerException ) : GraphicsException( message, innerException ) { } \
}
DEFINE_GRAPHICS_EXCEPTION( WrongTextureFormat, D3DERR_WRONGTEXTUREFORMAT );
DEFINE_GRAPHICS_EXCEPTION( UnsupportedColorOperation, D3DERR_UNSUPPORTEDCOLOROPERATION );
DEFINE_GRAPHICS_EXCEPTION( UnsupportedColorArgument, D3DERR_UNSUPPORTEDCOLORARG );
DEFINE_GRAPHICS_EXCEPTION( UnsupportedAlphaOperation, D3DERR_UNSUPPORTEDALPHAOPERATION );
DEFINE_GRAPHICS_EXCEPTION( UnsupportedAlphaArgument, D3DERR_UNSUPPORTEDALPHAARG );
DEFINE_GRAPHICS_EXCEPTION( TooManyOperations, D3DERR_TOOMANYOPERATIONS );
DEFINE_GRAPHICS_EXCEPTION( ConflictingTextureFilter, D3DERR_CONFLICTINGTEXTUREFILTER );
DEFINE_GRAPHICS_EXCEPTION( UnsupportedFactorValue, D3DERR_UNSUPPORTEDFACTORVALUE );
DEFINE_GRAPHICS_EXCEPTION( ConflictingTexturePalette, D3DERR_CONFLICTINGTEXTUREPALETTE );
DEFINE_GRAPHICS_EXCEPTION( DriverInternalError, D3DERR_DRIVERINTERNALERROR );
DEFINE_GRAPHICS_EXCEPTION( NotFound, D3DERR_NOTFOUND );
DEFINE_GRAPHICS_EXCEPTION( MoreData, D3DERR_MOREDATA );
DEFINE_GRAPHICS_EXCEPTION( DeviceLost, D3DERR_DEVICELOST );
DEFINE_GRAPHICS_EXCEPTION( DeviceNotReset, D3DERR_DEVICENOTRESET );
DEFINE_GRAPHICS_EXCEPTION( NotAvailable, D3DERR_NOTAVAILABLE );
DEFINE_GRAPHICS_EXCEPTION( OutOfVideoMemory, D3DERR_OUTOFVIDEOMEMORY );
DEFINE_GRAPHICS_EXCEPTION( InvalidDevice, D3DERR_INVALIDDEVICE );
DEFINE_GRAPHICS_EXCEPTION( InvalidCall, D3DERR_INVALIDCALL );
DEFINE_GRAPHICS_EXCEPTION( DriverInvalidCall, D3DERR_DRIVERINVALIDCALL );
DEFINE_GRAPHICS_EXCEPTION( WasStillDrawing, D3DERR_WASSTILLDRAWING );
DEFINE_GRAPHICS_EXCEPTION( CannotModifyIndexBuffer, D3DXERR_CANNOTMODIFYINDEXBUFFER );
DEFINE_GRAPHICS_EXCEPTION( InvalidMesh, D3DXERR_INVALIDMESH );
DEFINE_GRAPHICS_EXCEPTION( CannotAttributeSort, D3DXERR_CANNOTATTRSORT );
DEFINE_GRAPHICS_EXCEPTION( SkinningNotSupported, D3DXERR_SKINNINGNOTSUPPORTED );
DEFINE_GRAPHICS_EXCEPTION( TooManyInfluences, D3DXERR_TOOMANYINFLUENCES );
DEFINE_GRAPHICS_EXCEPTION( InvalidData, D3DXERR_INVALIDDATA );
DEFINE_GRAPHICS_EXCEPTION( LoadedMeshHasNoData, D3DXERR_LOADEDMESHASNODATA );
DEFINE_GRAPHICS_EXCEPTION( DuplicateNamedFragment, D3DXERR_DUPLICATENAMEDFRAGMENT );
DEFINE_GRAPHICS_EXCEPTION( CannotRemoveLastItem, D3DXERR_CANNOTREMOVELASTITEM );
DEFINE_GRAPHICS_EXCEPTION( BadObject, D3DXFERR_BADOBJECT );
DEFINE_GRAPHICS_EXCEPTION( BadValue, D3DXFERR_BADVALUE );
DEFINE_GRAPHICS_EXCEPTION( BadType, D3DXFERR_BADTYPE );
DEFINE_GRAPHICS_EXCEPTION( XNotFound, D3DXFERR_NOTFOUND );
DEFINE_GRAPHICS_EXCEPTION( NotDoneYet, D3DXFERR_NOTDONEYET );
DEFINE_GRAPHICS_EXCEPTION( FileNotFound, D3DXFERR_FILENOTFOUND );
DEFINE_GRAPHICS_EXCEPTION( ResourceNotFound, D3DXFERR_RESOURCENOTFOUND );
DEFINE_GRAPHICS_EXCEPTION( BadResource, D3DXFERR_BADRESOURCE );
DEFINE_GRAPHICS_EXCEPTION( BadFileType, D3DXFERR_BADFILETYPE );
DEFINE_GRAPHICS_EXCEPTION( BadFileVersion, D3DXFERR_BADFILEVERSION );
DEFINE_GRAPHICS_EXCEPTION( BadFileFloatSize, D3DXFERR_BADFILEFLOATSIZE );
DEFINE_GRAPHICS_EXCEPTION( BadFile, D3DXFERR_BADFILE );
DEFINE_GRAPHICS_EXCEPTION( ParseError, D3DXFERR_PARSEERROR );
DEFINE_GRAPHICS_EXCEPTION( BadArraySize, D3DXFERR_BADARRAYSIZE );
DEFINE_GRAPHICS_EXCEPTION( BadDataReference, D3DXFERR_BADDATAREFERENCE );
DEFINE_GRAPHICS_EXCEPTION( NoMoreObjects, D3DXFERR_NOMOREOBJECTS );
DEFINE_GRAPHICS_EXCEPTION( NoMoreData, D3DXFERR_NOMOREDATA );
DEFINE_GRAPHICS_EXCEPTION( BadCacheFile, D3DXFERR_BADCACHEFILE );
DEFINE_CUSTOM_GRAPHICS_EXCEPTION( OutOfMemory, E_OUTOFMEMORY, "Out of memory." );
DEFINE_CUSTOM_GRAPHICS_EXCEPTION( Direct3D9NotFound, E_FAIL, "Direct3D 9 not found." );
DEFINE_CUSTOM_GRAPHICS_EXCEPTION( Direct3DX9NotFound, E_FAIL, "Direct3DX 9 not found." );
DEFINE_CUSTOM_GRAPHICS_EXCEPTION( Direct3DNotInitialized, E_FAIL, "Direct3D not initialized." );
inline GraphicsException^ GraphicsException::GetExceptionFromHResult( HRESULT hr )
{
GraphicsException^ ex;
# define GENERATE_EXCEPTION(errCase, type) \
case errCase:\
ex = gcnew type ## Exception ();\
break;
# define GENERATE_EXCEPTION_IF(errCase, type, condition) \
case errCase:\
if(condition)\
ex = gcnew type ## Exception ();\
else\
return nullptr;\
break;
switch( hr )
{
GENERATE_EXCEPTION_IF(D3DERR_DEVICELOST, DeviceLost, GraphicsException::EnableForDeviceState);
GENERATE_EXCEPTION_IF(D3DERR_DEVICENOTRESET, DeviceNotReset, GraphicsException::EnableForDeviceState);
GENERATE_EXCEPTION_IF(D3DERR_WASSTILLDRAWING, WasStillDrawing, GraphicsException::EnableForStillDrawing);
GENERATE_EXCEPTION(D3DERR_WRONGTEXTUREFORMAT, WrongTextureFormat);
GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDCOLOROPERATION, UnsupportedColorOperation);
GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDCOLORARG, UnsupportedColorArgument);
GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDALPHAOPERATION, UnsupportedAlphaOperation);
GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDALPHAARG, UnsupportedAlphaArgument);
GENERATE_EXCEPTION(D3DERR_TOOMANYOPERATIONS, TooManyOperations);
GENERATE_EXCEPTION(D3DERR_CONFLICTINGTEXTUREFILTER, ConflictingTextureFilter);
GENERATE_EXCEPTION(D3DERR_UNSUPPORTEDFACTORVALUE, UnsupportedFactorValue);
GENERATE_EXCEPTION(D3DERR_CONFLICTINGTEXTUREPALETTE, ConflictingTexturePalette);
GENERATE_EXCEPTION(D3DERR_DRIVERINTERNALERROR, DriverInternalError);
GENERATE_EXCEPTION(D3DERR_NOTFOUND, NotFound);
GENERATE_EXCEPTION(D3DERR_MOREDATA, MoreData);
GENERATE_EXCEPTION(D3DERR_NOTAVAILABLE, NotAvailable);
GENERATE_EXCEPTION(D3DERR_OUTOFVIDEOMEMORY,OutOfVideoMemory);
GENERATE_EXCEPTION(D3DERR_INVALIDDEVICE,InvalidDevice);
GENERATE_EXCEPTION(D3DERR_INVALIDCALL,InvalidCall);
GENERATE_EXCEPTION(D3DERR_DRIVERINVALIDCALL,DriverInvalidCall);
GENERATE_EXCEPTION(D3DXERR_CANNOTMODIFYINDEXBUFFER, CannotModifyIndexBuffer);
GENERATE_EXCEPTION(D3DXERR_INVALIDMESH, InvalidMesh);
GENERATE_EXCEPTION(D3DXERR_CANNOTATTRSORT, CannotAttributeSort);
GENERATE_EXCEPTION(D3DXERR_SKINNINGNOTSUPPORTED, SkinningNotSupported);
GENERATE_EXCEPTION(D3DXERR_TOOMANYINFLUENCES, TooManyInfluences);
GENERATE_EXCEPTION(D3DXERR_INVALIDDATA, InvalidData);
GENERATE_EXCEPTION(D3DXERR_LOADEDMESHASNODATA, LoadedMeshHasNoData);
GENERATE_EXCEPTION(D3DXERR_DUPLICATENAMEDFRAGMENT, DuplicateNamedFragment);
GENERATE_EXCEPTION(D3DXERR_CANNOTREMOVELASTITEM, CannotRemoveLastItem);
GENERATE_EXCEPTION(D3DXFERR_BADOBJECT, BadObject);
GENERATE_EXCEPTION(D3DXFERR_BADVALUE, BadValue);
GENERATE_EXCEPTION(D3DXFERR_BADTYPE, BadType);
GENERATE_EXCEPTION(D3DXFERR_NOTFOUND, NotFound);
GENERATE_EXCEPTION(D3DXFERR_NOTDONEYET, NotDoneYet);
GENERATE_EXCEPTION(D3DXFERR_FILENOTFOUND, FileNotFound);
GENERATE_EXCEPTION(D3DXFERR_RESOURCENOTFOUND, ResourceNotFound);
GENERATE_EXCEPTION(D3DXFERR_BADRESOURCE, BadResource);
GENERATE_EXCEPTION(D3DXFERR_BADFILETYPE, BadFileType);
GENERATE_EXCEPTION(D3DXFERR_BADFILEVERSION, BadFileVersion);
GENERATE_EXCEPTION(D3DXFERR_BADFILEFLOATSIZE, BadFileFloatSize);
GENERATE_EXCEPTION(D3DXFERR_BADFILE, BadFile);
GENERATE_EXCEPTION(D3DXFERR_PARSEERROR, ParseError);
GENERATE_EXCEPTION(D3DXFERR_BADARRAYSIZE, BadArraySize);
GENERATE_EXCEPTION(D3DXFERR_BADDATAREFERENCE, BadDataReference);
GENERATE_EXCEPTION(D3DXFERR_NOMOREOBJECTS, NoMoreObjects);
GENERATE_EXCEPTION(D3DXFERR_NOMOREDATA, NoMoreData);
GENERATE_EXCEPTION(D3DXFERR_BADCACHEFILE, BadCacheFile);
GENERATE_EXCEPTION(E_OUTOFMEMORY, OutOfMemory);
default:
ex = gcnew GraphicsException( "A graphics exception occurred." );
}
ex->HResult = hr;
return ex;
}
inline void GraphicsException::CheckHResult( HRESULT hr, String^ dataKey, Object^ dataValue )
{
GraphicsException::LastError = hr;
if( DirectXException::EnableExceptions && FAILED(hr) )
{
GraphicsException^ ex = GraphicsException::GetExceptionFromHResult( (hr) );
if( ex != nullptr )
{
if( dataKey != nullptr )
ex->Data->Add( dataKey, dataValue );
throw ex;
}
}
}
inline void GraphicsException::CheckHResult( HRESULT hr )
{
GraphicsException::CheckHResult( hr, nullptr, nullptr );
}
}
}
I imagine a few of you have choked on whatever drink you were in the middle of consuming at this point. But think about it. How would you have done it? Would you really type out the exception classes one by one? No, you'd copy paste them and edit. If there were any mistakes in the snip that you copy pasted, you've now multiplied the same bug fifty times and get to fix it fifty times. And let's not forget how much scrolling you'll be doing over nearly identical blocks of code. Maintaining the SlimDX exception setup is so easy, and it's so very easy to look at it when you need to examine behavior or fix something. Not only that, macros are really easy to work with as a group using regex find and replace, because they're simple single line constructs. Trying to replace entire classes is much trickier to actually do.
Some time back, just after I wrote about in code profilers, I was working on building one in C#. Partway through, I suddenly realized that C# doesn't have line, file, or function macros. If you want that sort of information, you have to get a StackFrame object that will tell you all the details. That's all great until you remember that you're now allocating a new object every time you hit a profiled function. (Oh, and StackFrame won't work when symbols aren't available.) Allocation in .NET is cheap, but it isn't that cheap. My solution was basically to stick to coarse profiling only, which isn't too bad and doesn't really hurt the original goals that much. That's not the point. The C preprocessor provides you with an extraordinarily elegant method for embedding information available at compile time into the executable itself and doing all sorts of useful debugging tricks with it. Asserts, in code profilers, debugging with automatic file/line tagging (with no cost other than code size!), even variables with magically unique names to pull off all sorts of useful tricks.
My point is that, easily abused or not, the C preprocessor is an unbelievably useful facility. I maintain that the preprocessor is one of the places where K&R actually did something right, but a bunch of idiots screwed it up for everyone. If it came to designing a programming language, I would include a preprocessor as part of the core. In fact, I might just drop the C preprocessor in outright, without even bothering to make it smarter. Half the brilliance of that thing is that it's such a stupidly crude system, lacking basically anything more clever than macros with arguments. Now naturally I wouldn't make it such a fundamental part of the compilation system, like in C, where it's totally impossible to get along with it. Imagine something much closer to C#, but with a full text based preprocessor sitting there for when you need it. Thus it's not critical to anything, and you can skip using it completely when you don't want it.
Of course, the problem solvers in the crowd have already realized that finding a standalone C preprocessor is extremely easy and can be dropped into a build as a pre-compile step quite easily. You can even be clever about it and write a csc replacement or a custom make system that automates it. Well, that'll work. There's some subtleties involved (timestamps, dependency analysis, etc) but it will do the trick. At least it will until you attempt to share your code with anyone else, since now you have custom bits in the build system. And it will never settle in as a core idiom of the language. (Maybe that's a good thing?) Personally, I'm very, very close to actually doing it and simply making the preprocessor binary part of any code distribution or source repository that I put out. It'll be quite easy in MSBuild, although you'll have to inject an extra task and item group in between the files and the compiler by hand. Once you've done that, it shouldn't interfere with anything though. I'm still a little concerned about what will happen to file timestamps in the process, but even if a full rebuild is forced it'll probably be worth it. It probably won't get me an insane engineer award, but it's at least a point in my favor.
| |
| Tuesday, November 6, 2007 |
 Tim Sweeney and the Future of Game Development I |
Posted - 11/6/2007 2:21:12 PM | I attended a talk at Johns Hopkins last Friday given by Tim Sweeney, and had dinner with him later in the evening. And in between those two events, I was at an office party where we played Fracture multiplayer. That was a really good day. Oh, and apparently Tim got himself a Ferrari F430 Spider a few months ago. Not bad.
Tim Sweeney and the Future of Game Development I
If you've been paying attention to anything Sweeney's been saying over the past year or two, this will all be very familiar. If you haven't, then I suggest you do, because as far as I can tell he's the only high profile guy in the industry who is actually trying to move us out of this ridiculous stone age which we are currently forced to suffer in when developing games. I don't mean to suggest that his ideas are necessarily the best way of moving forward, but the goal is to effect change, and that's really the biggest step.
There are two basic and somewhat related problems which Tim is concerned about for game development. The first is code correctness. He wants to be able to write code which is very heavily annotated with constraints, both on the values of variables and on the variables themselves. For example, a possible constraint on an array variable would be that it cannot be a null reference. A constraint on the value of the array would be that all its elements be unsigned integers less than 10. (These constraints can be dependent, too; the upper bound of those integers could be the last index of another array.) The compiler would statically enforce these constraints as much as possible, and presumably emit checks for the rest (he's always been kind of vague on that part). He mentioned, as he has before, that about half of the bugs in Unreal can be traced to null pointer and out of bounds memory accesses, integer overflow, and use of uninitialized variables. His ideal is a world where, as much as possible, when the compiler reports 0 errors and 0 warnings, the code is correct.
The whole correctness thing actually reminds me quite a bit of Spec# which is a really cool little language that I've been meaning to mess with for some time. It's worth reading the papers they have on that page, since they talk at some length about their static verification, which includes using a theorem solver to prove certain assertions about the code. It's really very cool that we can do that sort of thing, and quite unfortunate that it hasn't really made its way into the mainstream yet. (Although since Spec# is for .NET, we can blend it happily with anything else, which is pretty slick.) As I've said in the past, I am a big believer in static checking -- although the full explanation is rather difficult to lay out concisely, which is why I haven't finished that series yet. To cut the story short, I am very much on Sweeney's side here, and have considered switching from C# to Spec# outright for my hobby stuff on more than one occasion.
The other big problem is concurrency. It's obvious that more cores and more threads is going to be the way to scale from here on out. (My personal take, and I suspect that Tim would agree, is that the way forward is symmetric multiprocessing. PS3 style architectures will not be welcome or appreciated in years to come.) The problem is that we don't know how to scale up past more than just a handful of cores. 2 cores was pretty easy. 4 and 6 have been tricky but we've managed it to some extent. Beyond that, we're out of ideas that don't change the programming model. How exactly do you write a game that makes efficient use of 16 cores or more? We need to move beyond the current model and find a better way of handling concurrency if we're going to accomplish anything.
Tim gave an analogy I quite liked. Essentially he explained that locks (and atomic operations like CAS) are equivalent to manual memory management. We don't really want to do either, and removing them from the equation is a big step forward for programmer productivity. (He cited a figure of about 30%-40% less time to produce equivalent code in C# instead of C++, including bug fixing time.) Just like modern languages took over control of memory management, the next round of languages needs to take over control of concurrency, specifically the handling of shared data during concurrency. (Splitting up computation across threads automagically is a far more touchy thing and although things like OpenMP are neat, it's not something we'll have in a usable way any time soon.)
Sweeney's analysis of the situation agrees with the conclusion I came to some months ago, which is that in the long term, software transactional memory (STM) is the most likely to yield workable results. Message passing concurrency, similar to Erlang, is an interesting and useful approach in many situations, but it simply isn't effective in games, where a single update may need to touch quite a lot of different objects in order to complete. I'm not going to go into detail about STM because there's already plenty out there, I will summarize it. Essentially, STM allows us to write to shared state as if it were a single threaded program, with guarantees of atomicity inside a block. If anything goes wrong in the meantime -- for example, if another thread comes in and stomps all over our memory -- the underlying runtime backs out the atomic transactions and re-runs them one at a time so that they complete successfully. The basic idea is that although you have a lot of threads touching a lot of memory, it's very rare that two threads will touch the same memory at the same time. As long as we handle that situation correctly when it comes up, we can spend most of our time assuming it won't happen.
STM is awesome because it doesn't really require much thought. Any time you want to do something with shared state, any shared state, you simply open an atomic block and continue as usual. No locks to keep track of. No danger of deadlocks, livelocks, race conditions, or any of the other nasty things that we deal with in lock based concurrency. Things just work. The downside, of course, is that performance is non-optimal. We have a runtime living under us that is doing all kinds of maintenance and tracking work in order to be able to do things like roll back transactions, not to mention we can't do anything atomically that can't be reversed (like IO) and are forced to stick to locks in those situations. But the thing is, that stuff doesn't matter. STM is a constant overhead; it doesn't become significantly more expensive as the number of cores increases. Programs using STM, however, scale fantastically well. If you take a 40% performance hit by using STM on a dual core system, that really sucks, because you're nearly back to single core performance. But if you gain 50% every time the core count doubles, then when you go to quad core you're at 90%, and by eight cores you've crossed the threshold where it's a win. So although STM is a fairly poor choice in situations with less threads, it's likely to be a really great solution five to ten years from now, especially as we get better at implementing STM runtimes. (Remember how much Java performance sucked back in the day?)
Naturally these are all ideals. The reality of the situation is grim, because the industry is critically C++ locked and there's no clear language to move to. It doesn't look like core counts are going to wait for us, and whoever figures out how to leverage all that power instead of simply trying to keep up is going to blow the competition out of the water. The people who are determined to stick to C++ -- or even C (!) -- are dinosaurs that will become increasingly undesirable in the years to come. The only thing holding us back is that nobody's managed to really agree on where to go from here; we just know we need to go somewhere. The current development methods aren't sustainable, and it'll be interesting to see where we end up after we jump off this particular sinking ship.
| |
|
| S | M | T | W | T | F | S | | | | | 1 | 2 | 3 | 4 | 5 | | 7 | 8 | | 10 | | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | | 28 | 29 | | |
OPTIONS
Track this Journal
ARCHIVES
October, 2009
September, 2009
August, 2009
July, 2009
June, 2009
October, 2008
June, 2008
May, 2008
April, 2008
March, 2008
February, 2008
January, 2008
December, 2007
November, 2007
October, 2007
September, 2007
August, 2007
July, 2007
June, 2007
May, 2007
February, 2007
|