New/delete vs. declare/garbage-collect

V

Vincent Fatica

I noticed that if I give MY_STRUCT a simple c'tor and d'tor, say,

MY_STRUCT::MY_STRUCT()
{
hEvent = CreateEvent(...);
}
MY_STRUCT::~MY_STRUCT
{
CloseHandle(hEvent);
}

then using a local instance via simple declaration,

INT my_function()
{
MY_STRUCT foo;
// blah
return 0;
}

adds nearly 6KB (most in the .text segment) to my target DLL when compared to
using it via new/delete. Can someone explain why that happens? The example
above is just a little simplified. The actual struct has as members a HANDLE, a
BOOL, and an OVERLAPPED but still the only initialization I want is for the
HANDLE. Thanks!
 
V

Vincent Fatica

Let me ask the same question another way (more to the point I hope). I use an
ENUM_INFO (described below) struct like this:

INT function()
{
ENUM_INFO EnumInfo;
//blah
EnumWindows(..., &EnumInfo);
// blah
return 0;
}

The struct and its c'tor and d'tor look like this:

struct ENUM_INFO
{
HANDLE hPipe;
BOOL bGlobal;
OVERLAPPED Overlapped;
ENUM_INFO();
~ENUM_INFO();
};
ENUM_INFO::ENUM_INFO()
{
Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Overlapped.Offset = Overlapped.OffsetHigh = 0;
};
ENUM_INFO::~ENUM_INFO()
{
//CloseHandle(Overlapped.hEvent);
};

Quite simply, when I uncomment the CloseHandle() call (no other changes
anywhere) in the d'tor, my target DLL increases in size by 6KB (most in the
..text segment). There's nothing special about CloseHandle(); the same will be
observed with any WIN32 API function in its place(e.g., Beep() or Sleep()).
What's going on?

If I instantiate an ENUM_INFO using new/delete, I don't see the size increase.

Thanks.
 
C

Carl Daniel [VC++ MVP]

Vincent said:
Let me ask the same question another way (more to the point I hope).
I use an ENUM_INFO (described below) struct like this:

INT function()
{
ENUM_INFO EnumInfo;
//blah
EnumWindows(..., &EnumInfo);
// blah
return 0;
}

The struct and its c'tor and d'tor look like this:

struct ENUM_INFO
{
HANDLE hPipe;
BOOL bGlobal;
OVERLAPPED Overlapped;
ENUM_INFO();
~ENUM_INFO();
};
ENUM_INFO::ENUM_INFO()
{
Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Overlapped.Offset = Overlapped.OffsetHigh = 0;
};
ENUM_INFO::~ENUM_INFO()
{
//CloseHandle(Overlapped.hEvent);
};

Quite simply, when I uncomment the CloseHandle() call (no other
changes anywhere) in the d'tor, my target DLL increases in size by
6KB (most in the .text segment). There's nothing special about
CloseHandle(); the same will be observed with any WIN32 API function
in its place(e.g., Beep() or Sleep()). What's going on?

If I instantiate an ENUM_INFO using new/delete, I don't see the size
increase.

How many places in your code are you creating local variables of this type?
Every single one of them will entail adding code to the function epilog to
destroy the object that won't be there unless you explicitly delete it.
Probably more importantly, having a destructor will cause any function
containing the object to have an exception frame and exception handling code
generated for it if you're compiling with -GX, -EHa or -EHs (in other words,
if you have exception handling turned on - it's on by default in 2005). In
exchange for that extra code size you code code will behave correctly in the
face of exceptions.

-cd
 
V

Vincent Fatica

How many places in your code are you creating local variables of this type?

Only one place.
Every single one of them will entail adding code to the function epilog to
destroy the object that won't be there unless you explicitly delete it.
Probably more importantly, having a destructor will cause any function
containing the object to have an exception frame and exception handling code
generated for it if you're compiling with -GX, -EHa or -EHs (in other words,
if you have exception handling turned on - it's on by default in 2005). In
exchange for that extra code size you code code will behave correctly in the
face of exceptions.

Well, it looks like compiler madness to me. If I make things a bit more
complicated, like this:

ENUM_INFO::ENUM_INFO(BOOL boo, HANDLE hoo)
{
bGlobal = boo;
hPipe = hoo;
Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Overlapped.Offset = Overlapped.OffsetHigh = 0;
};

ENUM_INFO::~ENUM_INFO()
{
CloseHandle(Overlapped.hEvent);
CloseHandle(hPipe);
};

And use it as a local variable like this:

ENUM_INFO EnumInfo(stristr(psz, L"/G") ? TRUE : FALSE,
CreateNamedPipe(szPipeName, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
PIPE_WAIT, 1, 0, sizeof(EVENT), 2000, NULL));

