アーティスト:Incerry
出典httwww.example.com
著作権:この著作物は、“Attribution-Noncommercial Uses-Sharing the Same Way 4.0 International”ライセンス契約の下でライセンスされています。
注:このブログの著作権はInCerryに帰属します。
1. 前のページ
我们知道在 C#和 Java 明显的一个区别就是 C#可以自定义值类型,也就是今天的主角struct,我们有了更加方便的class为什么微软还加入了struct呢?这其实就是今天要谈到的一个优化性能的 Tips使用结构体替代类。
構造置換を使用する利点は何ですか?クラスの代わりに構造体を使う必要がある場合は?今日の記事はすべての人に答えます。
- ** 注:この記事はすべてx 64ビットプラットフォームの例です **
2. 現実的なケース
現実的なシステムの例を挙げると、誰もが航空券を購入するプロセスを知っています。出発都市と空港(これがルートです)を選択し、希望する日時に応じて希望するフライトとクラスを選択し、支払いを行います。

2.1メモリ使用量
那么全国大约 49 航司,8000 多个航线,平均每个航线有 20 个航班,每个航班平均有 10 组舱位价格(经济舱、头等还有不同的折扣权益),一般 OTA(Online Travel Agency:在线旅游平台)允许预订一年内的机票。也就是说平台可能有8000*20*10*365=~5亿的价格数据(以上数据均来源网络,实际中的数据量不方便透露)。
OTAプラットフォームは、目的のフライトをより速く検索できるようにするために、データベースから人気のあるルートの価格データをメモリにキャッシュします(メモリは、個々のネットワークとディスクよりもはるかに高速です。下の図を参照)。
| オペレーション·オペレーション | スピード感 |
|---|---|
| 命令の実行 | 1/1,000,000,000秒= 1ナノ秒 |
| レベル1キャッシュからのデータの読み込み | 0.5ナノ秒単位 |
| 分岐予測失敗 | 5ナノ秒です |
| 第2キャッシュからのデータの読み込み | 7ナノ秒 |
| Mutexによるロックとロック解除 | 25ナノ秒 |
| メインメモリ(RAMメモリ)からのデータの読み取り | 100ナノ秒 |
| 1 Gbpsのネットワーク上で2 Kbyteのデータを送信 | 2万ナノ秒 |
| メモリから1 MBのデータを読み取る | 25万ナノ秒 |
| ヘッドを新しい位置に移動機械式ハードドライブの代わりに | 800万ナノ秒 |
| ディスクから1 MBのデータを読み取る | 2000万ナノ秒 |
| 米国からヨーロッパにパケットを送信し、戻ってくる | 150 ms = 150,000,000ナノ秒 |
これらのプロパティを持つクラスがあると仮定します(実際にははるかに複雑で、ルート、日付などの次元に分けて格納され、フライトごとに異なる販売ルールがありますが、ここでは無視します)。1億のデータキャッシュにはメモリ内にどれくらいのスペースが必要ですか?
public class FlightPriceClass
{
/// <summary>
/// 航司二字码 如 中国国际航空股份有限公司:CA
/// </summary>
public string Airline { get; set; }
/// <summary>
/// 起始机场三字码 如 上海虹桥国际机场:SHA
/// </summary>
public string Start { get; set; }
/// <summary>
/// 抵达机场三字码 如 北京首都国际机场:PEK
/// </summary>
public string End { get; set; }
/// <summary>
/// 航班号 如 CA0001
/// </summary>
public string FlightNo { get; set; }
/// <summary>
/// 舱位代码 如 Y
/// </summary>
public string Cabin { get; set; }
/// <summary>
/// 价格 单位:元
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// 起飞日期 如 2017-01-01
/// </summary>
public DateOnly DepDate { get; set; }
/// <summary>
/// 起飞时间 如 08:00
/// </summary>
public TimeOnly DepTime { get; set; }
/// <summary>
/// 抵达日期 如 2017-01-01
/// </summary>
public DateOnly ArrDate { get; set; }
/// <summary>
/// 抵达时间 如 08:00
/// </summary>
public TimeOnly ArrTime { get; set; }
}
ベンチマークを書いて、100 Wのデータにどれだけのスペースが必要かを調べ、1億のデータを導出することができます。
// 随机预先生成100W的数据 避免计算逻辑导致结果不准确
public static readonly FlightPriceClass[] FlightPrices = Enumerable.Range(0,
100_0000
).Select(index =>
new FlightPriceClass
{
Airline = $"C{(char)(index % 26 + 'A')}",
Start = $"SH{(char)(index % 26 + 'A')}",
End = $"PE{(char)(index % 26 + 'A')}",
FlightNo = $"{index % 1000:0000}",
Cabin = $"{(char)(index % 26 + 'A')}",
Price = index % 1000,
DepDate = DateOnly.FromDateTime(BaseTime.AddHours(index)),
DepTime = TimeOnly.FromDateTime(BaseTime.AddHours(index)),
ArrDate = DateOnly.FromDateTime(BaseTime.AddHours(3 + index)),
ArrTime = TimeOnly.FromDateTime(BaseTime.AddHours(3 + index)),
}).ToArray();
// 使用类来存储
[Benchmakr]
public FlightPriceClass[] GetClassStore()
{
var arrays = new FlightPriceClass[FlightPrices.Length];
for (int i = 0; i < FlightPrices.Length; i++)
{
var item = FlightPrices[i];
arrays[i] = new FlightPriceClass
{
Airline = item.Airline,
Start = item.Start,
End = item.End,
FlightNo = item.FlightNo,
Cabin = item.Cabin,
Price = item.Price,
DepDate = item.DepDate,
DepTime = item.DepTime,
ArrDate = item.ArrDate,
ArrTime = item.ArrTime
};
}
return arrays;
}
最終結果を見てみましょう。写真は次のようです。

