递归调用的栈溢出估计
代码规范中不允许递归调用,实际开发中应该尽量避免对递归的使用,究其原因主要是以下两点:
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字节
进一步增大迭代次数和线程栈(计算4000的阶乘,栈空间定位256KB),将得到的数据绘制分析曲线如下:
(X轴是阶乘计算的总数n,Y轴是平均每次阶乘栈的开销KB):
可以看出来,每次平均每次栈的开销值并非线性增长,所以评估时注意使用最终持平的那个单次开销去估算。
保险期间,实际项目中用最高约束规格去做一把验证性测试。