農工大 Advent Calendar 2024
農工大 Advent Calendar 2024 の 17 日目の記事です。私は農工大について書こうと思いましたがみんな技術系書いているので技術系に変更しました。
はじめに
C/C++ でできる型変換ポインタ操作を C# でやりたいのでまとめました。
例えば double 型のビット列を整数のビット列として見たいとかありますよね?(あってくれ)
例えば高速逆平方根とかにあります。wiki参考
float Q_rsqrt(float number)
{
unsigned int i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = *(unsigned int *)&y;
i = 0x5f3759df - (i >> 1);
y = *(float *)&i;
y = y * (threehalfs - (x2 * y * y));
return y;
}
i = * ( long * ) &y;
y = * ( float * ) &i;
この部分です。こんな感じのポインタ操作を以下の3種類 C# でやります。構造体使ってるのはヒープだけでなくスタックでも使えると幸せだからです。
環境
- .NET 9
- C# 13.0
1. float
を int32
として扱う
先ほど示した高速逆平方根の例です。
unsafe
を使う
float a = 10.0f;
Console.WriteLine($"MathF.ReciprocalSqrtEstimate({a}) = {MathF.ReciprocalSqrtEstimate(a)}");
Console.WriteLine($"Q_rsqrt({a}) = {Q_rsqrt(a)}");
unsafe static float Q_rsqrt( float number )
{
uint i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = *(uint*)&y;
i = 0x5f3759df - ( i >> 1 );
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) );
return y;
}
MathF.ReciprocalSqrtEstimate(10) = 0.3161621
Q_rsqrt(10) = 0.31568578
近似だから誤差があるのは置いといて、 unsafe 使うのはチートすぎるし、プロジェクトの設定変えないと行けないのがめんどい。あと unsafe は Unity で楽に使えない。
BitConverter
を使う
float a = 10.0f;
Console.WriteLine($"MathF.ReciprocalSqrtEstimate({a}) = {MathF.ReciprocalSqrtEstimate(a)}");
Console.WriteLine($"Q_rsqrt({a}) = {Q_rsqrt(a)}");
static float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = BitConverter.SingleToUInt32Bits(y);
i = 0x5f3759df - ( i >> 1 );
y = BitConverter.Int32BitsToSingle((int)i);
y = y * ( threehalfs - ( x2 * y * y ) );
return y;
}
unsafe
使わないしこれ正解かも
2. 構造体を別の構造体として見る
何言ってるんだ?って感じですが、C言語だと以下のようなコードです。別の構造体として見て参照すればメモリ上でちょうど同じ場所のデータを書き換えられます。
#include <stdio.h>
struct int3
{
int x;
int y;
int z;
};
struct int2
{
int x;
int y;
};
int main(void)
{
struct int3 st3 = {1, 2, 3};
struct int2 *st2 = (struct int2 *)&st3;
st2->x = 10;
st2->y = 20;
printf("st3 = {%d, %d, %d}\n", st3.x, st3.y, st3.z);
return 0;
}
st3 = {10, 20, 3}
System.Runtime.CompilerServices.Unsafe
を使う
using System.Runtime.CompilerServices;
int3 st3 = new int3 {x = 1, y = 2, z = 3};
ref int2 st2 = ref Unsafe.As<int3, int2>(ref st3);
st2.x = 10;
st2.y = 20;
Console.WriteLine($"st3 = {{{st3.x}, {st3.y}, {st3.z}}}");
struct int3
{
public int x;
public int y;
public int z;
}
struct int2
{
public int x;
public int y;
}
st3 = {10, 20, 3}
よさそう。でもこれ Unity じゃ使えないじゃん。
StructLayout
で Union
っぽいもの作って使う
using System.Runtime.InteropServices;
IntUnion intUnion;
intUnion.st3 = new int3 {x = 1, y = 2, z = 3};
intUnion.st2 = new int2 {x = 10, y = 20};
Console.WriteLine($"st3 = {{{intUnion.st3.x}, {intUnion.st3.y}, {intUnion.st3.z}}}");
struct int3
{
public int x;
public int y;
public int z;
}
struct int2
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Explicit)]
struct IntUnion
{
[FieldOffset(0)]
public int3 st3;
[FieldOffset(0)]
public int2 st2;
}
st3 = {10, 20, 3}
union
いいですよね。以下のようなC言語コードと同じ感じです。
union intunion
{
struct int3 st3;
struct int2 st2;
};
構造体に StructLayout
属性を付けて LayoutKind.Explicit
を指定すると構造体のメンバーのメモリ上の位置を指定できます。0
にすることで先頭から始められます。st3
もst2
も先頭から始まるようにしてるのでメモリ上の位置を被せています。
Interface
を使う(class なら)
int3 st3 = new int3 {x = 1, y = 2, z = 3};
int2 st2 = st3;
st2.x = 10;
st2.y = 20;
Console.WriteLine($"st3 = {{{st3.x}, {st3.y}, {st3.z}}}");
class int3 : int2
{
public int x { get; set; }
public int y { get; set; }
public int z;
}
interface int2
{
public int x { get; set; }
public int y { get; set; }
}
一番C#らしいコードで一番無難なコードだけどクラスだからヒープに確保されちゃうんだよなぁ、、、スタックで扱いたいときもある。ちなみに構造体でやるとインターフェースに代入したときにコピーされます。
3. byte[]
と構造体を変換する
C言語のコードだとこんな感じのことです。ちなみに構造体はポインタです。
#include <stdio.h>
struct int3
{
int x;
int y;
int z;
};
int main(void)
{
char bytes[12] = "\x01\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00";
struct int3 *st3p = (struct int3 *)&bytes;
printf("st3 = {%d, %d, %d}\n", st3p->x, st3p->y, st3p->z);
return 0;
}
st3 = {1, 2, 2}
逆向きもC言語なら簡単です。
#include <stdio.h>
struct int3
{
int x;
int y;
int z;
};
int main(void)
{
struct int3 st3 = {1, 2, 3};
char *p = (char *)&st3;
printf("p = {%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d}\n", p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9], p[10], p[11]);
return 0;
}
p = {1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0}
byte[]
→ struct
MemoryMarshal.Cast
で Span
経由で使う
using System.Runtime.InteropServices;
byte[] bytes = [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0];
ref int3 st3 = ref MemoryMarshal.Cast<byte, int3>(bytes)[0];
Console.WriteLine($"st3 = {{{st3.x}, {st3.y}, {st3.z}}}");
st3.x = 10;
Console.WriteLine($"bytes = {{{bytes[0]}, {bytes[1]}, {bytes[2]}, {bytes[3]}, {bytes[4]}, {bytes[5]}, {bytes[6]}, {bytes[7]}, {bytes[8]}, {bytes[9]}, {bytes[10]}, {bytes[11]}}}");
struct int3
{
public int x;
public int y;
public int z;
}
st3 = {1, 2, 3}
bytes = {10, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0}
よさそう
st3.x
に 10
代入したら bytes[0]
が 10
になりました。
MemoryMarshal.Cast
で返ってくるは Span<int3>
なので構造体の配列でもできます。今回は単体(要素数が1つ)なので[0]
を指定してます。
struct
→ byte[]
MemoryMarshal.CreateSpan
で Span<int3>
にしてから MemoryMarshal.AsBytes
で Span<byte>
にする
using System.Runtime.InteropServices;
int3 st3 = new int3 { x = 1, y = 2, z = 3 };
var bytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref st3, 1));
Console.WriteLine($"bytes = {{{bytes[0]}, {bytes[1]}, {bytes[2]}, {bytes[3]}, {bytes[4]}, {bytes[5]}, {bytes[6]}, {bytes[7]}, {bytes[8]}, {bytes[9]}, {bytes[10]}, {bytes[11]}}}");
bytes[0] = 10;
Console.WriteLine($"st3 = {{{st3.x}, {st3.y}, {st3.z}}}");
struct int3
{
public int x;
public int y;
public int z;
}
bytes = {1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0}
st3 = {10, 2, 3}
よさそう
bytes[0]
に 10
を代入したら st3.x
が 10
になりました。
MemoryMarshal.CreateSpan
の第二引数は要素数です。先頭の参照から要素数を指定してSpan<int3>
が作れます。
int3[] _st3 = new int3[] { st3,st3,st3 };
ref int3 ref_st3 = ref _st3[0];
Span<int3> span = MemoryMarshal.CreateSpan(ref ref_st3, 3);
まとめ
こんな操作は需要がなさそうに見えますが、プログラムの高速化には必要不可欠です。デカいデータを代入したときにコピーが発生したら重くなってしまいます。「ここにデカいデータ置いたから見てくれ~」ってポインタ渡すほうが速いです。特に構造体とかクラスのインスタンスを簡単に高速にbyte[]
に変換できるならファイル保存とか通信のバイナリとして使えるので幸せです。