上の図から、100 Wのデータは約107 MBのメモリストレージを必要とすることがわかります。したがって、占有オブジェクトは約112バイト、1億個のオブジェクトは約10.4 GBに等しいです。このサイズはすでにかなり大きいので、メモリフットプリントを削減する他の方法はありませんか?子供たちはいくつかのプログラムを提案した。
- 文字列はintで番号付けできます。
- longを使用してタイムスタンプを格納できます。
- zipのようなものを使って圧縮できます
- 待って待って
この記事のタイトルに照らして、誰もがどのような方法を考えることができるはずです、ねえ、それはクラスの代わりに構造体を使用することです、我々は次のように同じ構造体を定義します。
[StructLayout(LayoutKind.Auto)]
public struct FlightPriceStruct
{
// 属性与类一致
......
}
我们可以使用Unsafe.SizeOf来查看值类型所需要的内存大小,比如像下面这样。

この構造体は88バイトしか必要としません。これはクラスが必要とする112バイトよりも27%少ないことがわかります。どれだけのメモリを節約できるか見てみましょう

結果は非常に良いです。メモリは計算したように27%少なく、割り当ては57%速くなり、さらに重要なことにGCの数も少なくなりました。
なぜ構造体はそんなにメモリを節約できるのでしょうか?ここでは、構造体とクラス格納データの違いについて話す必要があります。次の図は、クラス配列格納形式です。

クラス配列は配列参照要素へのポインタのみを保持し、データを直接格納しないことがわかります。参照型の各インスタンスは以下のようなものを持っています。
- オブジェクトヘッダ:サイズは8バイトで、CoreCLRの記述は、オブジェクトのロック値やHashCodeキャッシュ値など、“オブジェクトにロードする必要があるすべての追加情報”を格納することです。
- メソッドテーブルポインタ:サイズは8Byteで、型のデータ、すなわちよく言及されるTableを指し、MTにはGCInfo、フィールド、メソッドなどがされる。
- オブジェクトプレースホルダ:サイズは8バイトで、現在のGCでは、すべてのオブジェクトが現在のポインタサイズのフィールドを少なくとも1つ必要とします。空クラスの場合は、オブジェクトヘッダーとメソッドテーブルポインタに加えて、8バイトを占有します。空クラスでない場合は、最初のフィールドを格納します。
つまり、空クラスは何も定義しておらず、少なくとも24バイトのスペース、8バイトのオブジェクトヘッダ+8バイトのメソッドテーブルポインタ+8バイトのオブジェクトプレースホルダが必要です。
この記事に戻ると、空クラスではないため、各オブジェクトはデータストアに加えてオブジェクトヘッダとメソッドテーブルを格納するために16バイトの余分な必要があり、配列はオブジェクトへのポインタを格納するために8バイトの余分なスペースを必要とするため、配列内のオブジェクトを格納するために24バイトの余分なスペースが必要です。値型(構造体)を見てみましょう。

