遞歸調用的棧溢出估計


代碼規范中不允許遞歸調用,實際開發中應該盡量避免對遞歸的使用,究其原因主要是以下兩點:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM