Gamedevs presenting language problems to C++ standards working group

Started by
25 comments, last by l0calh05t 9 years ago

Many of the complaints in the paper are out of date( as the follow up comment often shows), why include that stuff?


The original discussion that much of the paper was cut-n-paste from was an analysis pass I did over the EASTL paper, which I hadn't intended to be part of any paper, but since I disappeared from the mailing list for a while I didn't exactly give Michael much to work with.

Michael and Nicolas did a fantastic thing in putting the paper together and deserve lots of thanks for helping getting game developers' issues in front of the committee at all. Same goes to the other contributors on the unofficial real time list.

If y'all have complaints with the paper or its contents, come help contribute to the next round of papers and proposals for the meeting after Lenexa. smile.png

Regarding customizing vtables, I think typeclasses would be an improvement over the inheritance crap we have today for runtime polymorphism, so a proposal for that would be nice


I look forward to reading such a proposal. Perhaps you'll write one? smile.png

Sean Middleditch – Game Systems Engineer – Join my team!

Advertisement

since there is a small but measurable cost to both RTTI and C++ Exceptions just by their very existence


I would not say that exceptions have a small cost. GCC is still sometimes utterly incapable of producing code that works if you decide to use them. I would call that a serious cost, but it does not happen for everyone, just the unlucky ones, so statistically it might be small.


My proposals:

--- 1 ---

For vtables, I would suggest implementing non-export vtables. The advantage is that you would know all the implementation functions and thus be able to use a smaller integer to point to a vtable, allowing some of the functions to be compiled into devirtualized functions with top-level switch statements for implementation resolution, if they're small enough for that.

The downside is, of course, that you can't override such classes across dynamic module boundaries. Or, if you do, the overrides would perform as usual, not having the benefits of this optimization.

--- 2 ---

Inline calls. This can be worked around but since it's such a small issue, why not do it the right way - by adding a special attribute to the call that would inline just the function call, if possible.

--- 3 ---

Manual struct layouts. Something like...


struct large_int
{
    uint32_t high @ 0;
    uint32_t low @ 4;
    uint64_t wide @ 0; // overlaps both previous members
};

This would simplify data structures and their reinterpretation, allow to define interface structs (such as event type/data structs) easier.

--- 4 ---

An "I know what I'm doing" cast: "jiggabyte* ptr = %% &car_object.private_thing;" I realize that auto solves many of those problems (the horrible reinterpret_cast type retyping) but it doesn't solve all. For example, it is still impossible to cast from member function pointers and access private member variables. This is necessary sometimes as it happens to be a better solution than messing with the source. And, of course, an optional compiler warning could be created that prints all occurrences of this cast.

--- 5 ---

Setters and getters: defining a function with a __set__ or __get__ prefix would create a function that is triggered on a property. __get__ could return either a reference or value, to help avoid the issues that are present in C# and similar languages. Additionally, a prefix could be added for accessing such properties, to make the user acknowledge the presence of a getter (and the performance hit that may come with it).

--- 6 ---

Make externally defined operators contextually equal to internally defined ones in that they're able to return "temporary" arguments passed by reference. This is useful for logging system extensions, such that there's an externally defined operator:


Logger& operator << ( Logger& me, const Thing& t ){ magic; return me; }

It works if the operator is defined inside the class, why shouldn't it work outside? Perhaps this has been fixed and I just don't know the way to do this right but I haven't managed to find anything on this except people saying "C++ can't do this". Because it should be able to. But perhaps the next proposal would solve this.

--- 7 ---

"this" arguments. Make their member variables equivalent to local ones, as if they were coming from the called class of a method.


Logger& operator << ( this Logger& me, const Thing& t ){ me.magic(); magic(); /* < equivalent to prev. call */ return me; }

--- 8 ---

Standard data dumping functions, automatically generated for structs and other types that don't have them. I'd like to be able to write "dump( x )" and it would print everything about "x", whatever it is, without a debugger. Additionally, I'd like to be able to specify my own class instance of a dumper, to be able to dump wherever I want. And it's important here not to lean on RTTI or anything that would add complexity and make dumping error-prone or unavailable for some.

Can't think of anything else ATM. Will add it when I can.

Related to the vtable... I always wondered why thunked virtual functions are assembled into this:


base.function:
   ...
   ret


derived1.function:  // derived1 class is derived from base
   add ecx, 8
   jmp base.function

derived2.function: // derived2 class is derived from derived1
   add ecx, 8
   jmp derived1.function

 and so on...

instead of this;


and so on... upwards
.
.
.
derived2.function: // derived2 class is derived from derived1
   add ecx, 8
derived1.function:  // derived1 class is derived from base
   add ecx, 8
base.function:
   ...
   ret

(ecx is the "this" pointer)