上の図から、値型の配列の場合、データは参照なしで配列に直接格納されることがわかります。したがって、同じデータを格納するために、空の構造体ごとに24バイトを節約できます(オブジェクトヘッダ、メソッドテーブル、インスタンスへのポインタは不要です)。
また、構造体配列内の配列も参照型であるため、24バイトのデータがあり、オブジェクトプレースホルダは配列型の最初のフィールドである配列サイズを格納します。
我们可以使用ObjectLayoutInspector这个 NuGet 包打印对象的布局信息,类定义的布局信息如下,可以看到除了数据存储需要的 88byte 以外,还有 16byte 额外空间。

構造体定義のレイアウト情報は以下のとおりです。各構造体が実際のデータストアであり、追加のフットプリントはありません。

那可不可以节省更多的内存呢?我们知道在 64 位平台上一个引用(指针)是 8byte,而在 C#上默认的字符串使用Unicode-16,也就是说 2byte 代表一个字符,像航司二字码、起抵机场这些小于 4 个字符的完全可以使用 char 数组来节省内存,比一个指针占用还要少,那我们修改一下代码。
// 跳过本地变量初始化
[SkipLocalsInit]
// 调整布局方式 使用Explicit自定义布局
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
public struct FlightPriceStructExplicit
{
// 需要手动指定偏移量
[FieldOffset(0)]
// 航司使用两个字符存储
public unsafe fixed char Airline[2];
// 由于航司使用了4byte 所以起始机场偏移4byte
[FieldOffset(4)]
public unsafe fixed char Start[3];
// 同理起始机场使用6byte 偏移10byte
[FieldOffset(10)]
public unsafe fixed char End[3];
[FieldOffset(16)]
public unsafe fixed char FlightNo[4];
[FieldOffset(24)]
public unsafe fixed char Cabin[2];
// decimal 16byte
[FieldOffset(28)]
public decimal Price;
// DateOnly 4byte
[FieldOffset(44)]
public DateOnly DepDate;
// TimeOnly 8byte
[FieldOffset(48)]
public TimeOnly DepTime;
[FieldOffset(56)]
public DateOnly ArrDate;
[FieldOffset(60)]
public TimeOnly ArrTime;
}
新しい構造オブジェクトのレイアウト情報を見てみましょう。

可以看到现在只需要 68byte 了,最后 4byte 是为了地址对齐,因为 CPU 字长是 64bit,我们不用管。按照我们的计算能比 88Byte 节省了 29%的空间。当然使用unsafe fixed char以后就不能直接赋值了,需要进行数据拷贝才行,代码如下。
// 用于设置string值的扩展方法
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void SetTo(this string str, char* dest)
{
fixed (char* ptr = str)
{
Unsafe.CopyBlock(dest, ptr, (uint)(Unsafe.SizeOf<char>() * str.Length));
}
}
// Benchmark的方法
public static unsafe FlightPriceStructExplicit[] GetStructStoreStructExplicit()
{
var arrays = new FlightPriceStructExplicit[FlightPrices.Length];
for (int i = 0; i < FlightPrices.Length; i++)
{
ref var item = ref FlightPrices[i];
arrays[i] = new FlightPriceStructExplicit
{
Price = item.Price,
DepDate = item.DepDate,
DepTime = item.DepTime,
ArrDate = item.ArrDate,
ArrTime = item.ArrTime
};
ref var val = ref arrays[i];
// 需要先fixed 然后再赋值
fixed (char* airline = val.Airline)
fixed (char* start = val.Start)
fixed (char* end = val.End)
fixed (char* flightNo = val.FlightNo)
fixed (char* cabin = val.Cabin)
{
item.Airline.SetTo(airline);
item.Start.SetTo(start);
item.End.SetTo(end);
item.FlightNo.SetTo(flightNo);
item.Cabin.SetTo(cabin);
}
}
return arrays;
}
このストレージブーストが29%のスペースを節約できるかどうかを見てみましょう。

