簡體   English   中英

在C ++中優化空間而不是速度

[英]Optimizing for space instead of speed in C++

當你說“優化”時,人們傾向於認為“速度”。 但是速度並不是那么關鍵的嵌入式系統呢,但內存是一個主要的限制因素呢? 什么是一些指南,技術和技巧可用於削減ROM和RAM中的額外千字節? 一個“配置文件”代碼如何查看內存膨脹的位置?

PS One可能會爭辯說,在嵌入式系統中“過早地”優化空間並不是那么邪惡,因為你給自己留下了更多的數據存儲空間和功能蠕變。 它還允許您降低硬件生產成本,因為您的代碼可以在較小的ROM / RAM上運行。

PPS也歡迎參考文章和書籍!

PPPS這些問題密切相關: 4046151561629

我從極其受限的嵌入式內存環境中獲得的經驗:

  • 使用固定大小的緩沖區 不要使用指針或動態分配,因為它們有太多的開銷。
  • 使用有效的最小int數據類型。
  • 不要使用遞歸。 始終使用循環。
  • 不要傳遞大量的函數參數。 請改用全局變量。 :)

你可以做很多事情來減少你的記憶足跡,我相信人們已經有關於這個主題的書籍,但是一些主要的是:

  • 編譯器選項以減少代碼大小(包括-Os和打包/對齊選項)

  • 用於去除死代碼的鏈接器選項

  • 如果您從閃存(或ROM)加載到ram執行(而不是從閃存執行),則使用壓縮的閃存映像,並使用引導加載程序對其進行解壓縮。

  • 使用靜態分配:堆是一種分配有限內存的低效方法,如果它受到約束,則可能由於碎片而失敗。

  • 查找堆棧高水印的工具(通常它們用模式填充堆棧,執行程序,然后查看模式保留的位置),這樣就可以最佳地設置堆棧大小

  • 當然,優化用於內存占用的算法(通常以犧牲速度為代價)

一些明顯的

  • 如果速度不重要,請直接從閃存執行代碼。
  • 使用const聲明常量數據表。 這樣可以避免數據從閃存復制到RAM
  • 使用最小的數據類型緊密打包大型數據表,並以正確的順序避免填充。
  • 對大型數據集使用壓縮(只要壓縮代碼不超過數據)
  • 關閉異常處理和RTTI。
  • 有沒有人提到使用-Os? ;-)

將知識折疊成數據

Unix哲學的規則之一可以幫助使代碼更緊湊:

表示規則:將知識折疊成數據,因此程序邏輯可以是愚蠢和健壯的。

我無法計算有多少次我看到精心設計的分支邏輯,跨越許多頁面,可以折疊成一個很好的緊湊的規則,常量和函數指針表。 狀態機通常可以這種方式表示(狀態模式)。 命令模式也適用。 這完全是關於聲明性和命令式編程風格。

記錄代碼+二進制數據而不是文本

而不是記錄純文本,記錄事件代碼和二進制數據。 然后使用“短語手冊”重新構建事件消息。 短語中的消息甚至可以包含printf樣式的格式說明符,以便事件數據值在文本中整齊地顯示。

最小化線程數

每個線程都需要它自己的內存塊用於堆棧和TSS。 如果您不需要搶占,請考慮讓您的任務在同一個線程內協同執行( 協作多任務 )。

使用內存池而不是囤積

為了避免堆碎片,我經常看到單獨的模塊囤積大型靜態內存緩沖區供自己使用,即使只是偶爾需要內存。 可以使用內存池,因此內存僅“按需”使用。 但是,這種方法可能需要仔細分析和檢測,以確保池在運行時不會耗盡。

僅在初始化時動態分配

在只有一個應用程序無限期運行的嵌入式系統中,您可以以一種不會導致碎片的合理方式使用動態分配:只需在各種初始化例程中動態分配一次,並且永遠不會釋放內存。 reserve()您的容器到正確的容量,不要讓它們自動生長。 如果需要頻繁分配/釋放數據緩沖區(例如,用於通信數據包),則使用內存池。 我甚至擴展了C / C ++運行時,以便在初始化序列之后嘗試動態分配內存時,它會中止我的程序。

從鏈接器生成映射文件。 它將顯示如何分配內存。 在優化內存使用時,這是一個很好的開始。 它還將顯示所有功能以及代碼空間的布局方式。

與所有優化一樣,首先優化算法,然后優化代碼和數據,最后優化編譯器。

我不知道你的程序是做什么的,所以我不能對算法提出建議。 許多人都寫過關於編譯器的文章。 所以,這里有一些關於代碼和數據的建議:

  • 消除代碼中的冗余。 任何重復的代碼都是三行或更多行,在代碼中重復三次,應該更改為函數調用。
  • 消除數據中的冗余。 找到最緊湊的表示:合並只讀數據,並考慮使用壓縮代碼。
  • 通過常規分析器運行代碼; 消除所有未使用的代碼。

這是一本關於這個主題的書。 小內存軟件:內存有限的系統模式

使用/ Os在VS中編譯。 通常,這甚至比優化速度更快,因為較小的代碼大小==較少的分頁。