My DLL drops the 6KB it gained from the set-up mentioned above.
 
V

Vincent Fatica

Only one place.


Well, it looks like compiler madness to me. If I make things a bit more
complicated, like this:

ENUM_INFO::ENUM_INFO(BOOL boo, HANDLE hoo)
{
bGlobal = boo;
hPipe = hoo;
Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Overlapped.Offset = Overlapped.OffsetHigh = 0;
};

ENUM_INFO::~ENUM_INFO()
{
CloseHandle(Overlapped.hEvent);
CloseHandle(hPipe);
};

And use it as a local variable like this:

ENUM_INFO EnumInfo(stristr(psz, L"/G") ? TRUE : FALSE,
CreateNamedPipe(szPipeName, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
PIPE_WAIT, 1, 0, sizeof(EVENT), 2000, NULL));

My DLL drops the 6KB it gained from the set-up mentioned above.

Whereas (here's the real corker) if, in the code just above, I instantiate it
thus:

ENUM_INFO EnumInfo(FALSE, NULL);

(regardless of whether I later set the real (desired) values of hPipe and
bGlogal) the DLL size jumps back up 6KB. BTW, it's a release build.
 
A

Arnaud Debaene

Whereas (here's the real corker) if, in the code just above, I instantiate
it
thus:

ENUM_INFO EnumInfo(FALSE, NULL);

(regardless of whether I later set the real (desired) values of hPipe and
bGlogal) the DLL size jumps back up 6KB. BTW, it's a release build.

It's hard to be sure of anything without a complete sample (complete code +
compiler settings + compiler version), but I would say that when you see the
6 Kb increase, it's because the compiler decides that you need exception
safety mechanisms : that is, you've got a local (stack) variable with a
non-trivial destructor. In this situation, in order to be exception-safe,
the compiler needs to add an exception frame to the function where this
variable is declared, and it needs to add all it's internal machinery that
is used during stack-unwinding (eg, "catch" handlers tables, with filters by
exception types....) The stack-unwinding machinery is quite complex and need
a lot of static data (therefore the .text section increase).

However, I bet most of the 6 Ko increase is to be paid only once (that is,
if you add a second function that also allocates an ENUM_INFO on the stack,
you will see a much smaller increase).

Arnaud
MVP - VC
 
V

Vincent Fatica

It's hard to be sure of anything without a complete sample (complete code +
compiler settings + compiler version), but I would say that when you see the
6 Kb increase, it's because the compiler decides that you need exception
safety mechanisms : that is, you've got a local (stack) variable with a
non-trivial destructor. In this situation, in order to be exception-safe,
the compiler needs to add an exception frame to the function where this
variable is declared, and it needs to add all it's internal machinery that
is used during stack-unwinding (eg, "catch" handlers tables, with filters by
exception types....) The stack-unwinding machinery is quite complex and need
a lot of static data (therefore the .text section increase).

I don't know enough to say whether that's likely or not. But IMHO,

ENUM_INFO EnumInfo(FALSE, NULL);

(which causes the 6KB increase) looks safer than

ENUM_INFO EnumInfo(stristr(psz, L"/G") ? TRUE : FALSE,
CreateNamedPipe(szPipeName, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
PIPE_WAIT, 1, 0, sizeof(EVENT), 2000, NULL));

which doesn't.
 
V

Vincent Fatica

I don't know enough to say whether that's likely or not. But IMHO,

ENUM_INFO EnumInfo(FALSE, NULL);

(which causes the 6KB increase) looks safer than

ENUM_INFO EnumInfo(stristr(psz, L"/G") ? TRUE : FALSE,
CreateNamedPipe(szPipeName, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
PIPE_WAIT, 1, 0, sizeof(EVENT), 2000, NULL));

which doesn't.

FWIW, I'm building my DLL with VS2002/SP1. Ths settings are:

/O1 /Ob1 /Os /Oy /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_USRDLL" /D
"EVENT2_EXPORTS" /D "_UNICODE" /D "UNICODE" /D "_WINDLL" /GF /FD /EHsc /ML /Gy
/Zc:wchar_t /Fo"Release/" /Fd"Release/vc70.pdb" /W3 /nologo /c /Wp64 /Zi /TP

/OUT:"Release/ev.dll" /INCREMENTAL:NO /NOLOGO /DLL /DEF:"event.def"
/SUBSYSTEM:WINDOWS /OPT:REF /OPT:ICF /OPT:NOWIN98 /IMPLIB:"Release/event2.lib"
/MACHINE:IX86 g:\Projects\event2\release\\takecmd.lib kernel32.lib user32.lib
gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib
oleaut32.lib uuid.lib odbc32.lib odbccp32.lib release\TakeCmd.lib
 