まあ、84 MBから> 65 MBのメモリを約29%節約しました。それは良いことです。
但是我们发现这个 Gen0 Gen1 Gen2 这些 GC 发生了很多次,在实际中的话因为这些都是使用的托管内存,GC 在进行回收的时候会扫描这 65MB 的内存,可能会让它的 STW 变得更久;既然这些是缓存的数据,一段时间内不会回收和改变,那我们能让 GC 别扫描这些嘛?答案是有的,我们可以直接使用非托管内存,使用Marshal类就可以申请和管理非托管内存,可以达到你写 C 语言的时候用的malloc函数类似的效果。
// 分配非托管内存
// 传参是所需要分配的字节数
// 返回值是指向内存的指针
IntPtr Marshal.AllocHGlobal(int cb);
// 释放分配的非托管内存
// 传参是由Marshal分配内存的指针地址
void Marshal.FreeHGlobal(IntPtr hglobal);
Benchmarkのコードを変更し、非管理メモリを使用するように変更します。
// 定义了out ptr参数,用于将指针传回
public static unsafe int GetStructStoreUnManageMemory(out IntPtr ptr)
{
// 使用AllocHGlobal分配内存,大小使用SizeOf计算结构体大小乘需要的数量
var unManagerPtr = Marshal.AllocHGlobal(Unsafe.SizeOf<FlightPriceStructExplicit>() * FlightPrices.Length);
ptr = unManagerPtr;
// 将内存空间指派给FlightPriceStructExplicit数组使用
var arrays = new Span<FlightPriceStructExplicit>(unManagerPtr.ToPointer(), FlightPrices.Length);
for (int i = 0; i < FlightPrices.Length; i++)
{
ref var item = ref FlightPrices[i];
arrays[i] = new FlightPriceStructExplicit
{
Price = item.Price,
DepDate = item.DepDate,
DepTime = item.DepTime,
ArrDate = item.ArrDate,
ArrTime = item.ArrTime
};
ref var val = ref arrays[i];
fixed (char* airline = val.Airline)
fixed (char* start = val.Start)
fixed (char* end = val.End)
fixed (char* flightNo = val.FlightNo)
fixed (char* cabin = val.Cabin)
{
item.Airline.SetTo(airline);
item.Start.SetTo(start);
item.End.SetTo(end);
item.FlightNo.SetTo(flightNo);
item.Cabin.SetTo(cabin);
}
}
// 返回长度
return arrays.Length;
}
// 切记,非托管内存不使用的时候 需要手动释放
[Benchmark]
public void GetStructStoreUnManageMemory()
{
_ = FlightPriceCreate.GetStructStoreUnManageMemory(out var ptr);
// 释放非托管内存
Marshal.FreeHGlobal(ptr);
}
Benchmarkの結果を見てください!

結果は非常に驚くべきことです。管理メモリに割り当てられたスペースがなく、割り当て速度が元よりもはるかに速く、GCが発生したときにメモリのこの部分をスキャンする必要がなく、GC圧力が低減されます。結果は基本的に満足です。
これまでのところ、約6.3 GBの1億のデータを格納していますが、上記の他の方法を使用すると、以下のコードのように、文字列を置き換えるために列挙を使用し、金額は“分”を使用して保存し、タイムスタンプのみを保存するなど、いくつか削減することができます。
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
[SkipLocalsInit]
public struct FlightPriceStructExplicit
{
// 使用byte标识航司 byte范围0~255
[FieldOffset(0)]
public byte Airline;
// 使用无符号整形表示起抵机场和航班号 2^16次方
[FieldOffset(1)]
public UInt16 Start;
[FieldOffset(3)]
public UInt16 End;
[FieldOffset(5)]
public UInt16 FlightNo;
[FieldOffset(7)]
public byte Cabin;
// 不使用decimal 价格精确到分存储
[FieldOffset(8)]
public long PriceFen;
// 使用时间戳替代
[FieldOffset(16)]
public long DepTime;
[FieldOffset(24)]
public long ArrTime;
}
最終的な結果は、各データは32バイトのスペースしか必要とせず、1億を格納する場合は3 GB未満です。

