2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

農工大Advent Calendar 2024

Day 17

C/C++の型変換ポインタ操作をC#でもやりたい

Last updated at Posted at 2024-12-16

農工大 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. floatint32 として扱う

先ほど示した高速逆平方根の例です。

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 で楽に使えない。

image.png

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 じゃ使えないじゃん。

StructLayoutUnion っぽいもの作って使う

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にすることで先頭から始められます。st3st2も先頭から始まるようにしてるのでメモリ上の位置を被せています。

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.CastSpan 経由で使う

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.x10 代入したら bytes[0]10 になりました。

MemoryMarshal.Cast で返ってくるは Span<int3> なので構造体の配列でもできます。今回は単体(要素数が1つ)なので[0]を指定してます。

structbyte[]

MemoryMarshal.CreateSpanSpan<int3> にしてから MemoryMarshal.AsBytesSpan<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.x10 になりました。 

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[]に変換できるならファイル保存とか通信のバイナリとして使えるので幸せです。

2
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?