1. 背景
メモリ コピー アクションは非常に一般的です。例えば、大学院入試408「オペレーティングシステム」では、システムバッファ内の周辺機器から受信したデータもユーザ領域にコピーする必要がある。ユーザー領域へのコピー処理はまさにメモリコピーです。
メモリ コピーは通常、memcpy 関数を直接呼び出すか、代入用のループを直接書き込みます。memcpy 関数は C ライブラリから直接呼び出すことができ、プログラミングにおいては確かに効率的です。しかし、実際に運用してみると、本当に速いのでしょうか?
2. 分析
memcpy の内部実装は実際には 2 つのポインターによって割り当てられ、オフセットはループによって制御されます。コードは次のとおりです。
void *memcpy(void *dest, const void *src, size_t count)
{undefined
if (NULL == dest || NULL == src || count <= 0)
return NULL;
while (count--)
*dest++ = *src++;
return dest;
}
memcpy がバイト コピー、つまり一度に 1 バイトずつコピーを実装していることを理解するのは難しくありません。このメソッドは、16 ビットおよび 32 ビット プロセッサのプロセッサのパフォーマンスを完全には返しません。バイトアライメントの問題を考慮しない前提で、16ビットと32ビットのプロセッサでは一度に最大2バイトと4バイトのデータをコピーできるため、ループ+ポインタ代入という方法でコピーすることもありますが、ポインタを16ビットまたは32ビットの配列ポインタにキャストすることで速度が向上するでしょうか?
3. 実験
ここでは、STM32 プレーヤーのコピーをデコード領域からバッファーにコピーする例を示します。
void Fill_SAI_Buff0(void)//缓冲区填充
{
if(buff0_attribute==NULL){
return;
}
for(uint16_t count=0; count<SAI_TX_BUFF_HALF/4; count++)
{
((uint32_t *)SAI_BUFF)[count] = ((uint32_t *)buff0_attribute)[count];
}
}
void Fill_SAI_Buff0(void)//缓冲区填充
{
if(buff0_attribute==NULL){
return;
}
memcpy(SAI_BUFF, buff0_attribute, SAI_TX_BUFF_HALF);
}
2 つのコードは、memcpy を使用してデコード領域からバッファーにコピーし、ポインターを 32 ビット配列にキャストし、循環的に値を割り当てます。このうち、buff0_attributeがデコード領域、SAI_BUFFがバッファです。コピーされた合計バイト数は SAI_TX_BUFF_HALF=4096 です。
計測後、memcpyの実装には40us程度、配列ポインタのコピーとして強制的に32に変換する方法には10us程度かかりました。後者は前者よりも大幅に高速です。測定方法は著者の以前の記事: KEIL5 デバッグ タイミング、プログラム実行時間の測定、STM32\MK60\IM6U および Cortex-M アーキテクチャ プロセッサに基づくその他のマイクロコントローラーに適しています_Fairchild_1947 のブログ - CSDN ブログ
4. 注意する
4.1 部数の変化に注意する
確かに32ビット配列ポインタに強制変換するコピー方式の方が高速ですが、1回のコピーの長さが変わると動作中のコピー数も変化するため、サイクル数の上限の設定には注意が必要です。たとえば、上記の例では、8 ビット配列ポインターが 32 ビット配列ポインターにキャストされると、コピー数は 4 分の 1 に削減されます。
4.2 コピーの全長に注意する
例えば上記の例では、8ビット配列ポインタを強制的に32ビット配列ポインタに変換していますが、この例が正常に使用できる前提として、8ビット配列でコピーされるバイト数がちょうど4の整数倍であることが前提となります。そうでない場合、境界外アクセスやデータ損失が発生する可能性があります。
4.3 強制変換の使用の詳細に注意する
これも作者の非常に愚かな間違いなので、この機会に皆さんにも共有したいと思います。
void Fill_SAI_Buff1(void)//缓冲区填充
{
if(buff1_attribute==NULL){
return;
}
for(uint16_t count=0; count<SAI_TX_BUFF_HALF/4; count++)
{
((uint32_t *)SAI_BUFF)[count+SAI_TX_BUFF_HALF/4] = ((uint32_t *)buff1_attribute)[count];
}
SCB_CleanDCache_by_Addr((uint32_t *)(SAI_BUFF+SAI_TX_BUFF_HALF), SAI_TX_BUFF_HALF);//Cortex-M7处理器CACHE回写模式时必须使用
}
2 番目のデコード領域をバッファにコピーする場合、バッファのアドレスは連続しているため、アドレスを対応する位置にオフセットして直接コピーを開始します。しかし、今ではキャストが参加しています。実際に書いているときに「(uint32_t *)(SAI_BUFF+SAI_TX_BUFF_HALF)」と「(uint32_t *)(SAI_BUFF)+SAI_TX_BUFF_HALF」を間違えてしまいました。前者は(SAI_BUFF+SAI_TX_BUFF_HALF)の強制変換であり、後者は(SAI_BUFF)とオフセットSAI_TX_BUFF_HALFの強制変換です。SAI_BUFF は配列としては 8 であり、オフセット SAI_TX_BUFF_HALF の単位もバイトなので、「(uint32_t *)(SAI_BUFF+SAI_TX_BUFF_HALF)」が正しいです。「(uint32_t *)(SAI_BUFF)+SAI_TX_BUFF_HALF」のオフセットは実際には32ビット(4バイト)単位なのでオフセットが間違っているため、2番目の書き方が間違っています。