本稿ではこれらの方法を議論しない。
2.2計算速度の計算
構造物の何が問題なのか?我々は計算を見てみましょう、この計算は非常に簡単で、適格なルートをフィルタリングすることであり、最初のクラスと構造体は次のコードメソッドを定義しており、構造体はより特殊で、Span比較を使用します。
// 类和结构体定义的方法 当然实际中的筛选可能更加复杂
// 比较航司
public bool EqulasAirline(string airline)
{
return Airline == airline;
}
// 比较起飞机场
public bool EqualsStart(string start)
{
return Start == start;
}
// 比较抵达机场
public bool EqualsEnd(string end)
{
return End == end;
}
// 比较航班号
public bool EqualsFlightNo(string flightNo)
{
return FlightNo == flightNo;
}
// 价格是否小于指定值
public bool IsPriceLess(decimal min)
{
return Price < min;
}
// 对于Explicit结构体 定义了EqualsSpan方法
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe bool SpanEquals(this string str, char* dest, int length)
{
// 使用span来比较两个数组
return new Span<char>(dest, length).SequenceEqual(str.AsSpan());
}
// 实现的方法如下所示
public static unsafe bool EqualsAirline(FlightPriceStructExplicit item, string airline)
{
// 传需要比较的长度
return airline.SpanEquals(item.Airline, 2);
}
// 下面的方式类似,不再赘述
public static unsafe bool EqualsStart(FlightPriceStructExplicit item, string start)
{
return start.SpanEquals(item.Start, 3);
}
public static unsafe bool EqualsEnd(FlightPriceStructExplicit item, string end)
{
return end.SpanEquals(item.End, 3);
}
public static unsafe bool EqualsFlightNo(FlightPriceStructExplicit item, string flightNo)
{
return flightNo.SpanEquals(item.FlightNo, 4);
}
public static unsafe bool EqualsCabin(FlightPriceStructExplicit item, string cabin)
{
return cabin.SpanEquals(item.Cabin, 2);
}
public static bool IsPriceLess(FlightPriceStructExplicit item, decimal min)
{
return item.Price < min;
}
Benchmarkの最後のコードは次のようになります。各ストレージ構造に対して同じロジックを持ち、100 Wのデータが一度に実行されるため、各ストレージモードのデータ量は150 Wになります。
// 将需要的数据初始化好 避免对测试造成影响
private static readonly FlightPriceClass[] FlightPrices = FlightPriceCreate.GetClassStore();
private static readonly FlightPriceStruct[] FlightPricesStruct = FlightPriceCreate.GetStructStore();
private static readonly FlightPriceStructUninitialized[] FlightPricesStructUninitialized =
FlightPriceCreate.GetStructStoreUninitializedArray();
private static readonly FlightPriceStructExplicit[] FlightPricesStructExplicit =
FlightPriceCreate.GetStructStoreStructExplicit();
// 非托管内存比较特殊 只需要存储指针地址即可
private static IntPtr _unManagerPtr;
private static readonly int FlightPricesStructExplicitUnManageMemoryLength =
FlightPriceCreate.GetStructStoreUnManageMemory(out _unManagerPtr);
[Benchmark(Baseline = true)]
public int GetClassStore()
{
var caAirline = 0;
var shaStart = 0;
var peaStart = 0;
var ca0001FlightNo = 0;
var priceLess500 = 0;
for (int i = 0; i < FlightPrices.Length; i++)
{
// 简单的筛选数据
var item = FlightPrices[i];
if (item.EqualsAirline("CA"))caAirline++;
if (item.EqualsStart("SHA"))shaStart++;
if (item.EqualsEnd("PEA"))peaStart++;
if (item.EqualsFlightNo("0001"))ca0001FlightNo++;
if (item.IsPriceLess(500))priceLess500++;
}
Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");
return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;
}
[Benchmark]
public int GetStructStore()
{
var caAirline = 0;
var shaStart = 0;
var peaStart = 0;
var ca0001FlightNo = 0;
var priceLess500 = 0;
for (int i = 0; i < FlightPricesStruct.Length; i++)
{
var item = FlightPricesStruct[i];
if (item.EqualsAirline("CA"))caAirline++;
if (item.EqualsStart("SHA"))shaStart++;
if (item.EqualsEnd("PEA"))peaStart++;
if (item.EqualsFlightNo("0001"))ca0001FlightNo++;
if (item.IsPriceLess(500))priceLess500++;
}
Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");
return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;
}
[Benchmark]
public int GetFlightPricesStructExplicit()
{
var caAirline = 0;
var shaStart = 0;
var peaStart = 0;
var ca0001FlightNo = 0;
var priceLess500 = 0;
for (int i = 0; i < FlightPricesStructExplicit.Length; i++)
{
var item = FlightPricesStructExplicit[i];
if (FlightPriceStructExplicit.EqualsAirline(item,"CA"))caAirline++;
if (FlightPriceStructExplicit.EqualsStart(item,"SHA"))shaStart++;
if (FlightPriceStructExplicit.EqualsEnd(item,"PEA"))peaStart++;
if (FlightPriceStructExplicit.EqualsFlightNo(item,"0001"))ca0001FlightNo++;
if (FlightPriceStructExplicit.IsPriceLess(item,500))priceLess500++;
}
Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");
return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;
}
[Benchmark]
public unsafe int GetFlightPricesStructExplicitUnManageMemory()
{
var caAirline = 0;
var shaStart = 0;
var peaStart = 0;
var ca0001FlightNo = 0;
var priceLess500 = 0;
var arrays = new Span<FlightPriceStructExplicit>(_unManagerPtr.ToPointer(), FlightPricesStructExplicitUnManageMemoryLength);
for (int i = 0; i < arrays.Length; i++)
{
var item = arrays[i];
if (FlightPriceStructExplicit.EqualsAirline(item,"CA"))caAirline++;
if (FlightPriceStructExplicit.EqualsStart(item,"SHA"))shaStart++;
if (FlightPriceStructExplicit.EqualsEnd(item,"PEA"))peaStart++;
if (FlightPriceStructExplicit.EqualsFlightNo(item,"0001"))ca0001FlightNo++;
if (FlightPriceStructExplicit.IsPriceLess(item,500))priceLess500++;
}
Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");
return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;
}
Benchmarkの結果は以下の通り。

