午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

C#でSIMDを使うSystem.Runtime.Intrinsicsを試してみた、byte型配列の合計

SIMDを使っての計算はSystem.Numerics.Vectorクラスと、これとは別にSystem.Runtime.Intrinsicsってのもあるみたいで試してみた

 

環境

  • CPU AMD Ryzen 5 2400G(4コア8スレッド)
  • MEM DDR4-2666
  • Window 10 Home 64bit
  • Visual Studio 2019 Community .NET Core 3.1 WPF C#

.NET FrameworkだとSystem.Numericsの参照の追加とかあってめんどくさいけど、.NET Coreならラク

 

参考にしたところ

habr.com

今回はここからコピペしたりして試した

 

IntrinsicsはNumerics.Vectorと同じくらい速い

Numerics.Vectorに似ている

ポインタを使う

Vector256を使う

AVXをサポートしていない古い(2010年くらいの)CPUでは動かないっぽい

C#で説明しているところがほとんどないC++ばかり

 

ポインタは久しぶりに使うことになった、設定は以前の記事、このとき以来かな

 

追加するusing

f:id:gogowaten:20200223172407p:plain

System.Runtime.IntrinsicsとSystem.Runtime.Intrinsics.X86を追加

 

今回の目的はbyte配列の合計値を求める、なので起動時にbyte配列作成

f:id:gogowaten:20200223171713p:plain

34~38行目でbyte配列作成、値は0~255までの連番、要素数は256個、これをMyArrayって名前をつけたところ(27行目)に入れておく

 

値からVector256作成

f:id:gogowaten:20200223172244p:plain

横に長過ぎるので切って表示すると

 

f:id:gogowaten:20200223172601p:plain

Vector256のCreateに値を渡すと作成できる、実行して一時停止で中を見ると

f:id:gogowaten:20200223172842p:plain

値を1つ指定した場合は、その値で埋まって、型は適当なものになった

型を指定してZeroをつけると、その型の0で埋まった、180行目

それ以外の173~178行目は指定した型によって値の個数が決まっているみたいで

f:id:gogowaten:20200223173417p:plain

値の個数
32 Vector256<byte>
16 Vector256<short>
8 Vector256<int>
4 Vector256<long>

これは固定なのかなあ、Numerics.Vectorと同じ感じになっている

これ以外の個数で作ろうとするとエラーになる

var x = Vector256.Create(1, 1);//これはエラー

 

配列からVector作成は

System.Runtime.Intrinsics.AvxクラスのLoadVector256を使う

f:id:gogowaten:20200223174432p:plain

これの引数がポインタ、byte*ってアスタリスクがついているからbyte型のポインタ

f:id:gogowaten:20200223174105p:plain

ポインタを使うにはunsafeもつけて(159行目)、161行目のfixedはアドレスの固定?これでaa(配列)のメモリアドレスが固定されて安心してポインタを使えるとかそういうの、で、byte型ポインタの変数pに配列aaを指定、これをLoadVector256に渡して見ると

f:id:gogowaten:20200223180057p:plain

Vector256<byte>型ができた、値は配列の先頭から32個分の値

f:id:gogowaten:20200223180234p:plain

0~31が入っている

ポインタの値を+1したのを渡すと

f:id:gogowaten:20200223180425p:plain

配列の2番めから32個分の値、1~32までのVector256<byte>ができた

ポインタの値を+32だと

f:id:gogowaten:20200223180616p:plain

32番めからの32個分

これで配列からVectorは作成できたのでVector同士の足し算は

System.Runtime.Intrinsics.X86.Avx2のAdd

f:id:gogowaten:20200223181041p:plain

a,b2つのVectorをAddで足し算はAvx2.Add(a, b)、これで

f:id:gogowaten:20200223181311p:plain

ペア同士が足し算された値のVectorが返ってくる、返ってくるVectorの型も同じなので、足し算の結果が型の最大値を超えるとオーバーフローでおかしな値になる。このへんはNumerics.Vectorと同じだねえ

 

f:id:gogowaten:20200223181654p:plain

148行目で250で埋めたVectorを作って、これと連番の配列から作ったVectorを足し算すると

f:id:gogowaten:20200223182031p:plain

v 250 250 250 250 250 250 250 250 250 250
tempV 0 1 2 3 4 5 6 7 8 9
total 250 251 252 253 254 255 0 1 2 3
              256 257 258 259

オーバーフロー
なのでbyte型より大きな値が使えるintとかlong型のVectorで計算する必要がある

 

byte型配列から他の整数型Vectorを作成

f:id:gogowaten:20200223182800p:plain

これにはAvx2にConvertToVector256Intっていう丁度いいのがあった、これで

