フラッシュとRAMを増やすためにコードを圧縮する方法は?[閉まっている]


14

私たちの特定の製品の機能の開発に取り組んできました。同じ機能を別の製品に移植する要求がありました。この製品は、伝統的に64Kのフラッシュと2KのRAMを搭載したM16Cマイクロコントローラーに基づいています。

これは成熟した製品であるため、132バイトのフラッシュと2バイトのRAMしか残っていません。

要求された機能を移植するには(機能自体が最適化されています)、1400バイトのフラッシュと〜200バイトのRAMが必要です。

コード圧縮によってこれらのバイトを取得する方法について提案はありますか?既存の作業コードを圧縮しようとするとき、どのような特定の事柄を探しますか?

どんなアイデアでも大歓迎です。

ありがとう。


1
提案をありがとう。私はあなたに私の進捗状況を更新し続け、うまくいったステップとそうでないステップをリストします。
IntelliChick

わかりましたので、ここで私が試したものが機能しました:コンパイラのバージョンを上げました。最適化が大幅に改善され、約2KのFlashが得られました。リストファイルを調べて、特定の製品の冗長で未使用の機能(共通のコードベースのために継承)を確認し、さらにFlashを取得しました。
IntelliChick

RAMについては、次のことを行いました。ほとんどのRAMを使用していた機能/モジュールを確認するために、マップファイルを調べました。レガシーコードの非常に重い機能(12チャンネル、それぞれ一定量のメモリが割り当てられている)を見つけ、共通のチャンネル間で情報を共有することで、達成しようとしていることを理解し、RAM使用量を最適化しました。これにより、必要な〜200バイトが得られました。
インテリチック

ASCIIファイルがある場合は、8〜7ビットの圧縮を使用できます。12.5%節約できます。zipファイルを使用すると、そのままにするよりも、zipファイルを解凍したり解凍したりするのに多くのコードが必要になります。
Sparky256

回答:


18

いくつかのオプションがあります。最初は、冗長なコードを探し、それを単一の呼び出しに移動して、重複を取り除きます。2つ目は、機能を削除することです。

.mapファイルをよく見て、削除または書き換えできる関数があるかどうかを確認してください。また、使用されているライブラリ呼び出しが本当に必要であることを確認してください。

除算や乗算などの特定のものは多くのコードをもたらす可能性がありますが、シフトを使用し、定数をより適切に使用するとコードを小さくできます。文字列定数やprintfsのようなものも見てください。たとえば、それぞれprintfがROMを使い果たしますが、その文字列定数を何度も繰り返すのではなく、いくつかの共有フォーマット文字列を使用できる場合があります。

メモリについては、グローバルを削除して、代わりに関数でautoを使用できるかどうかを確認してください。また、メイン関数の変数をできるだけ多く使用しないでください。これらの変数は、グローバルと同様にメモリを消費します。


1
提案のおかげで、文字列定数に関するものを除き、それらのほとんどを間違いなく試すことができます。UIを持たない純粋な組み込みデバイスであるため、コード内でprintf()の呼び出しはありません。これらの提案が、私が必要とする1400バイトのフラッシュ/ 200バイトのRAMを手に入れるために私を奮い立たせることを願っています。
IntelliChick

1
@IntelliChickでは、組み込みデバイスの内部でprintf()を使用して、デバッグまたは周辺機器への送信のために印刷する人の数に驚くでしょう。あなたはこれよりもよく知っているようですが、誰かがあなたの前にプロジェクトにコードを書いていれば、それをチェックするのに害はないでしょう。
ケレンブ

5
また、前回のコメントを拡張するために、デバッグステートメントを追加する人がどれだけいるかに驚かされますが、それらは削除しないでください。#ifdefsを実行する人でさえ、時々怠け者になります。
ケレンブ

1
素晴らしいです、ありがとう!私はこのコードベースを継承しているので、間違いなくそれらを探しています。進捗状況を投稿し続け、将来的にこれを行う必要があるかもしれない他の人のための参考として、私が何をしたかによって得たメモリまたはフラッシュのバイト数を追跡​​しようとします。
IntelliChick

これに関する質問-ネストされた関数呼び出しはレイヤーからレイヤーへホップしますか?どのくらいのオーバーヘッドが追加されますか?複数の関数呼び出しを行うことでモジュール性を維持するか、関数呼び出しを減らして、いくつかのバイトを節約する方が良いでしょうか。それは重要ですか?
IntelliChick