構造体だけではクラスよりも少し遅いことがわかりますが、Explicitレイアウトと非管理メモリを使用する後者ははるかに遅く、2倍以上のギャップがあり、魚と熊は本当に両立できないのでしょうか?
最後の2つの方法が遅い理由を分析してみましょう。なぜなら、値コピーのためです。C#のデフォルトの参照型は参照渡しであり、値型は値渡しです。
- 参照型呼び出しメソッド転送は1回だけコピーする必要があり、長さはCPUワード長で、32ビットシステムは4バイト、64ビットは8バイトです。
- 値型呼び出しメソッドは値の渡しです。例えば、値は4バイトを占有する必要があり、4バイトをコピーする必要があり、CPUワードサイズ以下では利点があり、それ以上では利点が不利になります。
我々の構造体はCPUワード長64ビット8バイトよりもはるかに大きく、後のコード実装では複数の値コピーが発生し、全体的な速度が遅くなりました。
コピーしない方法はありますか?もちろん、値型はC#で参照渡すこともできます。refキーワードがあり、値のコピーがある場所に追加するだけです。
// 改造比较方法,使其支持引用传递
// 加入ref
public static unsafe bool EqualsAirlineRef(ref FlightPriceStructExplicit item, string airline)
{
// 传递的是引用 需要fixed获取指针
fixed(char* ptr = item.Airline)
{
return airline.SpanEquals(ptr, 2);
}
}
// Benchmark内部代码也修改为引用传递
[Benchmark]
public unsafe int GetStructStoreUnManageMemoryRef()
{
var caAirline = 0;
var shaStart = 0;
var peaStart = 0;
var ca0001FlightNo = 0;
var priceLess500 = 0;
var arrays = new Span<FlightPriceStructExplicit>(_unManagerPtr.ToPointer(), FlightPricesStructExplicitUnManageMemoryLength);
for (int i = 0; i < arrays.Length; i++)
{
// 从数组里面拿直接引用
ref var item = ref arrays[i];
// 传参也直接传递引用
if (FlightPriceStructExplicit.EqualsAirlineRef(ref item,"CA"))caAirline++;
if (FlightPriceStructExplicit.EqualsStartRef(ref item,"SHA"))shaStart++;
if (FlightPriceStructExplicit.EqualsEndRef(ref item,"PEA"))peaStart++;
if (FlightPriceStructExplicit.EqualsFlightNoRef(ref item,"0001"))ca0001FlightNo++;
if (FlightPriceStructExplicit.IsPriceLessRef(ref item,500))priceLess500++;
}
Debug.WriteLine($"{caAirline},{shaStart},{peaStart},{ca0001FlightNo},{priceLess500}");
return caAirline + shaStart + peaStart + ca0001FlightNo + priceLess500;
}
結果をもう一度見てみると、Explicit構造体はクラスを使用するよりも33%高速で、前回のラウンドでは非管理メモリを使用して2位にランクインしました。