應在鏈接器中啟用Comdat折疊(默認情況下,在發布版本中)

數據結構包裝要小心; 通常這會導致編譯器生成更多代碼(==更多內存)以生成訪問未對齊內存的程序集。 使用1位作為布爾標志是一個典型的例子。

另外,在選擇具有更好運行時間的算法的內存有效算法時要小心。 這是過早優化的地方。

好的大部分都已經提到了,但無論如何這里是我的清單:

  • 了解編譯器可以執行的操作。 閱讀編譯器文檔,試驗代碼示例。 檢查設置。
  • 檢查目標優化級別的生成代碼 有時結果令人驚訝,而且通常情況下,優化實際上會降低速度(或者只是占用太多空間)。
  • 選擇合適的記憶模型 如果您的目標是非常小的緊湊系統,那么大型或大型內存模型可能不是最佳選擇(但通常最容易編程......)
  • 首選靜態分配 僅在啟動時或靜態分配的緩沖區(池或最大實例大小的靜態緩沖區)上使用動態分配。
  • 使用C99樣式數據類型 對存儲類型使用最小的足夠數據類型。 對於“快速”數據類型,諸如循環變量之類的局部變量有時更有效。
  • 選擇內聯候選人。 在內聯時,一些具有相對簡單體的參數重函數更好。 或者考慮傳遞參數的結構。 Globals也是選擇,但要小心 - 如果其中的任何人沒有足夠的訓練,測試和維護就會變得困難。
  • 請使用const關鍵字,注意數組初始化的含義。
  • 映射文件 ,理想情況下也是模塊大小。 還要檢查crt中包含的內容(是否真的有必要?)。
  • 遞歸只是說不(有限的堆棧空間)
  • 浮點數 - 更喜歡定點數學。 傾向於包含並調用大量代碼(即使是簡單的加法或乘法)。
  • C ++你應該知道C ++非常好。 如果不這樣做,請用C編程對嵌入式系統進行編程。 那些膽敢的人必須小心所有高級C ++結構(繼承,模板,異常,重載等)。 考慮接近HW代碼而不是Super-C,並且在重要的地方使用C ++:高級邏輯,GUI等。
  • 禁用編譯器設置中不需要的任何內容(無論是庫的一部分,語言結構等)

最后但同樣重要的是 - 在尋找盡可能小的代碼大小時 - 不要過度 還要注意性能和可維護性。 過度優化的代碼往往會很快衰減。

首先,告訴編譯器優化代碼大小。 GCC有-Os標志。

其他所有東西都在算法級別 - 使用類似的工具來查找內存泄漏,而是查找可以避免的alloc和frees。

另外看一下常用的數據結構打包 - 如果你可以削減一兩個字節,你就可以大幅減少內存使用量。

分析代碼或數據膨脹可以通過映射文件完成:對於gcc,請參見此處 ,對於VS,請參見此處
我還沒有看到用於大小分析的有用工具(並且沒有時間修復我的VS AddIn hack)。

如果您正在尋找一種分析應用程序堆使用情況的好方法,請查看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];

不要那么長時間那樣做。

將多個文件打包在一起,讓編譯器內聯短函數並執行各種優化,鏈接器不能。

不要害怕在程序中寫“小語言”。 有時,一個字符串表和一個解釋器可以完成很多工作。 例如,在我所使用的系統中,我們有很多內部表,必須以各種方式訪問​​(循環,無論如何)。 我們有一個內部命令系統,用於引用表格,這些表格構成了一種中間語言,它非常緊湊。

不過要小心! 知道你正在寫這樣的東西(我自己寫了一個,我自己寫了),然后記下你在做什么。 最初的開發人員似乎並沒有意識到他們正在做什么,所以管理起來比應該更難。

優化是一個流行的術語,但在技術上通常是不正確的。 它確實意味着要做到最佳。 對於速度或尺寸,實際上從未實現這樣的條件。 我們可以簡單地采取措施來實現優化。

用於向計算結果移動到最小時間的許多(但不是全部)技術犧牲了存儲器需求,並且用於移向最小存儲器要求的許多(但不是全部)技術延長了結果的時間。

減少內存需求相當於一定數量的通用技術。 很難找到不完全適合這些中的一個或多個的特定技術。 如果您完成了所有這些操作,那么即使不是絕對最小值,您也可以獲得非常接近程序最小空間要求的東西。 對於一個真正的應用程序,可能需要一個有經驗的程序員團隊一千年才能完成它。

  1. 從存儲的數據中刪除所有冗余,包括中間體。
  2. 刪除所有需要存儲可以流式傳輸的數據。
  3. 僅分配所需的字節數,而不是一個字節。
  4. 刪除所有未使用的數據
  5. 刪除所有未使用的變量
  6. 一旦不再需要,就免費提供數據。
  7. 刪除算法中所有未使用的算法和分支。
  8. 找到在最小尺寸執行單元中表示的算法。
  9. 刪除項之間所有未使用的空間。

這是該主題的計算機科學視圖,而不是開發人員的視圖。

例如,打包數據結構是結合上述(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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM