« C#で高速にビットマップ画像を描く方法 | トップページ | Visual C#に対応したUSBテストプログラムを公開しました »

2020.05.11

C#で配列のキャストを行うには

大きな配列に何かのデータを詰めたり、比較したりするときに、より広いデータ幅で比較したり代入したほうが高速に実行できると考えられます。

CやC++ならば、

void hoge(unsigned char *received_data) {
unsigned long *buffer = (unsigned long *)received_data;
・・・・
}

というふうにキャストを使うことで、配列を1バイト単位でアクセスしたり4バイト単位でアクセスしたり、自由自在なサイズでアクセスできました。(ここではアラインメントが合わない場合とかは考えない)

ところが、これをC#でやろうとすると難しくなります。

やりたいことはByte[]型の配列に乱数を詰めたいだけなのですが、乱数は32bit単位で生成されてくるので、4バイト単位で詰められれば4倍速くなるだろうということです。

試行錯誤の末、以下のようになりました。

public void FillArray (Byte[] buffer, int bufSize)
{
    unsafe // この中ではポインタが使えるよ
    {
        fixed (byte* pb = buffer) // bufferがGCされないようにする
        {
        UInt32* p = (UInt32*)pb; // byte型ポインタをUInt32型ポインタに変換
        for (int i = 0; i < bufSize / 4; i++)
        {
        UInt32 t = x ^ (x << 11); // XorShiftという乱数生成アルゴリズム
            x = y; y = z; z = w;
            w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
            p[i] = w;
        }
}
}
}

このコードはbufferというByte[]型の配列に乱数を詰めるというものですがUInt32型のポインタに変換して4バイト単位で詰めていきます。

unsafeやfixedを使うのはいいのですが、fixedではポインタの型までは変えられないようなので、fixedではbyte型のポインタにして次の分でキャストしています。

実際に本当に速くなるのか試してみたら、256MByteの配列を埋めるのに、

  • 1バイト単位の場合
    • Debugビルド・・・1270ms
    • Releaseビルド・・・599ms
  • 4バイト単位の場合
    • Debugビルド・・・320ms
    • Releaseビルド・・・143ms

と、ちょうど4倍速くなっていました。

 

次に、USBでFPGAに送って戻ってきたデータをコンペアする関数を作りたいと思います。

C#で配列の中身を比較するのは、Enumerable.SequenceEqualというメソッドを使うようなのですが、これが遅い。256MByteのデータを比較するのに3200msほどかかっています。ReleaseビルドでもDebugビルドでもあまり差はありません。

unsafeとfixedの中でfor文を回して、8バイト単位で比較するプログラムを作ってみました。

// 8バイト単位で調べて、一致していればtrue、一致していなければfalseを返す
unsafe private bool UnsafeMemEqual8(byte[] s1, byte[] s2)
{
fixed (byte* pb1 = s1)
{
fixed (byte* pb2 = s2)
{
UInt64* p1 = (UInt64*)pb1;
UInt64* p2 = (UInt64*)pb2;
int len = Math.Min(s1.Length, s2.Length) / 8;
for(int i=0;i<len;i++)
{
if (*p1++ != *p2++) return false;
}
}
return true;
}
}

この関数の結果は、

  • Debugビルド・・・108ms
  • Releaseビルド・・・48ms

と、Enumerable.SequenceEqualの3200msに比べて圧倒的に高速でした。

もしかすると、Enumerable.SequenceEqualは高度な関数なので、配列の比較のような単純なことに使うべきではないのかもしれません。

 

最後にC#で作ったUSB3.0 DDR3テストプログラムと、Borland C++ Builder 6版での実行時間をまとめます。

コンパイラ Visual C# 2019 (x64) Borland C++ Builder 6
ビルド Debug Release Debug Release
乱数データ生成 320 143 224 147
USB3.0 送信 803
806 787 788
USB3.0 受信 745 749 728 728
データ比較 108 48 112 114
合計 1976 1746 1851 1777

※数字は[ms]
※データサイズは256MByte

いろいろ試してきましたが、unsafeとfixedを使うと、配列に(ガベージコレクトされないという意味で)安全に高速アクセスできることがわかりました。

実行時間もほとんど差がありません。

ハードウェアにアクセスするDLLを使ったり画像を生成したり、C言語と同じようにアクセスできることがわかったので、これからは組み込みのユーザインタフェースプログラムはC#で作ろうかなと思います。

Cs_usb3

Cs_usb3_2

 

|

« C#で高速にビットマップ画像を描く方法 | トップページ | Visual C#に対応したUSBテストプログラムを公開しました »

コメント

コメントを書く



(ウェブ上には掲載しません)




« C#で高速にビットマップ画像を描く方法 | トップページ | Visual C#に対応したUSBテストプログラムを公開しました »