同じ参照パスで、クラスを使う方が遅いのでしょうか?これは、基本的なコンピューティングユニットに加えて、CPUには、以下の図に示すように、L 1、L 2、L 3データキャッシュがあります。


これはCPUの性能に関係していますが、記事の冒頭の図を覚えていますか?CPU内部キャッシュは最速であるため、最初の理由は、構造体配列データが格納される連続したアドレス空間であり、CPUキャッシュにとって非常に有益です。クラスオブジェクトは参照型であるため、ポインタアクセスが必要であり、CPUキャッシュにとってはあまり有益ではありません。
2つ目の理由は、参照型がアクセスされると、逆参照操作が必要になります。つまり、ポインタを介して対応するメモリ内のデータを見つける必要がありますが、構造体は必要ありません。
那么如何验证我们的观点呢,其实BenchmarkDotNet提供了这样的指标展示,只需要引入BenchmarkDotNet.Diagnostics.Windows NuGet 包,然后在需要评测的类上面加入以下代码。
[HardwareCounters(
HardwareCounter.LlcMisses, // 缓存未命中次数
HardwareCounter.LlcReference)] // 解引用次数
public class SpeedBench : IDisposable
{
......
}
結果は以下のとおりですが、Windows ETWに関する追加の統計情報が必要なため、実行が少し遅くなります。

上の図からわかるように、参照型キャッシュでのミスが最も多く、逆参照が多く、パフォーマンスが低下しています。
次の図に示すように、シーケンシャルストレージ構造体はホップ参照型よりも効率的にアクセスできます。また、オブジェクトのサイズが小さいほど、キャッシュに優しいです。


3. まとめまとめまとめ
この記事では、クラスを構造体で置き換えることで、メモリ使用量を削減し、計算性能をほぼ半分に向上させる方法について説明しました。. NETでの非管理メモリの単純な使用についても説明します。構造は私が本当に好きなもので、非常に効率的なストレージ構造と非常に優れたパフォーマンスを備えています。しかし、すべてのクラスを構造体に変換しないでください。適用可能なシナリオが異なるからです。
では、いつ構造体を使うべきか、いつクラスを使うべきか。マイクロソフトは公式に答えました。
✔型のインスタンスが小さく、一般的に寿命が短い場合、または他のオブジェクトに埋め込まれることが多い場合は、クラスではなく構造体を定義することを検討してください。️
以下の特性をすべて備えていない限り、構造を定義しないでください。
- 論理的にはプリミティブ型(int、doubleなど)に似た単一の値を表します。キャッシュデータは基本的にプリミティブ型です。
- インスタンスサイズは16バイト未満で、値のコピーには莫大なコストがかかりますが、refを使用するとより多くのユースケースが可能になります。
- これは不変です。今日の例では、キャッシュされたデータは変更されないので、この機能があります。
- 頻繁に梱包する必要はありません。頻繁に梱包するとパフォーマンスに大きな損失があります。このシナリオでは、関数はref適合を行っているので、これもありません。
それ以外の場合は、型をクラスとして定義してください。
-
- 実際には、これらの方法からも、C#は簡単な入門ですが、高い上限を持つ言語であり、通常はC#の構文特性、迅速な需要実現を使用することができます。パフォーマンスのボトルネックがある場合は、Cコードを書くようにC#コードを書くことができ、Cと同等のパフォーマンスを得ることができます。