8

リストファイル(アセンブラー)の出力を見て、特定のコンパイラーが特に苦手なものを探す価値は常にあります。

たとえば、ローカル変数は非常に高価であり、アプリケーションがリスクに見合うほど単純な場合、いくつかのループカウンターを静的変数に移動すると、多くのコードを節約できます。

または、配列のインデックス作成は非常に高価ですが、ポインタ操作ははるかに安価です。またはその逆。

しかし、アセンブリ言語を見ることは最初のステップです。


3
コンパイラーが何をするのかを知ることは非常に重要です。私のコンパイラーで何が除算されているかがわかるはずです。それは赤ちゃんを泣かせます(自分自身を含む)。
Kortuk

8

たとえば、-OsGCCでのコンパイラの最適化は、速度とコードサイズの最適なバランスを提供します。-O3コードサイズが大きくなる可能性があるため、を避けてください。


3
これを行う場合、すべてを再テストする必要があります!最適化により、コンパイラーが新しい仮定を行うため、作業コードが機能しなくなる可能性があります。
ロバート

@Robert、これは未定義のステートメントを使用する場合にのみ当てはまります。たとえば、a = a ++は-O0と-O3でコンパイルが異なります。
トーマスO

5
@Thomasは正しくありません。クロックサイクルを遅らせるためのforループがある場合、多くのオプティマイザーは何もしていないことに気付き、削除します。これはほんの一例です。
ケレンブ

1
@thomas O、また、揮発性関数の定義に注意する必要があります。オプティマイザーは、Cをよく知っているが、アトミック操作の複雑さを理解していないと思うものを爆破します。
コルトゥク

1
すべての良い点。揮発性の関数/変数は、定義により、最適化してはいけません。そのような最適化(呼び出し時間とインライン化を含む)を実行するオプティマイザーは壊れています。
トーマスO

8

RAMについては、すべての変数の範囲を確認してください。charを使用できるintを使用していますか?バッファーは必要以上に大きいですか?

コードの圧縮は、アプリケーションとコーディングスタイルに大きく依存します。残された金額は、コードが既に少し圧縮されているかもしれないことを示唆しています。

また、全体的な機能をよく見てください。実際に使用されておらず、放棄できるものはありますか?


8

古いプロジェクトであるが、コンパイラがその後開発された場合、より新しいコンパイラがより小さなコードを生成する可能性があります


マイクありがとう!過去にこれを試しましたが、獲得したスペースはすでに使用されています!:) IAR Cコンパイラ3.21dから3.40に移動しました。
IntelliChick

