Edit1:根據一些評論的建議,我列印了變數的地址。
Edit2:根據一些評論的建議,我添加了一些操作,以便編譯器不能簡單地將我的變數扔掉
Edit3:正如一個答案所建議的那樣,我修改了變數的列印方式。
Edit4:添加了很多無意義的東西,讓 gcc 生活困難
Edit5:添加一個片段三來測驗@4386427 的理論——結果看起來支持他的想法,即編譯器默認可以保留 32 位元組。因此,我們可能需要定義至少 5 個變數才能看到差異。
我對堆疊記憶體和堆記憶體有一些基本的了解。以C為例,如果我在一個函式中定義了一個區域變數,它會占用堆疊記憶體;如果我定義一個指標并為其分配一些記憶體塊,這些記憶體塊會占用堆記憶體。如果一個函式遞回地呼叫自己,那么堆疊就會滿了,就會發生溢位。所以我做了一個簡單的測驗,片段一和片段二之間的唯一區別是片段二定義了一個整數:
片段一:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int function(int depth) {
int tmp = rand() % 65536;
tmp = tmp - 1;
printf("val: %d; addr: %p; depth: %d\n", tmp, (void*)&tmp, depth);
tmp = function( depth) 1;
return tmp;
}
int main() {
srand(time(NULL));
int res = function(0);
printf("%d\n", res);
return 0;
}
輸出一:
...
val: 57227; addr: 0x7fff00dff78c; depth: 174626
val: 8288; addr: 0x7fff00dff75c; depth: 174627
val: 24194; addr: 0x7fff00dff72c; depth: 174628
Segmentation fault
片段二:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int function(int depth) {
int tmp0 = rand() % 65536;
int tmp1 = rand() % 65536;
tmp0 = tmp0 - 1;
printf("val: %d, %d; addr: %p, %p; depth: %d\n", tmp0, tmp1, (void*)&tmp0, (void*)&tmp1, depth);
tmp1 = function( depth);
return tmp1 - tmp0;
}
int main() {
srand(time(NULL));
int res = function(0);
printf("%d\n", res);
return 0;
}
輸出二:
...
val: 40745, 32446; addr: 0x7ffcb80b079c, 0x7ffcb80b0798; depth: 174528
val: 34014, 57470; addr: 0x7ffcb80b076c, 0x7ffcb80b0768; depth: 174529
val: 56801, 34478; addr: 0x7ffcb80b073c, 0x7ffcb80b0738; depth: 174530
Segmentation fault
我使用 gcc 編譯了這兩個代碼,正如預期的那樣,它們都會導致堆疊溢位。但是,我最初預期的是,鑒于代碼段 2 中的函式使用 2 倍記憶體,代碼段 2 的深度會淺得多。然而,雖然片段二確實早點出現段錯誤,但兩個堆疊的深度實際上非常接近......
If everything works as my naive theory, function in snippet one calls itself 174,616 times, it needs to occupy 4 Bytes * 174,616 / 1,024 = 682 KBytes; function in snippet two calls itself 174,539 times, it need to occupy (4 4) Bytes * 174,539 = 1,363 KBytes.
So why is it like this?
snippet three
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int function(int depth) {
int tmp0 = rand() % 65536;
int tmp1 = rand() % 65536;
int tmp2 = rand() % 65536;
int tmp3 = rand() % 65536;
int tmp4 = rand() % 65536;
int tmp5 = rand() % 65536;
tmp0 = tmp0 - 1;
tmp1 = tmp1 1;
tmp2 = tmp2 - 2;
tmp3 = tmp3 2;
tmp4 = tmp4 - 3;
tmp5 = tmp5 3;
printf("val: %d, %d, %d; addr: %p, %p, %p; depth: %d\n", tmp0, tmp1, tmp2, (void*)&tmp0, (void*)&tmp1, (void*)&tmp2, depth);
tmp1 = function( depth);
return tmp0 - tmp1 tmp2 - tmp3 tmp4;
}
int main() {
srand(time(NULL));
long res = function(0);
printf("%d\n", res);
return 0;
}
output three
val: 9366, 56113, 48970; addr: 0x7fff063fe830, 0x7fff063fe82c, 0x7fff063fe828; depth: 130920
val: 11924, 11633, 26004; addr: 0x7fff063fe7f0, 0x7fff063fe7ec, 0x7fff063fe7e8; depth: 130921
val: 13316, 42397, 45027; addr: 0x7fff063fe7b0, 0x7fff063fe7ac, 0x7fff063fe7a8; depth: 130922
val: 4285, 58053, 21693; addr: 0x7fff063fe770, 0x7fff063fe76c, 0x7fff063fe768; depth: 130923
Segmentation fault
uj5u.com熱心網友回復:
請記住,區域變數并不是存盤在堆疊中的唯一內容。還有引數和呼叫函式的回傳地址。因此,您更有可能看到至少 16 位元組與 20 位元組的差異,而不是 4 與 8 的差異。
如果您仔細查看每次迭代之間列印的地址,您會發現它們在兩種情況下都相差 48 個位元組,因此給定堆疊幀大約在同一時間達到頂峰是合理的。
因此,編譯器似乎也插入了一些填充,最終在后一種情況下被額外的變數占用。
uj5u.com熱心網友回復:
另一個較小的實驗:
#include <stdio.h>
void function(int depth) {
if (depth==0) return;
int tmp = 65536;
printf("%p\n",&tmp);
function(--depth);
}
void function2(int depth) {
if (depth==0) return;
int tmp = 65536;
int tmp2 = 65536;
printf("%p %p\n",&tmp,&tmp2);
function2(--depth);
}
int main() {
function(2);
function2(2);
return 0;
}
可以產生以下:
0x7ffee80f0988
0x7ffee80f0968
0x7ffee80f0988 0x7ffee80f0984
0x7ffee80f0968 0x7ffee80f0964
在這里可以觀察到兩個區域變數之間的距離對于兩個函式來說是相同的。這可能意味著在這些情況下,堆疊分配的大小是恒定的,并且兩者的大小都相同。如果在第二種情況 (4/5) 中添加更多區域變數,則會發生變化。堆疊幀可能以四舍五入到某個值的大小分配。
uj5u.com熱心網友回復:
那么為什么會這樣呢?
好吧,你所描述的原則上是正確的,但是......你忘記了編譯器優化。只要不改變程式的可觀察行為,編譯器就可以進行各種優化。
例如,編譯器可以決定將所有tmp變數保存在 cpu 暫存器中。在這種情況下,不會有堆疊記憶體分配給它們。
您可以嘗試通過列印它們的地址來強制它們記憶,例如:
printf("%p\n", (void*)&tmp);
可能發生的另一件事是編譯器為兩個函式更改了相同數量的堆疊指標。在我的系統上,編譯器在兩種情況下都保留了 32 個位元組。tmp在編譯器將其更改為 48 之前,我必須使用 5 個變數。換句話說 - 不要指望該函式完全保留它所需要的。允許保留更多。在我的系統上,它似乎總是將堆疊指標更改 N x 16。
為了更好地理解,您需要查看生成的機器代碼。為此,您可以使用gcc -S
你也可以查看這個https://godbolt.org/z/x5c3bj7M9
兩個函式都以:
push rbp
mov rbp, rsp
sub rsp, 32 <------ Stack pointer change, i.e. reserving memory
mov DWORD PTR [rbp-20], edi
所以它們對兩個呼叫使用相同數量的堆疊。
使用編譯的相同代碼-O2給出了完全不同的結果https://godbolt.org/z/f6jMEqaPd
第一個函式給出:
push rbx
mov ebx, edi
sub rsp, 48 <------ Stack pointer change, i.e. reserving memory
但第二個給出:
push rbx
mov ebx, edi
sub rsp, 16 <------ Stack pointer change, i.e. reserving memory
結論是:你不能只看 C 代碼就知道會發生什么。你需要機器碼。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/338890.html
