I recently purchased a copy of your book. After having read through
about half of the book so far I wanted to send you some comments. I
thought about posting this in a review on Amazon, but rather than
diminsh your otherwise excellent book I thought it better to simply
write you instead and perhaps this information can be included in a
future printing, or published on your website as errata. Because to
be honest the book is really good otherwise and I think that despite
the following issues it deserves an excellent review.
Anyway My comments are related to the information you provide on
exception handling in chapter 3. For starters, you should be careful
about using the term "SEH" because technically SEH is very specific to
the Windows platform and is a native OS-level system service. C++
exception handling, on the other hand, is a *completely* different
implementation of an exception handling mechanism, which may or may
not be implemented using SEH by the compiler. Obviously if your code
is being compiled for any platform other than Windows, then the
compiler is obviously not using SEH. In the book I believe you are
actually referring to C++ exception handling.
Secondly, you mention on page 132 that "SEH (sic) adds a lot of
overhead to the program. Every stack frame must be augmented to
contain additional information required by the stack unwinding
process. Also, the stack unwind is usually very slow -- on the order
of two to three times more expensive than simply returning from the
function. Also, if even one function in your program (or a library
that your program links with) uses SEH, your entire program must use
SEH. The compiler can't know which functions might be above you on
the call stack when you throw an exception."
This is entire paragraph is almost completely wrong. The only stack
frames that are augmented at all are ones that contain a catch block.
To demonstrate proof of this, consider the following small program.
#include <iostream>
#include <exception>
void test1();
void test2();
void test3();
void __declspec(noinline) test1()
{
test2();
}
void __declspec(noinline) test2()
{
try { test3(); }
catch(const std::exception& e)
{
std::cout << "Got an exception in test2" << std::endl;
}
}
void __declspec(noinline) test3()
{
throw std::runtime_error("Error");
}
int _tmain(int argc, _TCHAR* argv[])
{
test1();
return 0;
}
Now let's take a look at the assembly language generated by the
compiler. The following is simply the above code repeated but with
assembly inlined at each sequence point. It's not necessary to give
the assembly anything more than a cursory glance just to see how much
code the compiler is generating in each function.
void __declspec(noinline) test1()
{
test2();
004010C0 jmp test2 (4010D0h)
}
void __declspec(noinline) test2()
{
004010D0 push ebp
004010D1 mov ebp,esp
004010D3 push 0FFFFFFFFh
004010D5 push offset __ehhandler$?test2@@YAXXZ (401E90h)
004010DA mov eax,dword ptr fs:[00000000h]
004010E0 push eax
004010E1 mov dword ptr fs:[0],esp
004010E8 sub esp,8
004010EB push ebx
004010EC push esi
004010ED push edi
004010EE mov dword ptr [ebp-10h],esp
try { test3(); }
004010F1 mov dword ptr [ebp-4],0
004010F8 call test3 (401140h)
catch(const std::exception& e)
{
std::cout << "Got an exception in test2" << std::endl;
004010FD mov eax,dword ptr [__imp_std::endl (402038h)]
00401102 mov ecx,dword ptr [__imp_std::cout (402054h)]
00401108 push eax
00401109 push offset string "Got an exception in test2" (40215Ch)
0040110E push ecx
0040110F call std::operator<<<std::char_
00401114 add esp,8
00401117 mov ecx,eax
00401119 call dword ptr
[__imp_std::basic_ostream<
(40204Ch)]
}
0040111F mov eax,offset $LN7 (401125h)
00401124 ret
}
00401125 mov ecx,dword ptr [ebp-0Ch]
00401128 pop edi
00401129 pop esi
0040112A mov dword ptr fs:[0],ecx
00401131 pop ebx
00401132 mov esp,ebp
00401134 pop ebp
00401135 ret
void __declspec(noinline) test3()
{
00401140 push ebp
00401141 mov ebp,esp
00401143 and esp,0FFFFFFF8h
00401146 push 0FFFFFFFFh
00401148 push offset __ehhandler$?test3@@YAXXZ (401E82h)
0040114D mov eax,dword ptr fs:[00000000h]
00401153 push eax
00401154 mov dword ptr fs:[0],esp
0040115B sub esp,4Ch
throw std::runtime_error("Error");
0040115E push offset string "Error" (402178h)
00401163 lea ecx,[esp+8]
00401167 call dword ptr
[__imp_std::basic_string<char,
>::basic_string<char,std::
(40203Ch)]
0040116D lea ecx,[esp+20h]
00401171 mov dword ptr [esp+54h],0
00401179 call dword ptr [__imp_std::exception::
(4020E4h)]
0040117F lea eax,[esp+4]
00401183 mov byte ptr [esp+54h],1
00401188 push eax
00401189 lea ecx,[esp+30h]
0040118D mov dword ptr [esp+24h],offset
std::runtime_error::`vftable' (4021A0h)
00401195 call dword ptr
[__imp_std::basic_string<char,
>::basic_string<char,std::
(402040h)]
0040119B push offset __TI2?AVruntime_error@std@@ (402410h)
004011A0 lea ecx,[esp+24h]
004011A4 push ecx
004011A5 mov byte ptr [esp+5Ch],0
004011AA call _CxxThrowException (401E00h)
$LN10:
004011AF int 3
}
int _tmain(int argc, _TCHAR* argv[])
{
test1();
004011B0 call test1 (4010C0h)
return 0;
004011B5 xor eax,eax
}
Note that in neither main nor in test1() is there any code having to
do with exception handling. The reason this works is that it's true
that the compiler does not know what functions will be on the
callstack at the time the exception is thrown, but it *does* know what
functions *might* try to handle exceptions. So in each of these
functions, it generates code to modify the exception handling chain.
What really happens when the stack unwinds is that it starts walking
through the stack frames and the exception handling chain in parallel.
If a stack frame is found has no entry at all in the chain, or it has
one or more entries that don't match the current exception, it simply
calls all destructors for constructed objects and then moves up the
stack until it finds one or the program terminates. But there is no
extra code generated in any of these functions. These destructors
would have had to have been called anyway even if the function
terminated normally.
That's my biggest comment. My final comment is in the early chapters
when you're discussing visual studio and different types of builds:
debug, release, production, and hybrid. At one point you mention that
systems like gnu make make it easy to define certain options on a
per-translation unit basis, but that this is very difficult in Visual
Studio. In fact it's very easy! Right click a cpp file in the
solution explorer, click properties, and bam. Any settings you make
in that window are applied only to that translation unit. You can
change any setting that you could normally change on a per-project
basis, as long as it is not a linker setting. Preprocessor,
optimization, etc are all changable on a per-translation unit basis
though.
Aside from these comments, however, the book is definitely a
refreshing addition to the sometimes dilluted market for game engine
books. Too many books try to cash in on the game craze and while it's
clear the authors have experience, the books are not rigorous enough
to leave one satisfied. I like the encyclopedic approach taken in
this book, and I'd definitely be interested in seeing an additional
volume at some point in the future.
Regards,
Zachary Turner
No comments:
Post a Comment