1
私はもう1つバージョンを上げて、この機能に合わせてFlashを追加することに成功しました。しかし、RAMには本当に苦労しています。:(
IntelliChick

7

スペースを最適化するオプションについては、コンパイラのマニュアルを常に確認する価値があります。

GCCの場合-ffunction-sections-fdata-sections--gc-sections、リンカフラグは、デッドコードを除去するのに適しています。

他の優れたヒントをいくつか紹介します(AVR向け)


これは実際に機能しますか? ドキュメントには、「これらのオプションを指定すると、アセンブラとリンカがより大きなオブジェクトと実行可能ファイルを作成し、速度も低下します」と書かれています。フラッシュセクションとRAMセクションを備えたマイクロの場合、別々のセクションを使用するのが理にかなっていることを理解しています。
ケビンフェルメール

私の経験では、AVRでうまく機能するということです
トビージャフィー

1
これは、私が使用したほとんどのコンパイラではうまく機能しません。registerキーワードを使用するようなものです。変数がレジスターに入ることをコンパイラーに伝えることができますが、優れたオプティマイザーはこれを人間よりもはるかにうまく行います(実際にはこれを行うことは受け入れられないと考えられる場合もあります)。
Kortuk

1
場所の割り当てを開始すると、コンパイラーに特定の場所に物事を配置するように強制します。これは高度なブートローダーコードにとって非常に重要ですが、オプティマイザーで対処するのは非常に困難です。出来ました。一部のコンパイラでは、使用するコードのセクションを含むように設計します。これは、使用方法を理解するためにコンパイラに詳細情報を伝える場合です。これは役立ちます。コンパイラが提案しない場合は、しないでください。
Kortuk

6

割り当てられているスタックスペースとヒープスペースの量を調べることができます。これらのいずれかまたは両方が過剰に割り当てられている場合、かなりの量のRAMを取り戻すことができる場合があります。

私の推測では、動的なメモリの割り当てがないとRAMの2Kへのフィットを開始することをプロジェクト(の使用のためであるmalloccallocなど)。この場合、元の作者がヒープ用に割り当てられたRAMを残したと仮定して、ヒープを完全に取り除くことができます。

スタックサイズを減らすことは非常に注意する必要があります。これにより、見つけるのが非常に難しいバグが発生する可能性があります。スタックスペース全体を既知の値(0x00または0xff以外の値が既に一般に発生するため)で初期化することから始めて、しばらくシステムを実行し、未使用のスタックスペースを確認すると役立つ場合があります。


これらは非常に良い選択です。組み込みシステムでmallocを使用しないでください。
-Kortuk

1
@Kortuk埋め込みの定義と実行されるタスクに依存します
トビージャフィー

1
@joby、ええ、私はそれを理解しています。再起動が0で、LinuxのようなOSが存在しないシステムでは、Mallocは非常に悪い場合があります。
Kortuk

動的なメモリ割り当て、malloc、callocが使用されている場所はありません。ヒープの割り当ても確認しましたが、既に0に設定されているため、ヒープの割り当てはありません。現在割り当てられているスタックサイズは254バイトで、割り込みスタックサイズは128バイトです。
IntelliChick

5

コードで浮動小数点演算を使用していますか?整数演算のみを使用してアルゴリズムを再実装し、C浮動小数点ライブラリを使用するオーバーヘッドを排除できる場合があります。たとえば、サイン、ログ、expなどの関数は、整数多項式近似に置き換えることができます。

コードは、CRC計算などのアルゴリズムに大きなルックアップテーブルを使用していますか?ルックアップテーブルを使用する代わりに、オンザフライで値を計算する別のバージョンのアルゴリズムに置き換えることもできます。注意点は、アルゴリズムが小さいほど遅くなる可能性が高いため、十分なCPUサイクルを確保することです。

コードには、文字列テーブル、HTMLページ、ピクセルグラフィックス(アイコン)などの大量の定数データが​​ありますか?十分に大きい場合(10 kBなど)、非常に単純な圧縮スキームを実装して、必要に応じてデータを圧縮し、オンザフライで圧縮解除する価値があります。


2つの小さなルックアップテーブルがありますが、どちらも残念ながら10Kにはなりません。また、浮動小数点演算も使用されていません。:(ただし、提案をありがとう。彼らは良いです。
IntelliChick10年

2

より多くのコードを、よりコンパクトなスタイルに再配置することができます。コードが何をしているかに大きく依存します。重要なのは、類似したものを見つけて、互いの観点から再実装することです。極端な方法は、Forthのような高レベル言語を使用することです。Forthを使用すると、Cやアセンブラーよりも高いコード密度を達成しやすくなります。

ここでM16C用フォース


2

Set the optimization level of the compiler. Many IDE's have settings that allow for code-size optimizations at the expense of compile-time (or maybe even processing time in some cases). They can accomplish code compacting by rerunning their optimizer a couple of times, searching for less-common optimize-able patterns, and a whole other host of tricks that may not be necessary for the casual/debug compilation. Usually, by default, compilers are set to a medium level of optimization. Dig around in the settings an you should be able to find some integer-based optimization scale.


1
現在、サイズの最大値に最適化されています。:)しかし提案をありがとう。:)
IntelliChick

2

If you're already using a professional-level compiler like IAR, I think you're going to struggle to get any serious savings by minor low-level code-tweaking - you'll need to be looking more towards removing functionality or doing major rewrites of parts in a more efficient way. You'll need to be a smarter coder than whoever wrote the original version... As for RAM you need to take a very hard look at how it is currently used, and see if there is scope for overlaying usage of the same RAM for different things at different times (unions are handy for this). IAR's default heap and stack sizes in the ARM/AVR ones I've have tended to be over-generous, so these would be the first thing to look at.


Thanks Mike. The code is already using unions in most places, but I will have a look at some other places, where this might still help. I will also have a look at the stack size chosen and see if that can be optimised at all.
IntelliChick

How do I know what Stack size size is appropriate?
IntelliChick

2