f:id:gogowaten:20200223182949p:plain

byte型配列からshort、Int、long型のVectorが作成できる、すごい!Numerics.Vectorにはこれがなかったんだよねえ
これを使ってさっきオーバーフローしてたのを少し変えたのが

f:id:gogowaten:20200223183519p:plain

129行目でint型に変換してVectorを作成

f:id:gogowaten:20200223183721p:plain

v 250 250 250 250 250 250 250 250 250
tempV 0 1 2 3 4 5 6 7 8
total 250 251 252 253 254 255 256 257 258

Int型Vectorなので要素数は8に減っているけど、256以上になっても正しい値になっている。

これでVectorの足し算はできたので、次はVectorの中の値を合計したいけど、そういうメソッドはないみたいで、ググってみたところではAvxクラスのStoreっていうメソッドで、普通の配列に値をコピーしてから合計していた。

Avx.Store

f:id:gogowaten:20200223185114p:plain

用意した配列のポインタとVectorを渡すと配列に値が入る

 

//Vectorから配列に変換というか値をコピーその1

f:id:gogowaten:20200223184204p:plain

100で埋めたInt型Vectorの値の合計

あらかじめVectorの要素数と同じサイズ同じ型の配列を用意して(100行目)、これのポインタとVectorを渡す(103行目)、あとは普通にForやLINQのSumとかで足し算すればいいってことになる

f:id:gogowaten:20200223185557p:plain

fixedの意味がわかっていないので念のために使ってみたけど必要ないかも?

もう一つの方法はstackallocってのを使って配列のポインタにしていた

f:id:gogowaten:20200223185915p:plain

これだとLINQのSumで合計できないからForで回す、多分こっちのほうが速い

f:id:gogowaten:20200223190339p:plain

計算結果は同じ、こっちのほうがいいのかなあ

 

ここまでのを全部合わせてできたのが

渡したbyte型配列の合計を返す

f:id:gogowaten:20200223190609p:plain

大まかな方法はNumerics.Vectorとほとんど同じだねえ

0~255の連番の配列を渡すので結果は(255+0)*(255-0+1)/2=32640になるはず

 

f:id:gogowaten:20200223191401p:plain

Vector同士のたし算が終わったとろこ

 

f:id:gogowaten:20200223191557p:plain

Vectorの中の値を合計し終わったところ、もとの配列の要素数256はInt型Vectorの要素数8で割り切れて、あまりの要素はないので合計値は32640で出ている

 

f:id:gogowaten:20200223191925p:plain

出た

あとはNumerics.Vectorと実際に比較してみてどっちが速いかだねえ、Intrinsicsのほうが少し速いみたいなんだけど、ポインタを使うのがめんどくさいので、少し速いくらいならNumerics.Vectorを使うかなあ、でもメソッドの種類はIntrinsicsのが多い、すごい多い、この辺を使えたら便利そうなんだけど、日本語で説明しているところがないので使い方がほとんどわからない

 画像処理を早くしたくて色々ググってるとC++OpenCVGPUとかの単語が出てくるけど難しそう、それでもいろいろな方法があるのはいいねえ

 

 今回のアプリは合計値を表示するだけだからあんまり意味ないけど

ファイル名は、20200223_Intrinsics.zip

github.com

 

<Window x:Class="_20200223_Intrinsics.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20200223_Intrinsics"
        mc:Ignorable="d"
        Title="MainWindow" Height="100" Width="614">
  <Grid>
    <DockPanel>
      <TextBlock x:Name="tb1" FontSize="40" Text="text" VerticalAlignment="Center" HorizontalAlignment="Center"/>
    </DockPanel>
  </Grid>
</Window>

 

 

using System.Linq;
using System.Windows;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;


