在最佳实践中,是否可以通过其内存地址访问字段?

问题描述 投票:-1回答:1

我最近买了这本书,游戏引擎开发基础,第1卷:数学,其中的所有代码示例都是C ++。

在Vector3D的C ++实现中,作者创建了这样的索引器:

struct Vector3D
{
    float x, y, z;

    float& operator [](int i)
    {
        return ((&x)[i]);
    }
}

所以,C#等效于:

struct Vector3D
{
    float x, y, z;

    public float this[int index]
    {
        get
        {
            fixed (float* x = &X)
            {
                return x[index];
            }
        }
    }
}

这被允许吗?在C ++中,这是未定义的行为,因此我想知道在C#中这样做是否明智。这绝对是不安全的,但是,如果索引为0、1或2,行为是否会一致?

这里是完整的source code

c# unsafe
1个回答
2
投票

在最佳实践中,通过字段的内存地址访问字段吗?

简而言之:no。不,这不是最佳做法。没有任何性能上的好处,并且您失去了编译器强制的类型安全性。这样做可能会损害性能,因为编译器无法做出尽可能多的假设来优化已编译的程序。永远不要比编译器聪明。

在Vector3D的C ++实现中,作者创建了这样的索引器:

C ++代码正在做两件不同的事情:

  1. 正在返回float&

    • 请注意,float&not指针或“原始”内存地址(内部可以由指针表示,但是当您拥有C ++中的引用时,可以将其转换为一些非常时髦的本机指令逻辑)一个好的编译器)。
    • 在C#中,您可以将float&(实际上是C#中的ref float返回到数组元素或字段。
  2. 假定结构的字段将按与数组元素相同的填充顺序进行布局。

    • 这与C ++语言规范相反:
      • C ++规范仅保证数组中的元素是连续的(并且sizeof()包括必要的填充)。即使结构/类字段是统一类型,也不能将其保证。
    • 在C#中,编译器将积极阻止您编写进行此假设的代码。
    • [C#的代码(&x)[i]表达式没有真正的实用C#等效项-因为它只是错误
    • 这是错误的,因为struct Vector3D缺少#pragma指令来控制结构打包。
  3. 因此,此C ++:

class Foo
{
    int x[3];
}

是否不是

保证具有与此完全相同的内存中表示形式:
class Bar
{
    int x0;
    int x1;
    int x2;
}

此在此质量检查中已说明:Layout in memory of a struct. struct of arrays and array of structs in C/C++

((虽然在典型的x86计算机上它们可能会“正常工作”-索引器也有可能正在读取和写入与数组元素不对应的部分内存,但是因为当使用原始指针时,C ++没有自动运行时边界检查功能,您将不知道代码是否会错误地覆盖内存,直到为时已晚(如果您使用[[lucky

,是操作系统或运行时),从而破坏了进程的内存”内存分配器might检测到错误并终止),这些假设与32位世界中令人毛骨悚然的C ++程序员所用的假设相同,例如,假设sizeof(int*) == sizeof(int)-我们所有人都对此感到高兴我们在2000年代中期移至x64)现在,无论价格多少,C#

does

都可以通过使用unsafe属性为其用户提供有限数量的类型修饰,即使没有[FieldOffset]修饰符也是如此(这是定义[ C#中的C0]以与本机API兼容:通过使用重叠的偏移量)-但这是以性能为代价的(有点直觉上相反:由于本机字对齐问题,较小且有效打包的结构较慢处理)。
union上下文中,我

相信

等效于C ++的C#就是这个(我可能是索引器错误,从我上次不得不使用unsafe C#代码以来已经有5-6年了):
unsafe
而且这么多层次都是错的:

    struct Vector3D { [FieldOffset( sizeof(float) * 0 )] private float x; [FieldOffset( sizeof(float) * 1 )] private float y; [FieldOffset( sizeof(float) * 2 )] private float z; public unsafe float* this[Int32 i] { get { float* x0 = &this.x; return &x0[i]; } } } 解释为按索引的字段数组的指针不会比仅使用开关快,这也将更安全。
  1. 没有边界检查。
  2. 使用&this.x和struct-packing实际上使程序
  3. slower
  4. ,因为值将不会在CPU字边界上对齐。
      当今的机器是64位,并且x86 / x64处理器的速度
    • 明显快得多
    ,当值以字长对齐时。 FieldOffset
如果要在C#中使用快速安全的This QA reports operations are at least twice as fast when they're aligned 3矢量,请执行以下操作:

float

[您也可以在C ++中执行相同的操作,并且与通过字段偏移量指针处理原始内存相比,编译器可以更好地优化直接命名字段访问,因此与原始的struct Vector3
{
    public float x;
    public float y;
    public float z;

    public ref float this[Int32 i]
    {
        get
        {
            switch( i )
            {
            case 0: return ref this.x;
            case 1: return ref this.y;
            case 2: return ref this.z;
            default: throw new ArgumentOutOfRangeException( nameof(i) );
            }
        }
    }
}
类相比,仍然有可能获得更好的性能。] >

更新

响应OP的评论答复中的问题:

将固定缓冲区用作X,Y和Z的后备字段是否有意义?这将消除对switch语句的需要。此外,关于可变结构的主题,具有只读结构的新功能,是否真的有任何时间不应该将结构设为只读?

在这种情况下,没有理由使用缓冲区(Vector3D或其他方式)。因为这是一个fixed,所以将永远只有3个元素,因此它应该仅使用字段(如果使用堆分配的数组,则会失去Vector3的好处。

总结:

原始方法没有优点

和许多缺点(例如缺少边界检查,这对于内存安全性非常重要)。请注意,整数Locality of Reference直接用机器代码编译成非常快的本机跳转表,从而使其实际上是一种零成本的语言功能。
© www.soinside.com 2019 - 2024. All rights reserved.