0

My C++ program is crashing recently, ASAN is showing me a stack-overflow error. I have recently rewritten my program to use multiple threads and since then the program crashes. I suspect the crash happens because of a too deep recursion because apparently on macOS the stack size for threads other than the main thread are reduced. I have compared my source code with other similiar programs. They usually increase the stack size to 8MB for each thread (the linux default) by using pthread_attr_setstacksize. However I am using std::thread, how would I do it with those?

danbo
  • 11
  • 1
  • 5
    The solution to too deep recursion or other stack overflow problems is almost never to increase the stack size. The limit will still be reachable. The solution is usually to lessen the stack usage, for example to limit the recursion depth. – Some programmer dude Dec 06 '20 at 12:40
  • @Someprogrammerdude Apparently it has to be increased, as a comment in a similiar program is saying: `/* On OSX threads other than the main thread are created with a reduced stack size of 512KB by default, this is too low for deep searches, which require somewhat more than 1MB stack, so adjust it to TH_STACK_SIZE. The implementation calls pthread_create() with the stack size parameter equal to the linux 8MB default, on platforms that support it.*/` – danbo Dec 06 '20 at 12:42
  • How deep is your recursion? How many parameters do you provide for your recursive function? How many local variables do you need in your recursive function? Stack size is typically at least 512 KiB, and even if you used 1 KiB per call, you could have a recursion depth of 500+. – Xaver Dec 06 '20 at 12:46
  • If it is really a problem with the stack size (i.e. if you really have a very deep recursion, let's say more than 500), you have to be aware of the fact that standard libraries only provide functionalities that are available on a wide range of platforms. For more specialized use cases like increasing the stack size, there may be no method in the standard library and you might have to use a library which is specifically designed for your target platform. – Xaver Dec 06 '20 at 12:53
  • 1
    `std::thread` gives you a way to get the OS native thread "handle". – Some programmer dude Dec 06 '20 at 12:56
  • @XAVER The maximum recursion depth is **100**, it is an alpha-beta search algorithm. The program crashes at a recursion depth of **43**. I have just summed up the size of all local variables in the function, it creates more than **10KB** per call. Is that too much? – danbo Dec 06 '20 at 12:56
  • There are some dupes, e.g. https://stackoverflow.com/questions/13871763/how-to-set-the-stacksize-with-c11-stdthread/20502804. In a POSIX setting, you can increase stack size with `makecontext` and `swapcontext`. – n. m. could be an AI Dec 06 '20 at 12:59
  • @danbo -- Rewrite the function so that it is iterative, not recursive. Simulate a stack by using a `std::vector` simulating a call stack and parameters. I had the same issue with a program a long time ago, and the latter approach was the solution. I bet if you gave this function to someone, they could rewrite it in maybe a few hours, max, to an iterative one. – PaulMcKenzie Dec 06 '20 at 13:00
  • 1
    Well, a recursion depth of 100 should be no problem, put 10 KiB of local variables per call is quite a lot. If your stack has a size of 512 KiB and you need a depth of 100, you have about 5 KiB per call available on the stack. 10 KiB is twice as much, and that's the problem. – Xaver Dec 06 '20 at 13:00
  • @Xaver Thanks a lot for the help, I think I have already found the problem. – danbo Dec 06 '20 at 13:08
  • 4
    https://en.cppreference.com/w/cpp/thread/thread/native_handle -- As others suggested, do rethink your approach. Blindly mixing threads and deep recursion is going to cause more problems than it solves. – Ulrich Eckhardt Dec 06 '20 at 14:25

1 Answers1

5

A 100 deep recursive call is probably actually an unboundedly deep recursive call. These are generally not safe. Increasing stack size is a patch, it doesn't fix the issue.

Recursion with logarithmic depth is usually ok, as 2^50 is a big data set. But recursion with above-logarithmic depth is usually a sign you need to get fancier.

The simples solution is to start migrating off the stack. Take a look at your recursive state; move it into a struct, and store it on an explicit stack (like a std vector). (This means pointers won't be stable; a std deque or a vector of unique ptrs provides memory stability).

Once your recursive state is explicit, you can say "good enough", because now the call stack is holding next to nothing, or you could rotate your recursive code into iterative code with an explicit call stack of tasks to perform.

That second option can lead to further optimization.

Needless to say, with that kind of refactoring, you'll want some robust unit testing and version control so you don't break everything and can't fix it.

bool search( int const* begin, int const* end, int x ){
  switch(end-begin){
    case 0: return false;
    case 1: return x==*begin;
    default: return search( begin, begin+(end-begin)/2, x) || search(begin+(end-begin)/2, end, x);
  }
}

here is a toy recursive function.

Let us make state explicit.

struct search_state {
  int* begin=0;
  int* end=0;
  int x;
};
bool search( search_state s ){
  switch(s.end-s.begin){
    case 0: return false;
    case 1: return s.x==*s.begin;
    default: return search( {s.begin, s.begin+(s.end-s.begin)/2, x} ) || search( {s.begin+(s.end-s.begin)/2, s.end, s.x} );
  }
}

now migrate to an explicit stack.

bool search( std::vector<search_state>& v ){
  if (v.empty()) return false;
  auto s=v.back();
  v.pop_back();
  switch(s.end-s.begin){
    case 0: return false;
    case 1: return s.x==*s.begin;
    default: {
      v.push_back({s.begin, s.begin+(s.end-s.begin)/2, x} );
      bool r = search(v);
      if(r) return true;
      v.push_back({s.begin+(s.end-s.begin)/2, s.end, s.x} );
      return search(v);
    }
  }
}

next move the return value off of the automatic storage.

void search( std::vector<search_state>& v, bool* retval ){
  if (v.empty()) return false;
  auto s=v.back();
  v.pop_back();
  switch(s.end-s.begin){
    case 0: return;
    case 1: if(s.x==*s.begin)*retval=true; return;
    default: {
      v.push_back({s.begin, s.begin+(s.end-s.begin)/2, x} );
      search(v, retval);
      if(*retval) return;
      v.push_back({s.begin+(s.end-s.begin)/2, s.end, s.x} );
      return search(v, retval);
    }
  }
}

we now have 0 local variables.

We can now convert to an iterative version pretty mechanically.

void search( std::vector<search_state>& v, bool* retval ){
  while( !v.empty() && !*retval){
    auto s=v.back();
    v.pop_back();
    switch(s.end-s.begin){
      case 0: continue;
      case 1: if(s.x==*s.begin) *retval=true; continue;
      default: {
       v.push_back({s.begin, s.begin+(s.end-s.begin)/2, x} );
       v.push_back({s.begin+(s.end-s.begin)/2, s.end, s.x} );
      }
    }
 }

see how similar it is?

Now clean up retval and hide the vector:

bool search( search_state sin ){
  std::vector<search_state> v={sin};
  bool retval=false;
  while( !v.empty() && !retval){
    auto s=v.back();
    v.pop_back();
    switch(s.end-s.begin){
      case 0: continue;
      case 1: if(s.x==*s.begin) retval=true; continue;
      default: {
       v.push_back({s.begin, s.begin+(s.end-s.begin)/2, x} );
       v.push_back({s.begin+(s.end-s.begin)/2, s.end, s.x} );
      }
    }
  }
  return retval;
}

and we are good. You might want to swap the recursive "call" order, as this version does it backwards compared to the baseline recursive version.

Andreas Wenzel
  • 22,760
  • 4
  • 24
  • 39
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524