web-dev-qa-db-ja.com

クリーンなCコードを記述しながら、ARMアラインされていないメモリアクセスを利用する

以前は、ARMプロセッサはアラインされていないメモリアクセス(ARMv5以下)を適切に処理できませんでした。u32 var32 = *(u32*)ptr;のようなものはptrが4バイトで正しく配置されていませんでした。

ただし、これらのCPUは常にこのような状況を非常に効率的に処理しているため、このようなステートメントを記述してもx86/x64では問題なく機能します。しかし、C標準によれば、これはそれを書くための「適切な」方法ではありません。 _u32_は、明らかに4バイトの構造と同等であり、は4バイトに整列する必要があります

正統性の正確さおよびを維持しながら同じ結果を達成する適切な方法は、CPUとの完全な互換性を保証することです。

_u32 read32(const void* ptr) 
{ 
    u32 result; 
    memcpy(&result, ptr, 4); 
    return result; 
}
_

これは正しいです。アライメントされていない位置で読み取ることができる、または読み取れないCPUに対して適切なコードを生成します。さらに良いことに、x86/x64では、単一の読み取り操作に適切に最適化されているため、最初のステートメントと同じパフォーマンスが得られます。ポータブルで安全、そして高速です。誰がもっと尋ねることができますか?

問題は、ARMではそれほど幸運ではないということです。

memcpyバージョンを作成することは確かに安全ですが、ARMv6およびARMv7(基本的にはすべてのスマートフォン)では非常に遅い、体系的な慎重な操作が行われるようです。

読み取り操作に大きく依存するパフォーマンス指向のアプリケーションでは、1番目と2番目のバージョンの違いを測定できます。_gcc -O2_設定で> 5xになります。これは無視できないほど多すぎます。

ARMv6/v7機能を使用する方法を見つけようとして、私はいくつかのサンプルコードに関するガイダンスを探しました。残念ながら、彼らは最初のステートメント(直接_u32_アクセス)を選択しているようですが、これは正しくないと思われます。

それだけではありません。新しいGCCバージョンは現在自動ベクトル化を実装しようとしています。 x64ではSSE/AVXを意味し、ARMv7ではNEONを意味します。 ARMv7は、いくつかの新しい「LoadMultiple」(LDM)および「StoreMultiple」(STM)オペコードもサポートしています。これらのオペコードは、ポインタを揃える必要があります

どういう意味ですか ?コンパイラは、Cコードから特に呼び出されていない場合でも(組み込み関数なし)、これらの高度な命令を自由に使用できます。このような決定を行うために、_u32* pointer_が4バイトに整列されることになっているという事実を使用します。そうでない場合は、すべての賭けがオフになります。未定義の動作、クラッシュ。

つまり、アラインされていないメモリアクセスをサポートするCPUでも、高い最適化設定(_u32_)でバグのあるコード生成が発生する可能性があるため、ダイレクト_-O3_アクセスを使用するのは危険です。

だから今、これはジレンマです:アラインされていないメモリアクセスでARMv6/v7のネイティブパフォーマンスにアクセスする方法間違ったバージョンを書かないで_u32_アクセス?

PS:__packed()命令も試しましたが、パフォーマンスの観点からは、memcpyメソッドとまったく同じように機能するようです。

[編集]:これまでに受け取った優れた要素に感謝します。

生成されたアセンブリを見ると、memcpyバージョンが実際に適切なldrオペコード(非整列ロード)を生成していることが@Notlikethatで確認できました。ただし、生成されたアセンブリがstr(コマンド)を無用に呼び出すこともわかりました。したがって、完全な操作は、整列されていないロード、整列されたストア、そして最後の整列されたロードになります。それは必要以上に多くの作業です。

@haneefmubarakに答えると、はい、コードは適切にインライン化されています。いいえ、memcpyは、コードに直接_u32_アクセスを受け入れるように強制すると、パフォーマンスが大幅に向上するため、可能な限り最高の速度を提供するにはほど遠いです。したがって、より良い可能性が存在する必要があります。

@artless_noiseに感謝します。 godboltサービスへのリンクは貴重です。 Cソースコードとそのアセンブリ表現の同等性をこれほど明確に確認することはできませんでした。これは非常に刺激的です。

@artlessの例の1つを完了しましたが、次のようになります。

_#include <stdlib.h>
#include <memory.h>
typedef unsigned int u32;

u32 reada32(const void* ptr) { return *(const u32*) ptr; }

u32 readu32(const void* ptr) 
{ 
    u32 result; 
    memcpy(&result, ptr, 4); 
    return result; 
}
_

ARM GCC 4.8.2 at -O3または-O2を使用してコンパイルされた場合:

_reada32(void const*):
    ldr r0, [r0]
    bx  lr
