1、各抒己見
小明說:想要計算 一段算法在所占用的內(nèi)存
A(筆者):
建議看map文件,map文件可以看到data 段 的一些占用size,以armcc 為例,以.o為單位,統(tǒng)一一個.o文件中的data段的size。
所以我建議他放在一個文件,可以看到這個算法中.o文件的data段的大小,即就是全局變量以及靜態(tài)變量所占用的size。
如果有malloc的話,會另算。
棧空間這塊的,我沒有考慮,棧是循環(huán)利用的,不是光算法占用,但是實(shí)際也應(yīng)該考慮,如果棧消耗太大,則也會存在問題。
聽同學(xué)說,如果代碼需要放在內(nèi)存中執(zhí)行,那么這部分Code也需要占內(nèi)存。
B:認(rèn)為:
全局所需內(nèi)存=全局變量(靜態(tài)內(nèi)存部分)+ 局部變量(動態(tài)棧內(nèi)存部分)+malloc(動態(tài)堆內(nèi)存部分),
map只能統(tǒng)計靜態(tài)部分,不能統(tǒng)計動態(tài)部分,因?yàn)閙ap是編譯靜態(tài)產(chǎn)生的,
動態(tài)內(nèi)存分為棧和堆,棧體現(xiàn)在動態(tài)變化的,
map文件中,局部變量是看不到,即便是偏移地址,而在匯編中是可以看到的,(棧中的偏移地址)
C:認(rèn)為:
局部變量是靜態(tài)內(nèi)存,編譯時確定,map里面局部變量的地址是相對于函數(shù)的偏移。
函數(shù)大小包括局部變量大小
動態(tài)內(nèi)存只有堆,沒有棧,如果局部變量很大,則會看到函數(shù)的體積變大
編譯出可執(zhí)行程序后,??臻g就不會增大了。
遞歸多次,只會增大函數(shù)的體積,不會棧超,棧超了鏈接器會報錯。
map文件可以看出棧小,導(dǎo)致棧溢出的問題。
2、筆者分析
筆者來說說看法,經(jīng)過試驗(yàn)得出的結(jié)果,以ARMCC、IAR以及GCC為例
2.1 ARMCC 分析
以一個例程來分析,led.c 最簡單的
?
u32?LEDValue1?=?0XFFFF; const?u32?LEDValue2=0XFFFF; u32?LEDValue3[4]; void?LED_Init(void) {?????? ??GPIO_InitTypeDef??GPIO_InitStructure; ??RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE,?ENABLE);//使能GPIOF時鐘 ??RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_CRC,?ENABLE); ? ??//GPIOF9,F10初始化設(shè)置 ??GPIO_InitStructure.GPIO_Pin?=?GPIO_Pin_11?|?GPIO_Pin_12?|GPIO_Pin_13|?GPIO_Pin_14; ??GPIO_InitStructure.GPIO_Mode?=?GPIO_Mode_OUT;//普通輸出模式 ??GPIO_InitStructure.GPIO_OType?=?GPIO_OType_PP;//推挽輸出 ??GPIO_InitStructure.GPIO_Speed?=?GPIO_Speed_100MHz;//100MHz ??GPIO_InitStructure.GPIO_PuPd?=?GPIO_PuPd_NOPULL;//上拉 ??GPIO_Init(GPIOE,?&GPIO_InitStructure);//初始化 ? ??GPIO_InitStructure.GPIO_Pin?=?GPIO_Pin_5; ??GPIO_Init(GPIOD,?&GPIO_InitStructure);//初始化 ??GPIO_Write(GPIOD,LEDValue1);??//以下代碼都是為了測試 ??GPIO_Write(GPIOE,LEDValue2); ??LEDValue3[0]=0XFFFF; ??GPIO_Write(GPIOE,LEDValue3[0]); } void?LedRun() { ??GPIO_ResetBits(GPIOD,GPIO_Pin_5); ??GPIO_SetBits(GPIOD,GPIO_Pin_5); } 1234567891011121314151617181920212223242526272829303132
?
2.1.1 筆者A觀點(diǎn)
然后打開生成的map文件,可以看到具體編譯好的信息,很方便分析單個.o文件所占用的size信息,比如該文件led.c
從上面Map信息里面看到Led.o的size情況:
Code:156 Byte
RW Data:4Byte
ZI Data:16Byte
RO Data:0Byte
所以如果算法單獨(dú)使用了一個.o文件,在armcc下,很容易分析出數(shù)據(jù)的空間使用大小。但是棧的空間+堆的空間沒有統(tǒng)計到,
堆是運(yùn)行態(tài)的,靜態(tài)編譯出來的無法統(tǒng)計到,需要具體的情況具體分析,單獨(dú)去看malloc這種,或者自己內(nèi)存管理的空間申請。
至于棧的使用空間,編譯階段可能不知道,因?yàn)榫幾g階段不知道調(diào)用關(guān)系,而鏈接的時候則由鏈接器將多個.o文件組織起來,所以可以知道調(diào)用關(guān)系,
一個鏈接選項:–callgraph 就可以生成調(diào)用關(guān)系,
同時會分析出使用棧的情況。從下圖可以看到 main->LED_Init->GPIO_Init ,LED_Init 初始化使用棧24Byte,
接著來分析一下,為啥LED_Init函數(shù)的棧使用了24Byte空間。
大家都知道,數(shù)據(jù)的運(yùn)算以及函數(shù)的調(diào)用,都會用到寄存器,而用寄存器之前需要保存寄存器,所以棧主要是用來保存該函數(shù)用到的寄存器,來看一下匯編,很容易就明白了。
push的時候,都是4字節(jié)對齊的(寄存器都是32位的),所以總共push了6個寄存器,總共24Byte。
?
push?{r2-r6,r14}? 1
?
從上文可以看到LED.o共使用了156Byte,
從map文件中來看,兩個函數(shù)分別是116Byte + 24Byte,共140Byte,由上文可知,共156Byte,那么16Byte就是上文中的inc.data,就是Code中用到了一些數(shù)據(jù),這些數(shù)據(jù)無法直接訪問,需要開辟一塊單元來存儲這些數(shù)據(jù)地址,然后才可以加載。如下面第二張圖所示,比如0x40021000,很明顯這個就不是Code的地址或者RAM的地址,就是一個外設(shè)地址(GPIOE),根據(jù)STM32的手冊可以得知(下面圖三)。
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述
上文中RW Data 或者ZI Data 符合預(yù)期,但是RO Data 很奇怪是0,因?yàn)槲覀儽旧矶x了一個const的類型的數(shù)據(jù),但是統(tǒng)計竟然是0,
?
u32?LEDValue1?=?0XFFFF; const?u32?LEDValue2=0XFFFF; u32?LEDValue3[4]; 123
?
這個需要從匯編入手,編譯器也不傻,定義一個const 類型的數(shù)據(jù),編譯器會就生成一個RO data嗎,不一定,比如本文這個,編譯器直接將0xFFFF 編譯到指令中,而不是從變量中加載數(shù)據(jù),這個需要從匯編中看。
如何才能產(chǎn)生一個RO data呢?如果引用到變量的地址,那么肯定會產(chǎn)生一個RO data,因?yàn)樾枰峙渥兞康刂?。例如下文中這樣。
?
??u32?*??data_p?=?(u32*)&LEDValue2; ??*data_p?=?*data_p?+1; ??GPIO_Write(GPIOE,LEDValue2); 123
?
然后分析map文件,可以看到RO data Size 為4,led.o中有了RO的變量以及地址,也可以看到ro data的地址不是在sram,而是在flash中(ROM)中,最后匯編也為const 變量申請了存儲空間(LEDValue2)
2.1.2 B同學(xué)觀點(diǎn)
對于B同學(xué)的觀點(diǎn),我基本 都是贊同的。補(bǔ)充一下:就是所需要的內(nèi)存,可能還需要加上Code所需要的空間(如果有這種場景的話,在內(nèi)存中允許代碼)
對于棧是動態(tài)的理解,我的想法也是棧是動態(tài)變化的
函數(shù)調(diào)用完成之后,棧就釋放了,還可以重新使用,
和堆相似,但是和堆不同的是,棧動態(tài)變化過程是相對固定的,就是編譯器編譯好指令之后,每個函數(shù)的棧使用Size就確定了,不會在變化了。
唯一變化的可能就是
一級一級的調(diào)用棧
,這個鏈接器統(tǒng)計的有些情況可能不準(zhǔn),(統(tǒng)計最大size)
比如出現(xiàn)環(huán)形調(diào)用,統(tǒng)計出來的情況就不準(zhǔn),類似遞歸調(diào)用,準(zhǔn)是有出口的,但是編譯器不知道,就會統(tǒng)計出錯。
還比如出現(xiàn)函數(shù)指針調(diào)用,編譯器可能也無法統(tǒng)計出最大的調(diào)用棧size,無法統(tǒng)計出具體的調(diào)用關(guān)系。
map文件是看不到局部變量的,原因有兩點(diǎn),
棧是動態(tài)變化的,會覆蓋掉,
而且如果多個函數(shù)調(diào)用,調(diào)用路徑不一樣,那么在棧中的偏移地址也不固定,所以說看不到的,
即便是匯編中,可以看到的是部分變量壓棧,其他的可能還是在寄存器中使用,所以基本上地址無法確定。
map文件中看到的 全局變量 或者局部靜態(tài)變量。
2.1.2 C同學(xué)觀點(diǎn)
對于C同學(xué)的觀點(diǎn),很多我都有不同的意見,
對于第一條,map文件可以看到局部變量地址,這個我可以肯定是看不到的,除非進(jìn)入函數(shù)那一刻,去獲取地址,但是靜態(tài)的map文件分析是看不到的。而且變量和代碼是分開存放的,及時能看到,也不在同一個區(qū)域,怎么可能是函數(shù)地址的偏移呢???
對于第二條,函數(shù)的大小,我認(rèn)為不包括局部變量的大小,局部變量的使用在棧中(寄存器),而棧的使用體現(xiàn)在sp的變化,也就是指令上面,從map文件中也可以看到LED_Run這個函數(shù)的大小是24Byte,在匯編中統(tǒng)計一下指令的大?。ㄗ筮吶ψ〉模『靡彩?4Byte。
上面那個LedRun函數(shù)可能沒有局部變量,那我們來加一個局部變量來看看,例如下面的代碼,如果包括局部變量,那么函數(shù)的size一定會超過100,畢竟還有指令的size,實(shí)際編出來 的map文件分析,看到函數(shù)大小為64,分析匯編代碼,指令數(shù)也是64,可以得出結(jié)論,函數(shù)的大小是不包括局部變量的。
?
void?LedRun() { ??u16?LEDData[100]={123}; ??u8?i=0; ??for(i=0;i<100;i++) ????GPIO_Write(GPIOE,LEDData[i]);?//測試,可能沒意義 ??GPIO_ResetBits(GPIOD,GPIO_Pin_5); ??GPIO_SetBits(GPIOD,GPIO_Pin_5); } 123456789
?
o文件的大小 包括了 code、data(RO RW ZI)的大小,也沒包括局部變量的大小。
但是函數(shù)本身使用的局部變量空間是可以統(tǒng)計出來的,剛剛也看到了,通過鏈接器生成的信息。
216 = (1002)+ Push(44 寄存器r4 r5 r6 r14)
對于第三條,如第二條所述
第四條,程序編譯好,??臻g的情況就不會變化了,這個也不是一定,比如有那種bank機(jī)制(下次介紹),由于Flash空間的限制,一些不常用的程序存放在nand里面或者其他spi nor flash里面,等用到的時候再加載,這樣棧的空間使用也會相對的動態(tài)增加,當(dāng)然這屬于一種特殊情況。
第五條,棧超了會報鏈接錯誤,這個不會的,鏈接器存在環(huán)這種情況的時候,統(tǒng)計出來的棧使用是不準(zhǔn)的,所以沒法報錯誤,如果棧溢出了,可能會將其他空間踩了,引入其他bug。
舉例說明,本程序的??臻g是0x400,還是剛剛的程序,同樣可以編譯過,沒有報任何錯誤,還統(tǒng)計出來棧的使用情況(2064> 0x400(1024))。
?
void?LedRun() { ??u16?LEDData[0x400]={123}; ??u16?i=0; ??for(i=0;i<0x400;i++) ????GPIO_Write(GPIOE,LEDData[i]);?//測試,可能沒意義 ??GPIO_ResetBits(GPIOD,GPIO_Pin_5); ??GPIO_SetBits(GPIOD,GPIO_Pin_5); } 123456789

?
第六條,map文件會分析棧的情況,好像也沒有,至少對于armcc 編譯器來說,沒有統(tǒng)計棧的使用情況,而是在一個鏈接選項中 會專門生成棧的調(diào)用關(guān)系,以及所使用的棧情況。
以上就是筆者分析的一些情況,有不同分意見可以分享評論。后面簡單以IAR以及arm-gcc 分析,看看是否有所不同。
附錄:
審核編輯:湯梓紅
?
評論