At least the MS compiler does this, no matter which optimization mode is used.

Also, what's the point of thunking functions imported from another library, instead of accessing them directly from the import table? It only helps hackers who might want to hook one of those functions.

My major problem with the paper is that it seems to harp on the standards committee for things it does not control and intentionally does not standardize.

However before I start criticizing - I want to thank all of you for putting the time in to make a paper in the first place. We need more representation in the standard and a lot of us are under too tight NDAs and legal red tape to feel like we can contribute much (or I'm just paranoid)

Global new and delete:
This one is hard. The language needs to support allocations some how. I think it's worth pointing out that the standard library does not use global new and delete, but allows you to specify allocaters that can do a lot of what you want. So you should probably be going to your middleware providers and tell them to provide allocator support - and design your own libraries in such a way rather then asking the committee to do something they've already done.

std::function and lambdas doing allocations
I've never seen a lambda do allocations, and the implementation of both are defined by the implementer, not the standard. The standard just defines how std::function and lambdas should work. If you don't like the implementation, then talk to your compiler vendor/STL provider.

Keep in mind that type erasure always has a cost, and you literally cannot get rid of allocations in something like std::function because it had to be able to hold an arbitrarily-sized object. If you are concerned about the allocations, then use what has already been provided and give std::function a custom allocator (which we've done on our project).

RTTI
I think it's wrong to state that game developers don't use it. What would be accurate is that game developers usually write their own because the "general" solution provided by C++ has too high a cost because it has to account for language features that are rarely used (or should be rarely used, like MI).

A better solution here would be a way to customize C++ RTTI rather then telling the committee to remove it or not produce features reliant on it.

Vtables
Vtables are not standard. Nothing for the committee to do here - they simply say "virtual dispatch" has to exist and how it behaves, not how it is implemented. This is a compiler vendor concern.

Alignment
Pretty sure C++11 or 14 has had some support for this. Or maybe I'm just thinking of aligned_storage and the supporting type traits to get something's alignment.

Debuggability
Again, not something the standards committee covers. Talk to your vendors.

Small object optimizations
I don't want the standards committee to standardize these. Different platforms have different memory and cache statistics that the compiler/STL vendors should be using to inform the sizes of their objects. I don't want the committee to force something on vendors that impairs performance on current (or future) platforms.

Maybe we can make it a policy - but I'm not sure how the templating would work on that...

STL documentation/implementation details
Um... yeah... not the standards committee's job. And relying on implementation details is never a good idea. (Sometimes necessary, if you can ensure it will never change during life of the product, but still)

No allocation containers
I feel like this could be a constructor parameter - like std::noalloc or something passed in. Not default behavior.

Disabling exceptions
This might be worth some discussion - but I still feel this is a tooling question/concern. C++ has exceptions. They generally have low cost on modern compilers and hardware. And in some cases there is no other option to return error codes then using an exception (constructors).

That being said, it is probably an important discussion to have in terms of if an alternate solution for error propagation was proposed. But with a huge swath of C++ libraries and applications already built on using them I can also imagine huge pushback.

Reflection
Hrm... I like this, but I'd want it to be some form of template extensions so the reflection is available at compile time and removed at runtime. Something that could replace the stringize preprocessor operation would be nice.

SIMD
I agree that this is common enough that it should be standardized in some manner - similar to how the committee has finally gotten around to recognizing that threads are a thing. However this will not be easy as several platforms can have widely varying implementations (i.e. Intel CPU vs. PS3 SPU vs. NVidia GPU)

I love your reply, but again several of the points can be reduced to this one line:


I agree that this is common enough that it should be standardized in some manner - similar to how the committee has finally gotten around to recognizing that threads are a thing. However this will not be easy as several platforms can have widely varying implementations

That is why nearly all of those complaints won't be addressed. Threading was particularly interesting and has always been on the radar, but they intentionally did not want to standardize it even in the first drafts in the '90s because so many systems had wildly different behaviors.

Allocators and custom behavior are system specific and generally should not be forced through the standard onto every use case. Just like you don't want things that corporate database developers are asking for, they don't want what games are asking for. If you don't like the behavior, build something different. If enough people from enough industries gravitate to the solution, which is common in the boost libraries, then it will get incorporated to the standard. An example is the c++ regular expression engine. Back in the early '90s there were many different needs and many different regex language definitions. Over time people from many industries gravitated to a specific regex syntax, a common implementation emerged, and that was eventually added to the language in TR1. The hash map and hash set similarly were discussed in early versions but were difficult to generalize to everybody; one group wants one way to hash, another wants another, a third wants a different interface. After boost had a widely-accepted implementation that multiple industries had widely adopted, it was added to the standard.

As for RTTI and exception handling, those have always been an interesting point.

The rules regarding exceptions and RTTI are being relaxed at many companies. Twenty years ago when the rules were being introduced everywhere it was because exception handlers had all kinds of negative properties. Compilers generated a little bit of code in every function call for stack tracking and unwinding, and RTTI required very large tables of data and were slow to process. Since several sub-components of both share the same technology, and since the cost was present even if exceptions were never called and dynamic casts never used, both were typically off by default. Skip to today, 2015, and compilers have advanced. There is a small table of data needed by the executable, but nowhere near as large as two decades ago. Exceptions no longer need every function call to include stack unwinding information, instead there is a lookup cost when they are used -- but games can be written in ways that never trigger c++ exceptions except through what are normally fatal bugs anyway. RTTI has been improved so that comparisons against the exact class and comparisons against the root class are nearly free, just an integer comparison, and that represents nearly all use of dynamic type info. The storage cost for the RTTI data has shrunk dramatically for nearly all compilers. In other words, the reasons behind the commonplace ban 20 years ago has been nearly completely resolved.

There are a small number of bad behaviors that can trigger slowness, but they are well known and fall into the "don't do that" category. Making it a policy to disable both exceptions and RTTI are a strong way to enforce a DDT situation, but coupled with the advances in compiler details that have solved most of the problems, as the number of DDT situations shrinks the reason behind the policy has eroded to being nearly non-existent.

The other is concern is reflection. If you want reflection, don't use C++. Reflection requires several tasks that are the exact opposite of what c++ is attempting to do. C++ is actively attempting to eliminate and remove the things reflections depends on. If you need reflection compile the code that needs reflection with a different language.

--- 3 ---

Manual struct layouts. Something like...


struct large_int
{
    uint32_t high @ 0;
    uint32_t low @ 4;
    uint64_t wide @ 0; // overlaps both previous members
};

This would simplify data structures and their reinterpretation, allow to define interface structs (such as event type/data structs) easier.

--- 4 ---

An "I know what I'm doing" cast: "jiggabyte* ptr = %% &car_object.private_thing;" I realize that auto solves many of those problems (the horrible reinterpret_cast type retyping) but it doesn't solve all. For example, it is still impossible to cast from member function pointers and access private member variables. This is necessary sometimes as it happens to be a better solution than messing with the source. And, of course, an optional compiler warning could be created that prints all occurrences of this cast.

--- 5 ---

Setters and getters: defining a function with a __set__ or __get__ prefix would create a function that is triggered on a property. __get__ could return either a reference or value, to help avoid the issues that are present in C# and similar languages. Additionally, a prefix could be added for accessing such properties, to make the user acknowledge the presence of a getter (and the performance hit that may come with it).

3. We already have this with unions.

4. We already have "I know what I'm doing" casts with C-style casts and (I think?) function-style casts.

5. Is this really that useful? How many getters/setters are you even writing? If you really need a getter/setter, why not just write methods with the name of the variable, ie. "X/X" instead of having "GetX()"/"SetX"?

On lambdas and std::function:

A lot of developers seem to think that capturing lambdas allocate because std::function can heap-allocate and lambdas are often passed around with std::function. I recently had just that belief challenged in a code review at work. But lambdas and std::function aren't the same thing (the type returned by a lambda isn't a std::function, it's an anonymous type), and lambdas themselves don't heap-allocate. They don't need to since defining the lambda is defining a new type which has a size known at compile time. Using auto and lambda to define a local function, or passing a lambda to a std::algorithm should never allocate as far as I'm aware.

If std::function allocating is a problem, could it be useful to allow one to specify the maximum size of a captured lambda object in the std::function instance declaration at compile time? Or even better, have the compiler infer it somehow?

Allocators and custom behavior are system specific and generally should not be forced through the standard onto every use case. Just like you don't want things that corporate database developers are asking for, they don't want what games are asking for. If you don't like the behavior, build something different. If enough people from enough industries gravitate to the solution, which is common in the boost libraries, then it will get incorporated to the standard.


I agree here - and it is up to the library provider to provide a good "default".

But I do think that sometimes we need discussion as to putting more "customization points" into the library. For example, can we add something to a container to tell it to not allocate in the constructor without changing the type of the container, or making the container slower. (Similar to how we currently have allocators as a customization point in many containers)

I don't want to have to write a custom vector or map - but I don't mind writing a custom allocator to plug into them, cause that's much less work.

The other is concern is reflection. If you want reflection, don't use C++. Reflection requires several tasks that are the exact opposite of what c++ is attempting to do. C++ is actively attempting to eliminate and remove the things reflections depends on. If you need reflection compile the code that needs reflection with a different language.


I think we DO need some more compile-time reflection capabilities. I want to be able to ask an enum for it's max value without having to make a MAX_VALUE element in the enum, for example. I also want to be able to generate a string from a name of something at compile time without having to use the preprocessor stringize operator.

I don't care about actually accessing a function at runtime by name especially when that comes with the cost of not inlining a function. (I can set up a map of strings to std::function and do that if I really wanted to)

Though I'd be ok if I could flag thing as "reflected" with the expectation that it increases code size, but allows the compiler to inline/eliminate during static compiliation. But certainly not everything in my code base.

On lambdas and std::function:
A lot of developers seem to think that capturing lambdas allocate because std::function can heap-allocate and lambdas are often passed around with std::function. I recently had just that belief challenged in a code review at work. But lambdas and std::function aren't the same thing (the type returned by a lambda isn't a std::function, it's an anonymous type), and lambdas themselves don't heap-allocate. They don't need to since defining the lambda is defining a new type which has a size known at compile time. Using auto and lambda to define a local function, or passing a lambda to a std::algorithm should never allocate as far as I'm aware.

If std::function allocating is a problem, could it be useful to allow one to specify the maximum size of a captured lambda object in the std::function instance declaration at compile time? Or even better, have the compiler infer it somehow?


You could probably specify a size like you do with std::array. The problem is now the size becomes part of the type and your function objects no longer do complete type erasure. In other words, I don't want to have to increase the size of my class because I'm storing a std::function<100> on the off chance someone passes me a big function object - and you also have additional complication of copying functions between sizes - potentially causing allocations when casting to a lower size.

In our codebase we can give std::function a custom allocator which is designed for short-term small allocations which almost eliminates the allocation cost as long as you're not storing the function inside a long-lived object (and in most cases, we aren't, we use it when we can't use a templated functor type - like passing through a virtual call).


The rules regarding exceptions and RTTI are being relaxed at many companies. Twenty years ago when the rules were being introduced everywhere it was because exception handlers had all kinds of negative properties. Compilers generated a little bit of code in every function call for stack tracking and unwinding, and RTTI required very large tables of data and were slow to process. Since several sub-components of both share the same technology, and since the cost was present even if exceptions were never called and dynamic casts never used, both were typically off by default. Skip to today, 2015, and compilers have advanced. There is a small table of data needed by the executable, but nowhere near as large as two decades ago. Exceptions no longer need every function call to include stack unwinding information, instead there is a lookup cost when they are used -- but games can be written in ways that never trigger c++ exceptions except through what are normally fatal bugs anyway.

Why aren't more console game developers using exceptions then? I'm aware that the "zero-cost" exception handling model increases the executable speed a bit and thrown exceptions are really slow, but surely that is better than locking up the console and forcing the user to reboot the entire system when an error condition occurs? From what I've seen, the typical error handling strategy is basically non-existent. Using asserts and trying to catch all the criticial bugs in debug builds is not an error handling strategy. C++ without exceptions is a broken language as far as I'm concerned.

3. We already have this with unions.
4. We already have "I know what I'm doing" casts with C-style casts and (I think?) function-style casts.
5. Is this really that useful? How many getters/setters are you even writing? If you really need a getter/setter, why not just write methods with the name of the variable, ie. "X/X" instead of having "GetX()"/"SetX"?


You're not reading what I wrote (especially about #4). Besides, why in the world would you assume that I want to make C++ language proposals and not know about unions?

And yes, setters are useful. Mainly for being able to change implementation without changing the interface. Something C++ is horrible at. I have to modify half my code after the interface is changed, even though it was completely unnecessary.

Why aren't more console game developers using exceptions then?


Because they break builds. They just do not work at all times. I've seen function argument overwrites because of exceptions. I've seen lots and lots of internal compiler errors because of exceptions, especially with gcc -O2. That might change with LLVM/Clang because it supports them much better, but until recently, Clang for consoles was simply not an option.

surely that is better than locking up the console and forcing the user to reboot the entire system when an error condition occurs?


If a game locks up without additional information, how do you know it was from an error? Could've just been an infinite loop or a deadlock. I've seen that sometimes consoles restart themselves automatically after some time. I haven't had one of the new consoles crash on me but I do not see how the old ones, with so little RAM, could reasonably recover from a crash. Who knows what parts of it the game has reprogrammed, or if there is any chance of recovery after a crash (hardware fault). Easier to just restart.

P.S. Added #8 in my first post.

Regarding exceptions... This is anecdotal evidence, but I've made some toy graphics applications that use exceptions, and noticed that weaker PCs would grind down to a halt if a bug caused an exception to be thrown at every frame of gameplay. I didn't figure out why, but I assume something is being totally thrown off. Makes me think games are better off just ignoring errors and just logging/asserting in debug builds instead.

I asked some emscripten people about best practices for exceptions, and the answer was something like "no dude... just... just don't." so it seems like you can't safely assume exceptions are okay universally.

This topic is closed to new replies.

Advertisement