Spanとは
Unity2021でデフォルトで含まれるようになったライブラリで、System.Memory
に含まれる構造体。
前からDLLを入れれば使ことはできたけども。
何をするためのものか
連続するメモリ領域を読み書きするためのもの。
配列は勿論、stackallocだろうが、マーシャリングしたメモリだろうがSpanとして扱うことができます。
配列をAsSpanしてspanを取得し、spanの値を書き換えると配列にも書き換えが反映されている。つまり、データへの参照を持っています。
C#の大きな欠点として、「配列はなんでもかんでもヒープ」というのがあったと思います。Span(やSpanを利用した様々なAPI)の登場により、ここの問題はだいぶ解決された感じがあります。
MemoryMarshalなどを使うともっと楽しいことができます。
floatは4バイトなので、4~7番地がfloatとして扱われ、floatとして1が代入された状態になっています。配列のコピーなどは行われず、もともとのbyte配列を直接操作しています。
Spanの制約
ref struct
ref structという制約が付いており、これは必ずスタックに置かれる構造体ということ。どういうことかというと、ヒープに置かれる可能性があるだけでコンパイルエラーになります。
これはめちゃくちゃ重い制約で、ゲーム的には非同期で使えないとかが厳しい。そんなときはSystem.Memoryを使います。
System.Memory
MemoryはSpanのファクトリのような構造体で、ref structではないのでSpanのような厳しい制限はありません。勿論プログラムにおいては制限とはパフォーマンス向上の手段であったりコードの秩序維持だったりに必要なものなので、いずれかが下位互換というわけでも上位互換というわけでもないです。
stackallocの制約
一時的に小さい配列を使いたいけどそのためにヒープを使いたくない、そんなときはstackallocが使えます。
が、スタックなので当然デカいメモリ確保はやめた方がいい。そういうときは大人しくヒープを使うが、何度もこういうことがあるならオブジェクトプールを使いたい気持ちになります。
こういうときはSystem.Buffers.ArrayPool
が使えます。名前の通り、配列のオブジェクトプールです。
System.Buffers.ArrayPool
確保される配列長は2のべき乗
上記コードでは128がログに出力されます。先ほどのstackallocと使い分けるコードでAsSpan(0, length)
していた理由はそこで、今必要な部分だけを取得したかったからです。
返却する必要がある
オブジェクトプールなので、借りたものは返すのを忘れないようにする必要があります。必ず後で〇〇する、と言えばDisposableを使ったusingを使えばいいですね。 なので、ArrayPoolをラップした構造体を用意しましょう。
無駄なヒープアロケーションを無くす
こうしてみると、色々な状況に応じてその場での最速の手段が選べるようにAPIが充実しており、本当に使いやすくて素晴らしいものだということがわかります。個人的にはasync/awaitに並ぶくらい革命的に感じますね。
安全性、単純さ、速度の全てが欠けることなく揃っているというのがやはり素晴らしく、アンマネージドメモリ操作の決定版、と言ってもいいんじゃないでしょうか。
僕自身まだまだ使い慣れていないところがあるので、今後も学んで極めていきたいところです。