Thursday, January 5, 2012

Errata

Hello,

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_traits<char> > (401330h)
00401114  add         esp,8
00401117  mov         ecx,eax
00401119  call        dword ptr
[__imp_std::basic_ostream<char,std::char_traits<char> >::operator<<
(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,std::char_traits<char>,std::allocator<char>
>::basic_string<char,std::char_traits<char>,std::allocator<char> >
(40203Ch)]
0040116D  lea         ecx,[esp+20h]
00401171  mov         dword ptr [esp+54h],0
00401179  call        dword ptr [__imp_std::exception::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,std::char_traits<char>,std::allocator<char>
>::basic_string<char,std::char_traits<char>,std::allocator<char> >
(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