管理到本机值 class 转换:转换指针安全吗?
Managed to native value class conversion: is it safe to cast pointer?
我有一个 C# 项目,它使用库中的 C++ classes。 C# classes 实际上是 C++ classes 的包装器,它们将 C++ 功能暴露给 C# 客户端代码。
在许多地方,C++ 值 classes 被转换为 C# 包装器并向后转换。
在代码审查期间,我发现了两种转换 classes 的方法:通过 reinterpret_cast(参见 operator * )和通过 pin_ptr(参见 MultiplyBy);
如您所见,本机和托管 class 都有三个 'double' 字段,这就是为什么有人使用 reinterpret_cast;
在许多地方 classes 是使用 memcpy 从 C# 复制到 C++:
memcpy(&NativePointInstance, &ManagedPointIntance, sizeof(double)*3);
我从一位开发人员那里听说,在某些情况下,当我们使用 C# 值 classes 时,reinterpret_cast 是安全的。
问题是:
什么时候可以安全地在 C# 值 classes 上使用 reinterpret_cast,什么时候不能?
在这种情况下,转换指针的最正确方法是什么——比如在运算符 * 中或在 MultiplyBy 中,或者其他替代方法?
谁能详细解释一下 MultiplyBy() 中发生了什么,这些技巧是如何工作的?
据我了解,问题可能是由于优化器可能会更改字段顺序、GC 可能会重组堆以及托管代码和本机代码之间的字段对齐方式可能不同所致。
// this is C++ native class
class NativePoint
{
public:
double x;
double y;
double z;
NativePoint(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
NativePoint operator * (int value)
{
return NativePoint(x * value, y * value, z * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint
{
internal:
double x;
double y;
double z;
ManagedPoint(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
public:
static ManagedPoint operator * (ManagedPoint a, double value)
{
return ManagedPoint((*reinterpret_cast<NativePoint*>(&(a))) * value);
}
ManagedPoint MultiplyBy(double value)
{
pin_ptr<ManagedPoint> pThisTmp = &*this;
NativePoint* pThis = reinterpret_cast<NativePoint*>(&*pThisTmp);
return ManagedPoint(*pThis * value);
}
};
// this should be called from C# code, or another .NET app
int main(array<System::String ^> ^args)
{
NativePoint p_native = NativePoint(1, 1, 1);
ManagedPoint p = ManagedPoint(p_native);
Console::WriteLine("p is {" + p.x + ", " + p.y + ", " + p.z + "}");
ManagedPoint p1 = p * 5;
Console::WriteLine("p1 is {" + p1.x + ", " + p1.y + ", " + p1.z + "}");
ManagedPoint p2 = p.MultiplyBy(5);
Console::WriteLine("p2 is {" + p2.x + ", " + p2.y + ", " + p2.z + "}");
Console::ReadLine();
return 0;
}
好吧,我最终使用了原生 类 的常用构造函数。它对我来说绝对安全,并且从剩余的变体中最快。来自 Marshal::PtrToStructure() 的评论的想法很好,但对于我的测试示例,执行速度比使用构造函数的速度慢。指针转换是最快的解决方案,但是在评论中的非常可怕的例子之后,我不会再冒险使用它了(除非我们真的需要优化它,然后 LayoutKind::Explicit 应该做的事情)。
这是我用于测试的代码:
// this is C++ native class
class NativePoint
{
public:
double x;
double y;
double z;
NativePoint()
{
}
NativePoint(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
NativePoint operator * (int value)
{
return NativePoint(x * value, y * value, z * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint
{
internal:
double x;
double y;
double z;
ManagedPoint(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
ManagedPoint(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
public:
static ManagedPoint operator * (ManagedPoint a, double value)
{
return ManagedPoint((*reinterpret_cast<NativePoint*>(&(a))) * value);
}
ManagedPoint MultiplyBy(double value)
{
pin_ptr<ManagedPoint> pThisTmp = &*this;
NativePoint* pThis = reinterpret_cast<NativePoint*>(&*pThisTmp);
return ManagedPoint(*pThis * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint2
{
internal:
double x;
double y;
double z;
ManagedPoint2(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
ManagedPoint2(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
public:
static ManagedPoint2 operator * (ManagedPoint2 a, double value)
{
return ManagedPoint2((NativePoint(a.x, a.y, a.z)) * value);
}
ManagedPoint2 MultiplyBy(double value)
{
return ManagedPoint2((NativePoint(this->x, this->y, this->z)) * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint3
{
internal:
double x;
double y;
double z;
ManagedPoint3(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
ManagedPoint3(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
public:
static ManagedPoint3 operator * (ManagedPoint3 a, double value)
{
NativePoint p;
Marshal::StructureToPtr(a, IntPtr(&p), false);
return ManagedPoint3(p * value);
}
ManagedPoint3 MultiplyBy(double value)
{
NativePoint p;
Marshal::StructureToPtr(*this, IntPtr(&p), false);
return ManagedPoint3(p * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint4
{
internal:
double x;
double y;
double z;
ManagedPoint4(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
ManagedPoint4(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
public:
static ManagedPoint4 operator * (ManagedPoint4 a, double value)
{
return ManagedPoint4(ManagedPoint4::ToNative(a) * value);
}
ManagedPoint4 MultiplyBy(double value)
{
return ManagedPoint4(ManagedPoint4::ToNative(*this) * value);
}
static NativePoint ToNative(const ManagedPoint4& pp)
{
NativePoint p;
Marshal::StructureToPtr(pp, IntPtr(&p), false);
return p;
}
};
// this should be called from C# code, or another .NET app
int main(array<System::String ^> ^args)
{
Stopwatch time;
time.Start();
for (int i = 0; i < 10000000; i++)
{
ManagedPoint a = ManagedPoint(1, 2, 3) * 4;
}
time.Stop();
Console::WriteLine("time: " + time.ElapsedMilliseconds);
Stopwatch time2;
time2.Start();
for (int i = 0; i < 10000000; i++)
{
ManagedPoint2 a2 = ManagedPoint2(1, 2, 3) * 4;
}
time2.Stop();
Console::WriteLine("time2: " + time2.ElapsedMilliseconds);
Stopwatch time3;
time3.Start();
for (int i = 0; i < 10000000; i++)
{
ManagedPoint3 a3 = ManagedPoint3(1, 2, 3) * 4;
}
time3.Stop();
Console::WriteLine("time3: " + time3.ElapsedMilliseconds);
Stopwatch time4;
time4.Start();
for (int i = 0; i < 10000000; i++)
{
ManagedPoint4 a3 = ManagedPoint4(1, 2, 3) * 4;
}
time4.Stop();
Console::WriteLine("time4: " + time4.ElapsedMilliseconds);
Console::ReadLine();
Console::WriteLine("======================================================");
Console::WriteLine();
return 0;
}
这是输出:
time: 374
time2: 382
time3: 857
time4: 961
time: 395
time2: 413
time3: 900
time4: 968
time: 376
time2: 378
time3: 840
time4: 909
我有一个 C# 项目,它使用库中的 C++ classes。 C# classes 实际上是 C++ classes 的包装器,它们将 C++ 功能暴露给 C# 客户端代码。 在许多地方,C++ 值 classes 被转换为 C# 包装器并向后转换。 在代码审查期间,我发现了两种转换 classes 的方法:通过 reinterpret_cast(参见 operator * )和通过 pin_ptr(参见 MultiplyBy); 如您所见,本机和托管 class 都有三个 'double' 字段,这就是为什么有人使用 reinterpret_cast;
在许多地方 classes 是使用 memcpy 从 C# 复制到 C++: memcpy(&NativePointInstance, &ManagedPointIntance, sizeof(double)*3);
我从一位开发人员那里听说,在某些情况下,当我们使用 C# 值 classes 时,reinterpret_cast 是安全的。
问题是: 什么时候可以安全地在 C# 值 classes 上使用 reinterpret_cast,什么时候不能? 在这种情况下,转换指针的最正确方法是什么——比如在运算符 * 中或在 MultiplyBy 中,或者其他替代方法?
谁能详细解释一下 MultiplyBy() 中发生了什么,这些技巧是如何工作的?
据我了解,问题可能是由于优化器可能会更改字段顺序、GC 可能会重组堆以及托管代码和本机代码之间的字段对齐方式可能不同所致。
// this is C++ native class
class NativePoint
{
public:
double x;
double y;
double z;
NativePoint(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
NativePoint operator * (int value)
{
return NativePoint(x * value, y * value, z * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint
{
internal:
double x;
double y;
double z;
ManagedPoint(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
public:
static ManagedPoint operator * (ManagedPoint a, double value)
{
return ManagedPoint((*reinterpret_cast<NativePoint*>(&(a))) * value);
}
ManagedPoint MultiplyBy(double value)
{
pin_ptr<ManagedPoint> pThisTmp = &*this;
NativePoint* pThis = reinterpret_cast<NativePoint*>(&*pThisTmp);
return ManagedPoint(*pThis * value);
}
};
// this should be called from C# code, or another .NET app
int main(array<System::String ^> ^args)
{
NativePoint p_native = NativePoint(1, 1, 1);
ManagedPoint p = ManagedPoint(p_native);
Console::WriteLine("p is {" + p.x + ", " + p.y + ", " + p.z + "}");
ManagedPoint p1 = p * 5;
Console::WriteLine("p1 is {" + p1.x + ", " + p1.y + ", " + p1.z + "}");
ManagedPoint p2 = p.MultiplyBy(5);
Console::WriteLine("p2 is {" + p2.x + ", " + p2.y + ", " + p2.z + "}");
Console::ReadLine();
return 0;
}
好吧,我最终使用了原生 类 的常用构造函数。它对我来说绝对安全,并且从剩余的变体中最快。来自 Marshal::PtrToStructure() 的评论的想法很好,但对于我的测试示例,执行速度比使用构造函数的速度慢。指针转换是最快的解决方案,但是在评论中的非常可怕的例子之后,我不会再冒险使用它了(除非我们真的需要优化它,然后 LayoutKind::Explicit 应该做的事情)。
这是我用于测试的代码:
// this is C++ native class
class NativePoint
{
public:
double x;
double y;
double z;
NativePoint()
{
}
NativePoint(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
NativePoint operator * (int value)
{
return NativePoint(x * value, y * value, z * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint
{
internal:
double x;
double y;
double z;
ManagedPoint(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
ManagedPoint(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
public:
static ManagedPoint operator * (ManagedPoint a, double value)
{
return ManagedPoint((*reinterpret_cast<NativePoint*>(&(a))) * value);
}
ManagedPoint MultiplyBy(double value)
{
pin_ptr<ManagedPoint> pThisTmp = &*this;
NativePoint* pThis = reinterpret_cast<NativePoint*>(&*pThisTmp);
return ManagedPoint(*pThis * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint2
{
internal:
double x;
double y;
double z;
ManagedPoint2(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
ManagedPoint2(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
public:
static ManagedPoint2 operator * (ManagedPoint2 a, double value)
{
return ManagedPoint2((NativePoint(a.x, a.y, a.z)) * value);
}
ManagedPoint2 MultiplyBy(double value)
{
return ManagedPoint2((NativePoint(this->x, this->y, this->z)) * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint3
{
internal:
double x;
double y;
double z;
ManagedPoint3(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
ManagedPoint3(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
public:
static ManagedPoint3 operator * (ManagedPoint3 a, double value)
{
NativePoint p;
Marshal::StructureToPtr(a, IntPtr(&p), false);
return ManagedPoint3(p * value);
}
ManagedPoint3 MultiplyBy(double value)
{
NativePoint p;
Marshal::StructureToPtr(*this, IntPtr(&p), false);
return ManagedPoint3(p * value);
}
};
// this class managed C++ class
[StructLayout(LayoutKind::Sequential)]
public value class ManagedPoint4
{
internal:
double x;
double y;
double z;
ManagedPoint4(const NativePoint& p)
{
x = p.x;
y = p.y;
z = p.z;
}
ManagedPoint4(double x, double y, double z)
{
this->x = x;
this->y = y;
this->z = z;
}
public:
static ManagedPoint4 operator * (ManagedPoint4 a, double value)
{
return ManagedPoint4(ManagedPoint4::ToNative(a) * value);
}
ManagedPoint4 MultiplyBy(double value)
{
return ManagedPoint4(ManagedPoint4::ToNative(*this) * value);
}
static NativePoint ToNative(const ManagedPoint4& pp)
{
NativePoint p;
Marshal::StructureToPtr(pp, IntPtr(&p), false);
return p;
}
};
// this should be called from C# code, or another .NET app
int main(array<System::String ^> ^args)
{
Stopwatch time;
time.Start();
for (int i = 0; i < 10000000; i++)
{
ManagedPoint a = ManagedPoint(1, 2, 3) * 4;
}
time.Stop();
Console::WriteLine("time: " + time.ElapsedMilliseconds);
Stopwatch time2;
time2.Start();
for (int i = 0; i < 10000000; i++)
{
ManagedPoint2 a2 = ManagedPoint2(1, 2, 3) * 4;
}
time2.Stop();
Console::WriteLine("time2: " + time2.ElapsedMilliseconds);
Stopwatch time3;
time3.Start();
for (int i = 0; i < 10000000; i++)
{
ManagedPoint3 a3 = ManagedPoint3(1, 2, 3) * 4;
}
time3.Stop();
Console::WriteLine("time3: " + time3.ElapsedMilliseconds);
Stopwatch time4;
time4.Start();
for (int i = 0; i < 10000000; i++)
{
ManagedPoint4 a3 = ManagedPoint4(1, 2, 3) * 4;
}
time4.Stop();
Console::WriteLine("time4: " + time4.ElapsedMilliseconds);
Console::ReadLine();
Console::WriteLine("======================================================");
Console::WriteLine();
return 0;
}
这是输出:
time: 374
time2: 382
time3: 857
time4: 961
time: 395
time2: 413
time3: 900
time4: 968
time: 376
time2: 378
time3: 840
time4: 909