namespace _20200223_Intrinsics
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private byte[] MyArray;

        public MainWindow()
        {
            InitializeComponent();
            this.Title = this.ToString();

            MyArray = new byte[256];
            for (int i = 0; i < 256; i++)
            {
                MyArray[i] = (byte)i;
            }

            Test1VectorCreate();
            Test2Load(MyArray);
            Test3AddOverflow(MyArray);
            Test4Convert(MyArray);
            Test5Add(MyArray);
            Test6Add(MyArray);
            Test7AvxStore();
            Test8AvxStore();
            tb1.Text = $"0から255まで連番の合計 = {Test9Add(MyArray).ToString("N0")}";
        }

        //byte型配列の合計値
        private unsafe int Test9Add(byte[] aa)
        {
            var v = Vector256<int>.Zero;
            int simdLength = Vector256<int>.Count;
            int i;
            fixed (byte* p = aa)
            {
                for (i = 0; i < aa.Length; i += simdLength)
                {
                    Vector256<int> vv = Avx2.ConvertToVector256Int32(p + i);
                    v = Avx2.Add(v, vv);
                }
            }
            //Vectorから配列にして合計
            int* temp = stackalloc int[simdLength];
            Avx.Store(temp, v);
            int total = 0;
            for (int k = 0; k < simdLength; k++)
            {
                total += temp[k];
            }
            //Vectorの要素数で割り切れなかった余りの配列要素も合計
            for (; i < aa.Length; i++)
            {
                total += aa[i];
            }
            return total;
        }

        //Vectorから配列に変換というか値をコピーその2
        private unsafe void Test8AvxStore()
        {
            Vector256<int> v = Vector256.Create(100);
            int simdLength = Vector256<int>.Count;
            int* temp = stackalloc int[simdLength];
            Avx.Store(temp, v);
            int total = 0;
            for (int i = 0; i < simdLength; i++)
            {
                total += temp[i];
            }
        }

        //Vectorから配列に変換というか値をコピーその1
        private unsafe void Test7AvxStore()
        {
            Vector256<int> v = Vector256.Create(100);
            int simdLength = Vector256<int>.Count;
            int[] temp = new int[simdLength];
            fixed (int* p = temp)
            {
                Avx.Store(p, v);
            }
            int total = temp.Sum();
        }

        //配列の足し算
        private unsafe void Test6Add(byte[] aa)
        {
            Vector256<int> total = Vector256<int>.Zero;
            fixed (byte* ptrA = aa)
            {
                for (int i = 0; i < aa.Length; i += Vector256<int>.Count)
                {
                    Vector256<int> tempV = Avx2.ConvertToVector256Int32(ptrA + i);
                    total = Avx2.Add(total, tempV);
                }
            }
        }

        //オーバーフローしないように、byte型配列からint型Vector作成して足し算
        private unsafe void Test5Add(byte[] aa)
        {
            Vector256<int> v = Vector256.Create((int)250);
            Vector256<int> total;
            fixed (byte* ptrA = aa)
            {
                Vector256<int> tempV = Avx2.ConvertToVector256Int32(ptrA);
                total = Avx2.Add(v, tempV);
            }
        }

        //byte型配列から他の整数型Vectorへの変換
        private unsafe void Test4Convert(byte[] aa)
        {
            fixed (byte* ptrA = aa)
            {
                var v1 = Avx2.ConvertToVector256Int16(ptrA);//short
                var v2 = Avx2.ConvertToVector256Int32(ptrA);//int
                var v3 = Avx2.ConvertToVector256Int64(ptrA);//long
            }
        }

        //Vectorでの足し算、オーバーフロー編
        private unsafe void Test3AddOverflow(byte[] aa)
        {
            Vector256<byte> v = Vector256.Create((byte)250);
            Vector256<byte> total;
            fixed (byte* ptrA = aa)
            {
                Vector256<byte> tempV = Avx.LoadVector256(ptrA);
                total = Avx2.Add(v, tempV);
            }
        }


        //配列からVector作成
        private unsafe void Test2Load(byte[] aa)
        {
            fixed (byte* p = aa)
            {
                var vv = Avx.LoadVector256(p);
                vv = Avx.LoadVector256(p + 1);
                vv = Avx.LoadVector256(p + 2);
                vv = Avx.LoadVector256(p + 32);
            }
        }

        //値からVector作成
        private void Test1VectorCreate()
        {
            Vector256<long> a = Vector256.Create(111, 1, 1, 1);
            Vector256<int> b = Vector256.Create(1322, 1, 1, 1, 1, 1, 1, 1);
            Vector256<short> c = Vector256.Create(54, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 16);
            Vector256<sbyte> d = Vector256.Create(87, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 16, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 32);
            Vector256<double> e = Vector256.Create(1, 1, 1, 1f);
            Vector256<float> f = Vector256.Create(32, 1, 1, 1f, 1, 1, 1, 1);

            var g = Vector256<int>.Zero;//int 0
            var h = Vector256.Create(2);//int
            var i = Vector256.Create(2f);//floar
            var j = Vector256.Create(2d);//double
            var k = Vector256.Create((byte)2);//byte

            //var x = Vector256.Create(1, 1);//エラー
            //整数型なら要素数1,4,8,16,32以外はエラー
            //小数点有りなら1,4,8以外はエラー
        }




    }
}

 

 

 参照したところ

ufcpp.net

 

 

docs.microsoft.com

見てもほとんどわからん 

  

 

 

関連記事

次回は2日後


前回は3日前


2年前…