readu32(void const*):
    ldr r0, [r0]    @ unaligned
    sub sp, sp, #8
    str r0, [sp, #4]    @ unaligned
    ldr r0, [sp, #4]
    add sp, sp, #8
    bx  lr
_

かなりわかります..。

21
Cyan

OK、状況は思ったよりも混乱しています。したがって、明確にするために、この旅の結果を以下に示します。

アラインされていないメモリへのアクセス

  1. アラインされていないメモリにアクセスするための唯一のポータブルC標準ソリューションは、memcpyソリューションです。私はこの質問を通して別のものを手に入れたいと思っていましたが、明らかにこれまでに見つかったのはそれだけです。

サンプルコード:

u32 read32(const void* ptr)  { 
    u32 value; 
    memcpy(&value, ptr, sizeof(value)); 
    return value;  }

このソリューションは、あらゆる状況で安全です。また、GCCを使用してx86ターゲットで簡単なload register操作にコンパイルします。

ただし、GCCを使用するARMターゲットでは、アセンブリシーケンスが大きすぎて役に立たないため、パフォーマンスが低下します。

ARM target、memcpyでClangを使用すると、正常に機能します(以下の@notlikethatコメントを参照)。GCC全体のせいにするのは簡単ですが、それほど単純ではありません:memcpyソリューションは、x86/x64、PPC、およびARM64ターゲットを使用するGCCで正常に機能します。最後に、別のコンパイラicc13を試してみると、memcpyバージョンはx86/x64で驚くほど重くなります(4命令、 1つで十分なはずです)そして、それは私がこれまでにテストできた組み合わせにすぎません。

私はそのような声明を出すためにgodboltのプロジェクトに感謝​​しなければなりません 観察しやすい

  1. 2番目の解決策は、__packed構造体を使用することです。このソリューションはC標準ではなく、コンパイラの拡張機能に完全に依存しています。結果として、それを書く方法はコンパイラーに依存し、時にはそのバージョンに依存します。これは、ポータブルコードのメンテナンスの混乱です。

そうは言っても、ほとんどの場合、memcpyよりも優れたコード生成につながります。ほとんどの状況でのみ...

たとえば、memcpyソリューションが機能しない上記のケースに関して、以下の調査結果があります。

  • iCCを使用するx86の場合:__packedソリューションは機能します
  • gCCを使用したARMv7の場合:__packedソリューションは機能します
  • gCCを使用するARMv6の場合:機能しません。アセンブリはmemcpyよりもさらに醜いように見えます。

    1. 最後の解決策は、整列されていないメモリ位置への直接のu32アクセスを使用することです。このソリューションは、x86 cpusで数十年にわたって機能していましたが、一部のC標準の原則に違反しているため、お勧めしません。コンパイラは、このステートメントをデータが適切に整列され、バグのあるコード生成につながることを保証するものと見なす権限があります。

残念ながら、少なくとも1つのケースでは、ターゲットからパフォーマンスを抽出できる唯一のソリューションです。つまり、ARMv6上のGCCの場合です。

ただし、ARMv7にはこのソリューションを使用しないでください。GCCは、整列されたメモリアクセス用に予約された命令、つまりLDM(Load Multiple)を生成し、クラッシュを引き起こす可能性があります。

X86/x64でも、新世代のコンパイラがいくつかの互換性のあるループを自動ベクトル化して、仮定に基づいてSSE/AVXコードを生成する可能性があるため、この方法でコードを記述することは危険になります。これらのメモリ位置が適切に整列され、プログラムがクラッシュすること。

要約すると、次の規則を使用して、結果を表として要約します:memcpy> packed> direct。

| compiler  | x86/x64 | ARMv7  | ARMv6  | ARM64  |  PPC   |
|-----------|---------|--------|--------|--------|--------|
| GCC 4.8   | memcpy  | packed | direct | memcpy | memcpy |
| clang 3.6 | memcpy  | memcpy | memcpy | memcpy |   ?    |
| icc 13    | packed  | N/A    | N/A    | N/A    | N/A    |
15
Cyan

問題の一部は、簡単なインライン化とさらなる最適化を許可していない可能性があります。負荷に特化した関数があるということは、呼び出しごとに関数呼び出しが発行される可能性があることを意味し、パフォーマンスが低下する可能性があります。

_static inline_を使用すると、コンパイラーが関数load32()をインライン化できるため、パフォーマンスが向上します。ただし、より高いレベルの最適化では、コンパイラーはすでにこれをインライン化する必要があります。

コンパイラーが4バイトのmemcpyをインライン化する場合、それは、整列されていない境界でも機能する最も効率的な一連のロードまたはストアに変換される可能性があります。したがって、コンパイラの最適化を有効にしてもパフォーマンスが低下する場合は、が、使用しているプロセッサでのアラインされていない読み取りと書き込みの最大パフォーマンスである可能性があります。 「___packed_命令」はmemcpy()と同じパフォーマンスをもたらすとおっしゃっていたので、これが当てはまるようです。


この時点で、データを調整する以外にできることはほとんどありません。ただし、整列されていない_u32_の連続した配列を処理している場合は、次の1つの方法があります。

_#include <stdint.h>
#include <stdlib.h>

// get array of aligned u32
uint32_t *align32 (const void *p, size_t n) {
    uint32_t *r = malloc (n * sizeof (uint32_t));

    if (r)
        memcpy (r, p, n);

    return r;
}
_

これは、malloc()を使用して新しい配列をallocatesするだけです。これは、malloc()とその友人が、すべてに対して正しい配置でメモリを割り当てるためです。

Malloc()関数とcalloc()関数は、あらゆる種類の変数に適切に配置された、割り当てられたメモリへのポインタを返します。

- malloc(3)、Linuxプログラマーズマニュアル

これは、データセットごとに1回だけ実行する必要があるため、比較的高速である必要があります。また、それをコピーしている間、memcpy()は最初の位置合わせの欠如を調整し、次に利用可能な最速の位置合わせされたロードおよびストア命令を使用できます。その後、を使用してデータを処理できるようになります。通常の整列された読み取りと書き込みは、フルパフォーマンスで行われます。

2
haneefmubarak