C# 为什么通过指针强制转换结构是慢的,而不安全的,因为是快的? 背景
我想制作一些整数大小的C# 为什么通过指针强制转换结构是慢的,而不安全的,因为是快的? 背景,c#,performance,struct,unsafe,c#-7.2,C#,Performance,Struct,Unsafe,C# 7.2,我想制作一些整数大小的structs(即32位和64位),它们可以轻松地与相同大小的原始非托管类型进行转换(特别是对于32位大小的struct,Int32和UInt32) 然后,结构将公开整数类型上无法直接使用的位操作/索引的附加功能。基本上,作为一种语法糖,提高可读性和易用性 然而,重要的部分是性能,因为这种额外的抽象本质上应该是零成本的(在一天结束时,CPU应该“看到”相同的位,就像它处理原始整数一样) 示例结构 下面是我提出的非常基本的结构。它没有所有的功能,但足以说明我的问题: [Str
struct
s(即32位和64位),它们可以轻松地与相同大小的原始非托管类型进行转换(特别是对于32位大小的struct,Int32
和UInt32
)
然后,结构将公开整数类型上无法直接使用的位操作/索引的附加功能。基本上,作为一种语法糖,提高可读性和易用性
然而,重要的部分是性能,因为这种额外的抽象本质上应该是零成本的(在一天结束时,CPU应该“看到”相同的位,就像它处理原始整数一样)
示例结构
下面是我提出的非常基本的结构。它没有所有的功能,但足以说明我的问题:
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 4)]
public struct Mask32 {
[FieldOffset(3)]
public byte Byte1;
[FieldOffset(2)]
public ushort UShort1;
[FieldOffset(2)]
public byte Byte2;
[FieldOffset(1)]
public byte Byte3;
[FieldOffset(0)]
public ushort UShort2;
[FieldOffset(0)]
public byte Byte4;
[DebuggerStepThrough, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe implicit operator Mask32(int i) => *(Mask32*)&i;
[DebuggerStepThrough, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe implicit operator Mask32(uint i) => *(Mask32*)&i;
}
测试
我想测试这个结构的性能。特别是,我想看看它是否能让我像使用常规的按位算术一样快速地获取单个字节:(I>>8)&0xFF
(例如获取第三个字节)
下面您将看到我提出的基准:
public unsafe class MyBenchmark {
const int count = 50000;
[Benchmark(Baseline = true)]
public static void Direct() {
var j = 0;
for (int i = 0; i < count; i++) {
//var b1 = i.Byte1();
//var b2 = i.Byte2();
var b3 = i.Byte3();
//var b4 = i.Byte4();
j += b3;
}
}
[Benchmark]
public static void ViaStructPointer() {
var j = 0;
int i = 0;
var s = (Mask32*)&i;
for (; i < count; i++) {
//var b1 = s->Byte1;
//var b2 = s->Byte2;
var b3 = s->Byte3;
//var b4 = s->Byte4;
j += b3;
}
}
[Benchmark]
public static void ViaStructPointer2() {
var j = 0;
int i = 0;
for (; i < count; i++) {
var s = *(Mask32*)&i;
//var b1 = s.Byte1;
//var b2 = s.Byte2;
var b3 = s.Byte3;
//var b4 = s.Byte4;
j += b3;
}
}
[Benchmark]
public static void ViaStructCast() {
var j = 0;
for (int i = 0; i < count; i++) {
Mask32 m = i;
//var b1 = m.Byte1;
//var b2 = m.Byte2;
var b3 = m.Byte3;
//var b4 = m.Byte4;
j += b3;
}
}
[Benchmark]
public static void ViaUnsafeAs() {
var j = 0;
for (int i = 0; i < count; i++) {
var m = Unsafe.As<int, Mask32>(ref i);
//var b1 = m.Byte1;
//var b2 = m.Byte2;
var b3 = m.Byte3;
//var b4 = m.Byte4;
j += b3;
}
}
}
编辑:修复了确保实际使用变量的代码。还注释掉了4个变量中的3个,以真正测试结构转换/成员访问,而不是实际使用这些变量
结果
我在版本中运行了这些,并在x64上进行了优化
Intel Core i7-3770K CPU 3.50GHz (Ivy Bridge), 1 CPU, 8 logical cores and 4 physical cores
Frequency=3410223 Hz, Resolution=293.2360 ns, Timer=TSC
[Host] : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.6.1086.0
DefaultJob : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.6.1086.0
Method | Mean | Error | StdDev | Scaled | ScaledSD |
------------------ |----------:|----------:|----------:|-------:|---------:|
Direct | 14.47 us | 0.3314 us | 0.2938 us | 1.00 | 0.00 |
ViaStructPointer | 111.32 us | 0.6481 us | 0.6062 us | 7.70 | 0.15 |
ViaStructPointer2 | 102.31 us | 0.7632 us | 0.7139 us | 7.07 | 0.14 |
ViaStructCast | 29.00 us | 0.3159 us | 0.2800 us | 2.01 | 0.04 |
ViaUnsafeAs | 14.32 us | 0.0955 us | 0.0894 us | 0.99 | 0.02 |
编辑:修复代码后的新结果:
Method | Mean | Error | StdDev | Scaled | ScaledSD |
------------------ |----------:|----------:|----------:|-------:|---------:|
Direct | 57.51 us | 1.1070 us | 1.0355 us | 1.00 | 0.00 |
ViaStructPointer | 203.20 us | 3.9830 us | 3.5308 us | 3.53 | 0.08 |
ViaStructPointer2 | 198.08 us | 1.8411 us | 1.6321 us | 3.45 | 0.06 |
ViaStructCast | 79.68 us | 1.5478 us | 1.7824 us | 1.39 | 0.04 |
ViaUnsafeAs | 57.01 us | 0.8266 us | 0.6902 us | 0.99 | 0.02 |
问题
基准测试结果令我惊讶,这就是为什么我有几个问题:
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 4)]
public struct Mask32 {
[FieldOffset(3)]
public byte Byte1;
[FieldOffset(2)]
public ushort UShort1;
[FieldOffset(2)]
public byte Byte2;
[FieldOffset(1)]
public byte Byte3;
[FieldOffset(0)]
public ushort UShort2;
[FieldOffset(0)]
public byte Byte4;
[DebuggerStepThrough, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe implicit operator Mask32(int i) => *(Mask32*)&i;
[DebuggerStepThrough, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe implicit operator Mask32(uint i) => *(Mask32*)&i;
}
编辑:修改代码后,保留的问题更少,因此变量实际得到使用
为什么指针的速度这么慢
为什么演员阵容需要两倍于基线的时间?隐式/显式运算符不是内联的吗
为什么新的System.Runtime.CompilerServices.Unsafe
包(4.5.0版)这么快?我想它至少会涉及一个方法调用
更一般地说,我怎样才能制作一个基本上是零成本的结构,它可以简单地充当某个内存的“窗口”或像UInt64
这样更大的原语类型,以便更有效地操作/读取该内存?这里的最佳实践是什么
对此的答案似乎是,当您使用Unsafe.As()
时,JIT编译器可以更好地进行某些优化
Unsafe.As()
的实现非常简单,如下所示:
public static ref TTo As<TFrom, TTo>(ref TFrom source)
{
return ref source;
}
正如您所看到的,ViaUnsafeAs
确实要快得多
让我们看看编译器生成了什么:
public static unsafe int ViaStructPointer()
{
int total = 0;
for (int i = 0; i < Program.count; i++)
{
total += (*(Mask32*)(&i)).Byte1;
}
return total;
}
public static int ViaUnsafeAs()
{
int total = 0;
for (int i = 0; i < Program.count; i++)
{
total += (Unsafe.As<int, Mask32>(ref i)).Byte1;
}
return total;
}
现在,我想这就更清楚了,为什么抖动可以很好地优化这一点。当您获取本地地址时,jit通常必须将该本地地址保留在堆栈上。这里就是这样。在viapoint
版本中,堆栈上保留i
。在via
中,i
被复制到一个临时文件,该临时文件保存在堆栈上。前者速度较慢,因为i
还用于控制循环的迭代
使用以下代码,您可以非常接近viasafure
perf,在这里您可以显式复制:
public static int ViaStructPointer2()
{
int total = 0;
for (int i = 0; i < count; i++)
{
int j = i;
var s = (Mask32*)&j;
total += s->Byte1;
}
return total;
}
ViaStructPointer took 00:00:00.1147793
ViaUnsafeAs took 00:00:00.0282828
ViaStructPointer2 took 00:00:00.0257589
public static int ViaStructPointer2()
{
int-total=0;
for(int i=0;iByte1;
}
返回总数;
}
ViaStructPointer时间:00:00:00.1147793
ViaUnsafeAs拍摄时间为00:00:00.0282828
ViaStructPointer2采用00:00:00.0257589
如果你能发布一个可编译的控制台应用程序,那就太好了。事实上,这些东西不会被编译,我想大多数人不会花时间去修复它……我尝试了一些测试方法,编译器正在从循环中删除代码,因为循环中的变量没有被使用。因此我怀疑你的测试是没有意义的,因为有时候你在用一种语言编程,你真的需要做些别的事情,但您仍然希望使用99%的代码所使用的语言。@Claies C#在这方面非常有效-能够以与旧程序相同的速度完成任务非常好。这就是将新的内存
、Span
等添加到语言中的全部原因。@FitDev您仍在进行操作,没有副作用。简单地修改局部变量不会引起副作用。智能编译器可以将其全部删除。它是这样工作的:如果一个局部变量是写的而不是读的,那么它是无用的,可以删除。您需要执行一个带有副作用的if
(如抛出
),或将值添加到静态变量或类似的操作。非常感谢您的详细回答和建议!那么,您是否认为依赖不安全是一种安全的选择(对于.NET Framework 4.6.1+和.NET Core 2+),因为性能原因,通常会尝试避免使用指针进行强制转换?@FitDev是的,我认为是这样;他们正是为了这种目的而设计的@MatthewWatson我查看了源代码,发现了以下内容:。为什么它们是抛出而不是返回?@aloisdg因为该平台不支持它。@MatthewWatson不是因为它们是内部函数(请参见每个方法的属性)?方法的主体将被jit/执行环境所取代(或者至少我是这样理解的)。请参见该链接顶部的注释
public static unsafe int ViaStructPointer()
{
int total = 0;
for (int i = 0; i < Program.count; i++)
{
total += (*(Mask32*)(&i)).Byte1;
}
return total;
}
public static int ViaUnsafeAs()
{
int total = 0;
for (int i = 0; i < Program.count; i++)
{
total += (Unsafe.As<int, Mask32>(ref i)).Byte1;
}
return total;
}
.method public hidebysig static int32 ViaStructPointer () cil managed
{
.locals init (
[0] int32 total,
[1] int32 i,
[2] valuetype Demo.Mask32* s
)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: br.s IL_0017
.loop
{
IL_0006: ldloca.s i
IL_0008: conv.u
IL_0009: stloc.2
IL_000a: ldloc.0
IL_000b: ldloc.2
IL_000c: ldfld uint8 Demo.Mask32::Byte1
IL_0011: add
IL_0012: stloc.0
IL_0013: ldloc.1
IL_0014: ldc.i4.1
IL_0015: add
IL_0016: stloc.1
IL_0017: ldloc.1
IL_0018: ldsfld int32 Demo.Program::count
IL_001d: blt.s IL_0006
}
IL_001f: ldloc.0
IL_0020: ret
}
.method public hidebysig static int32 ViaUnsafeAs () cil managed
{
.locals init (
[0] int32 total,
[1] int32 i,
[2] valuetype Demo.Mask32 m
)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: br.s IL_0020
.loop
{
IL_0006: ldloca.s i
IL_0008: call valuetype Demo.Mask32& [System.Runtime.CompilerServices.Unsafe]System.Runtime.CompilerServices.Unsafe::As<int32, valuetype Demo.Mask32>(!!0&)
IL_000d: ldobj Demo.Mask32
IL_0012: stloc.2
IL_0013: ldloc.0
IL_0014: ldloc.2
IL_0015: ldfld uint8 Demo.Mask32::Byte1
IL_001a: add
IL_001b: stloc.0
IL_001c: ldloc.1
IL_001d: ldc.i4.1
IL_001e: add
IL_001f: stloc.1
IL_0020: ldloc.1
IL_0021: ldsfld int32 Demo.Program::count
IL_0026: blt.s IL_0006
}
IL_0028: ldloc.0
IL_0029: ret
}
ViaStructPointer: conv.u
ViaUnsafeAs: call valuetype Demo.Mask32& [System.Runtime.CompilerServices.Unsafe]System.Runtime.CompilerServices.Unsafe::As<int32, valuetype Demo.Mask32>(!!0&)
ldobj Demo.Mask32
.method public hidebysig static !!TTo& As<TFrom, TTo> (
!!TFrom& source
) cil managed aggressiveinlining
{
.custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = (
01 00 00 00
)
IL_0000: ldarg.0
IL_0001: ret
}
public static int ViaStructPointer2()
{
int total = 0;
for (int i = 0; i < count; i++)
{
int j = i;
var s = (Mask32*)&j;
total += s->Byte1;
}
return total;
}
ViaStructPointer took 00:00:00.1147793
ViaUnsafeAs took 00:00:00.0282828
ViaStructPointer2 took 00:00:00.0257589