14

I have the code listed below and it reports a stack overflow when I run it. I use pass by value to showTest(). What I expect is that it will make a copy of the Test struct to the stack (pushed to the stack), then at the end of the function call the Test struct will be released (popped off of the stack). So I make a call for three times. It is supposed to push onto the stack and pop off at the end of each function call.

I don't see any stack issue if it pushes on and pops off of the stack each time the function is called. However, when I run this code, it reports a stack overflow exception on the first line of main. (I use Visual Studio 2017.)

If I remove one of the showTest() function calls then I can get it work.

Any feedback would be highly appreciated.

#include <iostream>

struct Test
{
  static int Userid;
  int data[100000] = { };

  Test()
  {
    ++Userid;
  };
};

int Test::Userid = 0;

void showTest(Test i_myint)
{
  std::cout << "test" << std::endl;
}

int main()
{
  Test *pint = new Test();
  showTest(*pint);
  Test *pint2 = new Test();
  showTest(*pint2);
  Test *pint3 = new Test();
  showTest(*pint3);
  return 0;
}
1201ProgramAlarm
  • 32,384
  • 7
  • 42
  • 56
  • What happens if you reduce the array size down to 10k? Are you on a 64 bit or 32 bit architecture? – FreudianSlip Jul 09 '18 at 05:18
  • 4
    Each of those arrays is ~400k. Visual Studio has a default stack size of ~1mb. Two squeezes by and 3 blows the stack apparently. So, now what we, and apparently you, know that, what's the actual question? The answer is don't do that. – Retired Ninja Jul 09 '18 at 05:28
  • 2
    @RetiredNinja Why doesn't it reuse the stack from the first call? – melpomene Jul 09 '18 at 05:59
  • Who knows? If OP shares the compiler used, we could look at the assembler and find out. – Useless Jul 09 '18 at 12:11
  • Note that some operating systems will not support objects this large on the stack, because they only have one or two guard pages mapped that cause an extension of the stack mapping when accessed. Skipping over these will get you unmapped memory. – Simon Richter Jul 09 '18 at 13:30
  • @Retired Ninja, actually I do this for R&D/learning purpose to understand stack behaviour. The answer I'm looking is from n.m below. Thanks for your input. – Mohamad Khaizul Zakaria Jul 10 '18 at 07:56

1 Answers1

26

What evidently happens here is deferred stack popping.

In C and C++ the common calling convention is that the caller pops arguments from the stack. As a common optimisation, many compilers don't pop arguments after each call, but do that after a number of calls and pop all the accumulated arguments together. This saves a few instructions, at the expense of a larger stack which may overflow.

In MSVC, when optimisations are disabled, the compiler allocates and checks the stack beforehand for all the calls it will need in a given function. This is why the program crashes even before it prints anything.

See corresponding assembly

Some of the first instructions in main are

    mov      eax, 1200120       ; 00124ff8H
    call     __chkstk
    sub      rsp, rax

This number is exactly what is needed to fit three instances of the object on the stack.

When optimisations are enabled, the compiler is smart enough to reuse the stack, so there's no crash.

Optimised assembly

    mov      eax, 400032          ; 00061aa0H
    call     __chkstk
    sub      rsp, rax

Enough for a single instance.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Good catch, though the objects in questions are temporaries whose lifetimes do not overlap. It should take an optimizer to figure out that the space can be reused. Maybe it has something to do with debuggability. (As a side note, the standard does not prescribe a minimum stack size in [implimits], though there is a "minimum maximum" object size of 256kiB, i.e. 262144 bytes.) – Arne Vogel Jul 09 '18 at 12:25
  • n.m Thanks for your answer. This is the answer I'm looking for. – Mohamad Khaizul Zakaria Jul 10 '18 at 07:52