递归调用的栈溢出估计

代码规范中不允许递归调用,实际开发中应该尽量避免对递归的使用,究其原因主要是以下两点:

1. 嵌套深度上会存在一定风险,递归层数过多,不断压栈,可能会引起栈溢出的问题;

2. 代码可读性,不太容易被后面维护的人理解;

但是,凡事总有例外。

比如要有一种需求场景,需要遍历一个目录下的所有文件,包括其中子目录中的文件,然后将满足一定条件的文件筛选出来,

你会发现,用递归去设计反而会比较简单。

对于解决一些包含重复类似逻辑的问题,递归对于开发人员来说是一个反而比较清晰的选择。

本文主要介绍,不得不使用递归时,针对上述第一个风险,如何评估栈空间是否足够。

评估思路:

1. 确认当前线程栈空间限制cur_stack_size是多少?

2. 递归调用n次,分析n次压栈后栈空间的损耗cost_size大约大少?

3. 结合业务评估,预估一个最大可能的递归调用次数max

4. (max*cost_size/n) 如果大于或已经接近 cur_stack_size, 表示存在栈越界风险,需要放大栈空间或者做功能规格约束

 

具体以一个例子来说明,main函数中启动一个线程,线程栈大小可配,线程中递归计算一个阶乘:

  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 #include <pthread.h>
  5 #include <errno.h>
  6 #include <limits.h>
  7 #include <sys/types.h>
  8 #include <sys/stat.h>
  9 #include <fcntl.h>
 10 
 11 static char* stack_begin = NULL;
 12 static char* stack_end = NULL;
 13 static int totalnum = 0;
 14 static const char* filepath = "datafile.txt";
 15 
 16 static unsigned long factor(int n)
 17 {
 18     unsigned long ulRet = 0;
 19     
 20     if (n == totalnum) 
 21     {
 22         stack_begin = (char*)(&ulRet); 
 23     }
 24     if (1 == n)
 25     {
 26         stack_end =(char*)(&ulRet);
 27         ulRet = 1; 
 28     }
 29     else
 30     {
 31         
 32         ulRet = n*factor(n-1); 
 33     }
 34 #if 0 
 35         printf("[%5d], begin:%p,end:%p, &n:%p\n", n, stack_begin,stack_end, &n);
 36 #endif
 37     return ulRet;
 38 }
 39 
 40 
 41 static int traverse_test(int test_num, int stack_size)
 42 {
 43     int i = 0;
 44     int fd = -1;
 45     int num = 0; 
 46     int iret = 0; 
 47     int stacksize = 0;
 48     long theoretical_max = 0; 
 49     float percost = 0.0;
 50     float stackcost = 0.0;
 51     pthread_t thread_id;
 52     pthread_attr_t attr; 
 53     char info[256];
 54     
 55     num = test_num;
 56     stacksize = stack_size * 1024;
 57     printf("--------- num :%d, stacksize:%d ---------\n",test_num,stack_size);
 58 
 59     fd = open(filepath,O_CREAT|O_RDWR|O_APPEND,0777);
 60     if (0 > fd) 
 61     {
 62         printf("open file failed, err:%d,%s\n",errno, strerror(errno)); 
 63         return -1;
 64     }
 65     else
 66     {
 67         (void)truncate(filepath,0); 
 68         lseek(fd, 0, SEEK_SET); 
 69     }
 70 
 71     memset(info,0,sizeof(info)); 
 72     snprintf(info, sizeof(info),"%s %s %s %s %s\n","num", "stacksize(KB)","percost(KB)","stackcost(KB)","maxNum"); 
 73     (void)write(fd, info, strnlen(info, sizeof(info)));
 74 
 75     if (0 != pthread_attr_init(&attr))
 76     {
 77         printf("pthread attr init err, errno:%d!!!!\n",errno);
 78         iret = -1;
 79         goto err_exit; 
 80     }
 81     if (0 != pthread_attr_setstacksize(&attr, stacksize))
 82     {
 83         printf("pthread set stack err, min[%d],set[%d],errno:%d,err:%s!!!!\n",
 84             PTHREAD_STACK_MIN,stacksize,errno,strerror(errno));
 85         iret = -1;
 86         goto err_exit; 
 87     }
 88 
 89     for (i = 2; i <= num; i++)
 90     {
 91         /*start a pthread to call recursive*/
 92         memset(&thread_id,0,sizeof(thread_id));
 93         stack_begin = 0;
 94         stack_end = 0; 
 95         totalnum = i;
 96         if (0 != pthread_create(&thread_id,&attr, factor,i))
 97         {
 98             printf("pthread create err, errno:%d!!!!\n",errno);
 99             iret = -1;
100             goto err_exit; 
101         } 
102         if (0 != pthread_join(thread_id, NULL))
103         {
104             printf("pthread join err, errno:%d!!!!\n",errno);
105             iret = -1;
106             goto err_exit; 
107         }
108 
109         percost =  (float)(stack_begin - stack_end)/(float)i;
110         stackcost = (float)(stack_begin - stack_end)/1024.0;
111         theoretical_max = (stacksize*i)/(stack_begin - stack_end);
112         memset(info,0,sizeof(info)); 
113         snprintf(info, sizeof(info),"%d %d %.2f %.2f %ld\n", i, stack_size, percost,stackcost,theoretical_max); 
114         (void)write(fd, info, strnlen(info, sizeof(info)));
115 
116         if (1 == i || 0 == i % 10)
117         {
118             printf("testnum[%d], stacksize[%d]KB, percost[%.2f]Byte, stackcost:[%.2f]KB, max maybe:[%ld],!!!!\n",
119                 i, stack_size,percost,stackcost,theoretical_max);
120         }
121     }
122     iret = 0;
123 
124 err_exit:
125     if (0 != pthread_attr_destroy(&attr))
126     {
127         printf("pthread attr destroy err, errno:%d!!!!\n",errno);
128     }
129     close(fd);
130     return iret;
131 }
132 
133 int main(int argc, char* argv[])
134 {
135     int num = 0;
136     int stacksize = 0;
137 
138     num = atoi(argv[1]);
139     stacksize = atoi(argv[2]);
140 
141     if (0 != traverse_test(num,stacksize))
142     {
143         printf("err happen!!!\n");
144         return -1; 
145     }
146 
147     return 0;
148 }

 

编译运行, 测试10的阶乘,线程栈配置为16KB:

gcc -g test.c -pthread -o test;./test 10 16

结果如下:

10次递归,栈开销大约: 0x7feef0a2bebc – 0x7feef0a2bbec = 0x2d0,, 720字节

image

进一步增大迭代次数和线程栈(计算4000的阶乘,栈空间定位256KB),将得到的数据绘制分析曲线如下:

(X轴是阶乘计算的总数n,Y轴是平均每次阶乘栈的开销KB):

可以看出来,每次平均每次栈的开销值并非线性增长,所以评估时注意使用最终持平的那个单次开销去估算

保险期间,实际项目中用最高约束规格去做一把验证性测试。

image

posted @ 2019-06-09 22:09  doctorJ  阅读(2160)  评论(0编辑  收藏  举报