Something else to check - some compilers on some architectures copy constants to RAM - typically used when access to flash constants is slow/difficult (e.g. AVR) e.g. IAR's AVR compiler requires a _ _flash qualifer to not copy a constant to RAM)


Thanks Mike. Yea I had already checked that - its called the 'Writeable constants' option for the M16C IAR C compiler. It copies the constants from ROM to RAM. This option is unchecked for my project. But a really valid check! Thanks.
IntelliChick

1

If your processor doesn't have hardware support for a parameter/local stack but the compiler tries to implement a run-time parameter stack anyway, and if your code doesn't need to be re-entrant, you may be able to save code space by statically allocating auto variables. In some cases, this must be done manually; in other cases, compiler directives can do it. Efficient manual allocation will require sharing of variables between routines. Such sharing must be done carefully, to ensure that no routine uses a variable which another routine considers to be "in scope", but in some cases the code-size benefits may be significant.

Some processors have calling conventions that may make some parameter-passing styles more efficient than others. For example, on the PIC18 controllers, if a routine takes a single one-byte parameter, it may be passed in a register; if it takes more than that, all parameters must be passed in RAM. If a routine would take two one-byte parameters, it may be most efficient to "pass" one in a global variable, and then pass the other as a parameter. With widely-used routines, the savings can add up. They can be especially significant if the parameter passed via global is a single-bit flag, or if it will usually have a value of 0 or 255 (since special instructions exist to store a 0 or 255 into RAM).

On the ARM, putting global variables which are frequently used together into a structure may significantly reduce code size and improve performance. If A, B, C, D, and E are separate global variables, then code which uses all of them must load the address of each into a register; if there aren't enough registers, it may be necessary to reload those addresses multiple times. By contrast, if they are part of the same global structure MyStuff, then code which uses MyStuff.A, MyStuff.B, etc. can simply load the address of MyStuff once. Big win.


1

1.If your code relies on a lot of structures, make sure the structure members are ordered from the ones that occupy most memory to the least.

Ex: "uint32_t uint16_t uint8_t" instead of "uint16_t uint8_t uint32_t"

This will ensure minimum structure padding.

2.Use const for variables where applicable. This will ensure that those variables will be in ROM and not eat up RAM


1

A few (perhaps obvious) tricks I've used successfully in compressing some customer's code:

  1. Compact flags into bit fields or bit masks. This may be beneficial as usually booleans are stored as integers thus wasting memory. This will save both RAM and ROM and isn't usually done by the compiler.

  2. Look for redundancy in the code, and use loops or functions to execute repeated statements.

  3. I've also saved some ROM by replacing many if(x==enum_entry) <assignment> statements from constants with an indexed array, by taking care that the enum entries could be used as the array index


0

If you can, use inline functions or compiler macros instead of small functions. There's size and speed overhead with passing arguments and such that can be remedied by making the function inline.


1
Any decent compiler should do this automatically for funcitons that are called only once.
mikeselectricstuff

5
I've found inlining is usually more useful for speed optimisations, and usually at the cost of increased size.
Craig McQueen

inlining will typically increase code size, except with trivial functions like int get_a(struct x) {return x.a;}
Dmitry Grigoryev

0

Change the local variables to be the same size of your CPU registers.

If the CPU is 32-bit, use 32-bit variables even if the max value will never get above 255. I you used an 8-bit variable, the compiler will add code to mask off the upper 24 bits.

The first place I would look is at the for-loop variables.

for( i = 0; i < 100; i++ )

This might seem like a good place for an 8-bit variable, but a 32-bit variable might produce less code.


That may save code but it will eat RAM.
mikeselectricstuff

It will only eat RAM if that function call is in the longest branch of the call trace. Otherwise, it is reusing stack space that some other function already needs.
Robert

2
Usually true providing it's a local variable. If short on RAM, the size of global vars, especially arrays, is a good place to start looking for savings.
mikeselectricstuff

1
Another possibility, interestingly, is to replace unsigned variables with signed. If a compiler optimizes an unsigned short to a 32-bit register, it must add code to ensure that its value wraps from 65535 to zero. If, however, the compiler optimizes a signed short to a register, no such code is required. Because there is no guarantee what will happen if a short is incremented beyond 32767, compilers are not required to emit code to deal with that. On at least two ARM compilers I've looked at, signed-short code can be smaller than unsigned-short code for that reason.
supercat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.