📜 ⬆️ ⬇️

Stack variables - fast and sometimes dead

FAIL C ++ programs use the so-called automatic storage for local and temporary variables. Usually, automatic memory is implemented on top of the program stack, so it is called stack memory. Its big plus is the allocation and release of memory is extremely fast (usually one processor instruction). Its big minus is a relatively small amount, an attempt to allocate memory in excess of this volume leads to the so-called stack overflow, and then the program crashes.

This implies a restriction - you can not try to allocate too much memory on the stack. Too much? How much it? The answer is not as obvious as you might think at first glance.


')
Too much - just more than what is available. The statement is perfectly accurate, but not very useful.

How much is available depends on various factors. For example, on Windows, for the main thread of the program, the amount of stack memory is specified in the linker settings, and for the remaining threads, when they are created. Maybe the code will be executed in another program (module for the web server), then the stack size will be set by the web server (IIS limits the stack of threads for the user code to 256 kilobytes, although the default size in Windows is 1 megabyte). On other systems, there may be level constraints on the entire system. In any case, the stack size is not very large and does not always depend on the code that uses it for stack variables.

How much stack is used is also not always dependent on the developer.

First, the code never starts by itself - it is launched by someone. There is some entry point - this is a user-defined function to which control is transferred. Before the entry point, there could be a bundle of functions that chained each other along the chain and eventually one of them caused the entry point. They, too, can contribute to the exhaustion of the stack, and the maximum allowed volume will remain less than the developer expects.

Secondly, even in user code, it is not so obvious how much stack memory is used. The reader will be outraged - how not obvious? To take and calculate the amount of memory for all local variables is quite simple. For example, here ...

 //sample 0 void trivial() { char buffer[4 * 1000 * 1000] = {}; MessageBoxA( 0, buffer, buffer, 0 ); } 


... obviously, 4 megabytes of memory is used, and on Windows (with the default stack size) it will not fly.

Well, then the example is more complicated. Go to ideone.com. Disclaimer: at the time of writing the post in C ++ mode on ideone.com, gcc-4.3.4 was used, in other versions of gcc the behavior may be different. Try this code:

 //sample 1 #include <stdio.h> int main() { char buffer[7 * 1000 * 1000] = {}; printf( "%s", buffer ); } 


The result is success. Well, now we try this:

 //sample 2 #include <stdio.h> #include <stdlib.h> int main() { if( rand() ) { char buffer[7 * 1000 * 1000] = {}; printf( "%s", buffer ); } else { char buffer[6 * 1000 * 1000] = {}; printf( "%s", buffer ); } } 

EXTREMELY UNEXPECTEDLY

The result is a runtime error. What happened? Did a rand () call cause stack exhaustion?

Very simple - in most implementations of C ++, the entire amount of stack memory needed by this function is immediately allocated to the function — the compiler simply inserts the “allocate so much” code to the beginning of the function (in Visual C ++, the _chkstk () function is used for this). the compiler, based on what variables it decides to place in memory (some variables are mapped to registers, and some may be in dead code and just be deleted) and in what order.

In the second example, the compiler decided that it was necessary to allocate memory for both arrays, ignoring that these arrays are allocated in mutually exclusive branches of the branch operator. The standard allows this behavior (section 3.7 of ISO / IEC 14882: 2003 (E) speaks of a “minimum lifetime”).

The consumption of stack memory by a particular function, among other things, depends on whether the compiler can reuse the memory occupied by the variables in this function. For example, gcc-4.3.4 failed in the second example. Visual C ++ 10 copes with the second example, but it does not cope in this case (it is assumed that the stack size is 1 megabyte):

 //sample 3 class Temp { public: Temp() { memset( buffer, 0, sizeof( buffer ) ); printf( "%S", buffer ); } void Process() { printf( "%S", buffer ); } private: WCHAR buffer[300 * 1024]; }; int _tmain(int argc, _TCHAR* argv[]) { switch( rand() ) { case 1: Temp().Process(); break; case 2: Temp().Process(); break; default: break; } } 


Now the reader will argue that "there is nothing to allocate memory for such large objects on the stack," and this will be another absolutely true and absolutely useless statement. In the real world, stack exhaustion occurs for less silly reasons.

For example, there was a switch with 19 branches, in each of which a temporary object was created as in the example above. The code worked for years. Then they added the 20th branch with their temporary object. The stack was, say, 256 kilobytes, before function call 56 was already used up. For the complete exhaustion of the stack, then it is enough that each temporary object is on average just over 20 kilobytes, which is not so blatant from the point of view of a Windows developer accustomed to a megabyte stack. 20 kilobytes - still “good developers don't allocate on the stack”? Well, there will be enough 2-kilobyte objects if the stack is almost exhausted.

What can be done? Three options.

Option number one is to create “large” objects not on the stack. The obvious alternative is dynamic memory. She has two problems. First, it will be necessary to take care of the timely and proper removal of the object on its own, but there is no need to reinvent this bike, there are smart pointers. Secondly, the allocation and release of dynamic memory is much slower - one processor instruction cannot be used there, so at least it is worth assessing whether such a transition will give a noticeable slowdown and, if suspicious, profile this code.

Option number two - reduce objects. Maybe it is not necessary to use a 256-kilobyte array, but you can get by with a 4-kilobyte array? This is not always possible and this does not exclude the risk of overflow, but it significantly reduces it in many cases.

Option number three - to divide the function into several, so that the memory for objects with non-intersecting lifetime is distinguished from different functions.

Be prepared for the fact that the compiler does not always allocate memory for stack variables as the developer expects. To win the Darwin Award has long been so easy.

Dmitry Mescheryakov,

product department for developers

Source: https://habr.com/ru/post/131713/


All Articles