B

Ben Voigt

Vincent Fatica said:
I don't know enough to say whether that's likely or not. But IMHO,

ENUM_INFO EnumInfo(FALSE, NULL);

(which causes the 6KB increase) looks safer than

ENUM_INFO EnumInfo(stristr(psz, L"/G") ? TRUE : FALSE,
CreateNamedPipe(szPipeName, PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
PIPE_WAIT, 1, 0, sizeof(EVENT), 2000, NULL));

Not from the compiler's point of view, because in the second case, all the
real work is done *before* calling the constructor. Any exceptions will
cause the object not to be constructed, and therefore not need destruction.

This extra work for exception handling is a very good thing. With stack
objects, you are guaranteed to (try to) free the kernel objects if an
exception is thrown after the object is created. In fact, you should deploy
the RAII idiom completely, so that every kernel object is in a separate
variable that knows to free it. These can all be member variables of
ENUM_INFO if you want. But, let's look at your code.

If the CreateEvent fails in the constructor, you may never CloseHandle on
the pipe. You don't try to use the event handle in the constructor, so the
object becomes live. Then, when you try to use the event handle
(WaitForMultipleObjects perhaps) you could get an access violation. The
compiler will begin stack unwinding, and call your destructor. The
destructor call to CloseHandle the event fails with another access
violation, immediately leaving the destructor. The pipe would not be
closed.

Using individual RAII objects will cause them to be separately destructed,
even if other destructors fail.

Getting all this right using new/delete would be:
(1) a big pain
(2) the __try/__finally logic would probably add the same 6kb as letting the
compiler use unwinding.

If you are willing to accept process termination in case of any failure
whatsoever, then disable exceptions. Otherwise, appreciate that the 6kb is
necessary and generated by the compiler so you don't have to do it by hand.
 
V

Vincent Fatica

It's hard to be sure of anything without a complete sample (complete code +
compiler settings + compiler version), but I would say that when you see the
6 Kb increase, it's because the compiler decides that you need exception
safety mechanisms : that is, you've got a local (stack) variable with a
non-trivial destructor. In this situation, in order to be exception-safe,
the compiler needs to add an exception frame to the function where this
variable is declared, and it needs to add all it's internal machinery that
is used during stack-unwinding (eg, "catch" handlers tables, with filters by
exception types....) The stack-unwinding machinery is quite complex and need
a lot of static data (therefore the .text section increase).

Well, Arnaud, that seems very likely. When I remove /EHsc I can no longer cause
the size increase. But I must repeat that whether or not the compiler decides
to add that code is quite serendipitous. In the latest version, with the most
the most going on in the c'tor and d'tor (below), the safety mechanisms are not
added.

ENUM_INFO::ENUM_INFO(BOOL bg)
{
WCHAR szPipeName[32];
Sprintf(szPipeName, L"%s%lu", szPipeNameStub, g.dwPid);
hPipe = CreateNamedPipe(szPipeName, PIPE_ACCESS_INBOUND |
FILE_FLAG_OVERLAPPED, PIPE_WAIT, 1, 0, sizeof(EVENT), 2000, NULL);
Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Overlapped.Offset = Overlapped.OffsetHigh = 0;
bGlobal = bg;
};

ENUM_INFO::~ENUM_INFO()
{
if ( Overlapped.hEvent ) CloseHandle(Overlapped.hEvent);
if ( hPipe ) CloseHandle( hPipe );
};

INT WINAPI function(WCHAR *psz)
{
// blah
// instantiation
ENUM_INFO EnumInfo(stristr(psz, L"/G") ? TRUE : FALSE);
if ( !EnumInfo.hPipe || !EnumInfo.Overlapped.hEvent )
return -1; // do more?
EnumWindows(..., &EnumInfo);
// blah
return 0;
}
 
V

Vincent Fatica

But, let's look at your code.

If the CreateEvent fails in the constructor, you may never CloseHandle on
the pipe. You don't try to use the event handle in the constructor, so the
object becomes live. Then, when you try to use the event handle
(WaitForMultipleObjects perhaps) you could get an access violation. The
compiler will begin stack unwinding, and call your destructor. The
destructor call to CloseHandle the event fails with another access
violation, immediately leaving the destructor. The pipe would not be
closed.

Thanks Ben. I wasn't worrying much about catching errors when trying to sort
out the 6KB thing. I have this sort of thing now (paraphrased)

FOO::FOO()
{
handle1 = ... // NULL on fail
}
FOO:~FOO()
{
if (handle1) CloseHandle(handle1);
}

code:

FOO foo;
if ( !foo.handle1 ) ... // don't do anything rash
else ... // life is right
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top