[英]Optimizing for space instead of speed in C++
我從極其受限的嵌入式內存環境中獲得的經驗:
你可以做很多事情來減少你的記憶足跡,我相信人們已經有關於這個主題的書籍,但是一些主要的是:
編譯器選項以減少代碼大小(包括-Os和打包/對齊選項)
用於去除死代碼的鏈接器選項
如果您從閃存(或ROM)加載到ram執行(而不是從閃存執行),則使用壓縮的閃存映像,並使用引導加載程序對其進行解壓縮。
使用靜態分配:堆是一種分配有限內存的低效方法,如果它受到約束,則可能由於碎片而失敗。
查找堆棧高水印的工具(通常它們用模式填充堆棧,執行程序,然后查看模式保留的位置),這樣就可以最佳地設置堆棧大小
當然,優化用於內存占用的算法(通常以犧牲速度為代價)
const
聲明常量數據表。 這樣可以避免數據從閃存復制到RAM Unix哲學的規則之一可以幫助使代碼更緊湊:
表示規則:將知識折疊成數據,因此程序邏輯可以是愚蠢和健壯的。
我無法計算有多少次我看到精心設計的分支邏輯,跨越許多頁面,可以折疊成一個很好的緊湊的規則,常量和函數指針表。 狀態機通常可以這種方式表示(狀態模式)。 命令模式也適用。 這完全是關於聲明性和命令式編程風格。
而不是記錄純文本,記錄事件代碼和二進制數據。 然后使用“短語手冊”重新構建事件消息。 短語中的消息甚至可以包含printf樣式的格式說明符,以便事件數據值在文本中整齊地顯示。
每個線程都需要它自己的內存塊用於堆棧和TSS。 如果您不需要搶占,請考慮讓您的任務在同一個線程內協同執行( 協作多任務 )。
為了避免堆碎片,我經常看到單獨的模塊囤積大型靜態內存緩沖區供自己使用,即使只是偶爾需要內存。 可以使用內存池,因此內存僅“按需”使用。 但是,這種方法可能需要仔細分析和檢測,以確保池在運行時不會耗盡。
在只有一個應用程序無限期運行的嵌入式系統中,您可以以一種不會導致碎片的合理方式使用動態分配:只需在各種初始化例程中動態分配一次,並且永遠不會釋放內存。 reserve()
您的容器到正確的容量,不要讓它們自動生長。 如果需要頻繁分配/釋放數據緩沖區(例如,用於通信數據包),則使用內存池。 我甚至擴展了C / C ++運行時,以便在初始化序列之后嘗試動態分配內存時,它會中止我的程序。
從鏈接器生成映射文件。 它將顯示如何分配內存。 在優化內存使用時,這是一個很好的開始。 它還將顯示所有功能以及代碼空間的布局方式。
與所有優化一樣,首先優化算法,然后優化代碼和數據,最后優化編譯器。
我不知道你的程序是做什么的,所以我不能對算法提出建議。 許多人都寫過關於編譯器的文章。 所以,這里有一些關於代碼和數據的建議:
這是一本關於這個主題的書。 小內存軟件:內存有限的系統模式 。
使用/ Os在VS中編譯。 通常,這甚至比優化速度更快,因為較小的代碼大小==較少的分頁。
應在鏈接器中啟用Comdat折疊(默認情況下,在發布版本中)
數據結構包裝要小心; 通常這會導致編譯器生成更多代碼(==更多內存)以生成訪問未對齊內存的程序集。 使用1位作為布爾標志是一個典型的例子。
另外,在選擇具有更好運行時間的算法的內存有效算法時要小心。 這是過早優化的地方。
好的大部分都已經提到了,但無論如何這里是我的清單:
最后但同樣重要的是 - 在尋找盡可能小的代碼大小時 - 不要過度 。 還要注意性能和可維護性。 過度優化的代碼往往會很快衰減。
首先,告訴編譯器優化代碼大小。 GCC有-Os
標志。
其他所有東西都在算法級別 - 使用類似的工具來查找內存泄漏,而是查找可以避免的alloc和frees。
另外看一下常用的數據結構打包 - 如果你可以削減一兩個字節,你就可以大幅減少內存使用量。
如果您正在尋找一種分析應用程序堆使用情況的好方法,請查看valgrind的massif工具。 它可以讓您隨着時間的推移拍攝應用程序的內存使用情況配置文件的快照,然后您可以使用該信息更好地查看“低懸的水果”的位置,並相應地針對您的優化。
其他人建議:
限制使用c ++特性,像ANSI C一樣編寫,帶有次要擴展。 標准(std::)模板使用大型動態分配系統。 如果可以,請完全避免使用模板。 雖然本質上不是有害的,但它們很容易從一些簡單,干凈,優雅的高級指令生成大量的機器代碼。 這鼓勵寫作的方式 - 盡管所有“清潔代碼”的優點 - 是非常渴望記憶。
如果您必須使用模板,編寫自己的模板或使用專為嵌入式使用而設計的模板,將固定大小作為模板參數傳遞,並編寫測試程序,以便測試模板並檢查-S輸出以確保編譯器不會生成可怕的匯編代碼來實例化它。
手動對齊結構,或使用#pragma pack
{char a; long b; char c; long d; char e; char f; } //is 18 bytes,
{char a; char c; char d; char f; long b; long d; } //is 12 bytes.
出於同樣的原因,使用集中式全局數據存儲結構而不是分散的本地靜態變量。
智能地平衡malloc()/ new和static結構的使用。
如果您需要給定庫的功能子集,請考慮編寫自己的庫。
展開短循環。
for(i=0;i<3;i++){ transform_vector[i]; }
比...長
transform_vector[0];
transform_vector[1];
transform_vector[2];
不要那么長時間那樣做。
將多個文件打包在一起,讓編譯器內聯短函數並執行各種優化,鏈接器不能。
不要害怕在程序中寫“小語言”。 有時,一個字符串表和一個解釋器可以完成很多工作。 例如,在我所使用的系統中,我們有很多內部表,必須以各種方式訪問(循環,無論如何)。 我們有一個內部命令系統,用於引用表格,這些表格構成了一種中間語言,它非常緊湊。
不過要小心! 知道你正在寫這樣的東西(我自己寫了一個,我自己寫了),然后記下你在做什么。 最初的開發人員似乎並沒有意識到他們正在做什么,所以管理起來比應該更難。
優化是一個流行的術語,但在技術上通常是不正確的。 它確實意味着要做到最佳。 對於速度或尺寸,實際上從未實現這樣的條件。 我們可以簡單地采取措施來實現優化。
用於向計算結果移動到最小時間的許多(但不是全部)技術犧牲了存儲器需求,並且用於移向最小存儲器要求的許多(但不是全部)技術延長了結果的時間。
減少內存需求相當於一定數量的通用技術。 很難找到不完全適合這些中的一個或多個的特定技術。 如果您完成了所有這些操作,那么即使不是絕對最小值,您也可以獲得非常接近程序最小空間要求的東西。 對於一個真正的應用程序,可能需要一個有經驗的程序員團隊一千年才能完成它。
這是該主題的計算機科學視圖,而不是開發人員的視圖。
例如,打包數據結構是結合上述(3)和(9)的努力。 壓縮數據是至少部分地實現上述(1)的方式。 減少更高級編程結構的開銷是在(7)和(8)中實現一些進展的一種方式。 動態分配是嘗試利用多任務環境來使用(3)。 編譯警告,如果打開,可以幫助(5)。 析構函數試圖協助(6)。 套接字,流和管道可用於完成(2)。 簡化多項式是一種在(8)中獲得基礎的技術。
理解九的含義以及實現它們的各種方法是多年學習和檢查編譯產生的內存映射的結果。 由於可用內存有限,嵌入式程序員通常會更快地學習它們。
在gnu編譯器上使用-Os選項向編譯器發出請求以嘗試查找可以轉換以完成這些的模式,但是-Os是一個聚合標志,它打開了許多優化功能,每個功能都嘗試執行轉換以完成上述9個任務之一。
編譯器指令可以在沒有程序員努力的情況下產生結果,但編譯器中的自動化過程很少糾正由於代碼編寫者缺乏意識而產生的問題。
請記住一些C ++功能的實現成本,例如虛擬功能表和創建臨時對象的重載運算符。
除了其他人都說,我只想添加不使用虛函數,因為虛擬函數必須創建一個可以占用誰知道多少空間的VTable。
還要注意例外情況。 使用gcc,我不相信每個try-catch塊的大小都在增長(除了每個try-catch的2個函數call
),但是有一個固定大小的函數必須鏈接,其中可能浪費寶貴的字節
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.