davideberly / GeometricTools

A collection of source code for computing in the fields of mathematics, geometry, graphics, image analysis and physics.
Boost Software License 1.0
1.08k stars 202 forks source link

Add support for environments where exceptions are disabled #61

Closed erebel55 closed 1 year ago

erebel55 commented 1 year ago

This PR adds support for GTE in build environments where exceptions are disabled. In this case, instead of throwing the exception it will terminate.

The specific environment this change was necessary in was when used with Unreal Engine https://github.com/EpicGames/UnrealEngine But it is fairly common for most game engines to have exceptions disabled for performance reasons.

davideberly commented 1 year ago

Have you measured the performance difference between (1) having code that has the ability to 'throw' and (2) replacing 'throw' by 'std::terminate()'? According to the checked answer in this Stackoverflow article, stack unwinding does not occur if the exception is not handled by a function in the call sequence. Not handling an exception or calling std::terminate() have the same effect--the program dies, in which case the performance really doesn't make a difference.

I do not know what code the compiler generates when 'throw' is present in the code. If there is a performance difference, my only guess is that such code is executed even if a 'throw' never occurs. Would you be kind enough to let me know how you measured the performance difference when using Unreal Engine?

Regardless, I have looked at your diffs and it looks reasonable. But before I approve, I need to verify the change is okay both on Microsoft Windows and on Linux.

Thanks.

erebel55 commented 1 year ago

No I didn't profile performance differences. Running without exception support is a pretty normal standard for game engines in general so I think it makes sense to support both regardless of performance. Performance isn't the only reason that exceptions are typically discouraged in game engines though. They also arguably make code more complicated and can lead to memory leaks.

My first attempt was to only enable exceptions for this specific Unreal module/plugin that I put GTE in but that ended up being difficult/impossible on clang. I noticed that Epic Games has hacked apart various other libraries that it includes in their engine to remove exceptions as well so it would be great if GTE supported this natively.

By default Unreal Engine runs with the -fno-exceptions option on clang. However, I was getting [] cannot use 'throw' with exceptions disabled compile errors without my fix.

This is an interesting read too https://pspdfkit.com/blog/2020/performance-overhead-of-exceptions-in-cpp/

Let me know how testing goes :) I appreciate the response!

Thank you for the awesome math library ❤️ Ethan

davideberly commented 1 year ago

Interesting and well written description at the link you provided. I had also read about zero-cost exceptions. The performance measurements at your link are very small times in nanoseconds. I thought that for 64-bit real-time clocks, the actual resolution of the timer is not 1 nanosecond, even when the ratio (std::chrono::high_resolution_clock::period::num / std::chrono::high_resolution::period::den reports) reports 1e-09 seconds (1 nanosecond). I implemented a loop that runs 1024 times, reads the high-resolution clock, and stores the associated nanoseconds in a std::array that is on the stack. I then have a loop that writes the nanoseconds to a console window. I got a short block of 0, a short block of 100, a short block of 200, and so on. These appear to indicate the resolution is 100. (I know that code for std::high_resolution_clock is implementation dependent.)

The argument about "game code" not using exceptions has been around for a long time. Always fodder for debate. My rule of thumb is to use Amdahl's law. In the game environment, say, running at 100 fps, you have 0.01 second to process a frame. That is 10 milliseconds or 10,000 microseconds or 10,000,000 nanoseconds. If handling an exception is on the order of 1000 nanoseconds (as mentioned in your link) and you reduce that time to 100 nanoseconds, that is not going to make a significant change in the game frame rate. I prefer to run a profiler and tackle the top bottlenecks for CPU consumption (as per Amdahl's law).

Finally, if an exception is unhandled, the program dies. If instead you call std::terminate(), the program dies. If functions return error codes and an error needs to be handled properly but you do not, the program dies. In all 3 cases, the performance is excellent. The program quickly dies.

Well, so much for philosophy.

The main argument for making the change is that Unreal will not allow the compilation, which has nothing to do with performance. I will approve the code but with 2 minor changes. First, when changes are made to my posted files, I prefer that the "Version: *" be modified to the current date. For Logger.h, that would be 6.0.2023.05.07. Second, you have used a preprocessor symbol TSL_NO_EXCEPTIONS, which appears to be something part of github/Tessil. I wish to minimize interactions with symbols from other distributions. My choice is to replace TSL_NO_EXCEPTIONS by GTE_NO_EXCEPTIONS. Are you able to make this work in your environment? I suppose you would add to your compiler settings to define GTE_NO_EXCEPTIONS to be TSL_NO_EXCEPTIONS.

Let me know whether you are okay with these modifications. I will then approve. I am soon to post GTE 6.6 (today or tomorrow) and will include the modified file in it.

Thank you.

erebel55 commented 1 year ago

That sounds great to me! I appreciate the philosophy :)

I made the changes you requested, which made sense. Please let me know if there are any other changes you require.

Awesome to hear that 6.6 is coming soon!