5

Long story short, I'll provide a simplistic example where it might be useful:

public struct Vector3f {
    public float x;
    public float y;
    public float z;

    public unsafe float this[int index] {
        get {
            // Get "p" somehow, so that it points to "this"...

            return p[index];
        }

        set {
            // Get "p" somehow, so that it points to "this"...

            p[index] = value;
        }
    }
}

I guess you got my point there:

var v = new Vector3f();

Assert(v.x == v[0]);

EDIT 1:

For those, who still ask :)

Assert(v.y == v[1]);
Assert(v.z == v[2]);

EDIT 2:

Does fixed create redundant overhead here? Or maybe this struct is already fixed, and therefore fixed has no effect here and is only needed to satisfy the compiler? Possible answer.

Community
  • 1
  • 1
Alexander Shukaev
  • 16,674
  • 8
  • 70
  • 85

4 Answers4

9

First off, I would not use unsafe code for this unless I had first determined (1) that the obvious code with the switch would be the slowest code in the entire program and causing significant, user-observable slowdowns, and that (2) going to unsafe code fixes the performance problem.

Second, if I were to use unsafe code, it is extraordinarily dangerous to make assumptions about structure packing. The CLR is allowed broad lattitude in how it chooses to pack structures. If you are going to do this dangerous thing then you should use the struct layout attribute to ensure that the floats are exactly where you need them to be.

Third, what stops a buggy caller from passing a negative index, or a too-large index?

Fourth:

Does fixed create redundant overhead here?

I don't know what "redundant overhead" means. "fixed" makes the jitter tell the garbage collector "do not move this thing because I need to do pointer arithmetic on it". You are fixing for a short period, which is ideal; fixing for a long time makes it more likely that a collection will get messed up because the pinned storage could not be moved.

Fifth:

Or maybe this struct is already fixed, and therefore fixed has no effect here and is only needed to satisfy the compiler?

Maybe! Maybe the variable referred to by "this" is already a fixed variable. Maybe it isn't. How is the compiler supposed to know whether the "this" of the struct is a ref to fixed storage or not? We have to assume the worst, so you are required to fix it.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • But the `struct` in the question has a sequential layout in managed memory and contiguously packed types by default, doesn't it? – ordag Jan 06 '12 at 16:14
  • @Eric yeah I've got the same question - I read somewhere that structs have strict sequential layout policy. Anyway my struct already explicitly defines the layout, so this is not a problem. – Alexander Shukaev Jan 06 '12 at 16:35
  • 4
    @ordag: I don't know -- does it? The C# specification states "The order in which members are packed into a struct is unspecified. For alignment purposes, there may be unnamed padding at the beginning of a struct, within a struct, and at the end of the struct." You are given *no guarantee whatsoever* that structures are packed in any particular way. If some implementation that you *happen* to be using just *happens* to pack structures in a way that you *happen* to like, that's your good luck. How far do you want to trust that luck? – Eric Lippert Jan 06 '12 at 16:37
  • @Eric However, I am concerned more about the "fixed" overhead, because I've read somewhere that pinning many times results in performance penalties. In other words, it's better to pin something once and work with it as much as possible, rather than repin it numerous times on demand. What I'm trying to figure out here is whether performance will suffer when my code has lots of "v[i]" expressions which cause "fixing" to happen every time. To be even more clear (if you still don't understand what is "redundant overhead"), you can think of "fixed" it as memory locking concept. – Alexander Shukaev Jan 06 '12 at 16:37
  • 1
    @Haroogan: I have no idea what happens in the garbage collector when you run your code on your machine. You are the only one who can possibly know that. If you want to know if there's a performance problem when you run your code on your machine, then *run your code on your machine and see if it is too slow*. – Eric Lippert Jan 06 '12 at 16:39
  • The drawback of memory locking - is performance penalty. Therefore it is usually recommended to do as much work as possible on the locked memory and free it ASAP. Does the same apply for "fixed"? – Alexander Shukaev Jan 06 '12 at 16:39
  • You are not supposed to know what happens on my machine. Maybe you know what happens in general or on another particular machine. – Alexander Shukaev Jan 06 '12 at 16:41
  • @EricLippert I was trusting the [`StructLayoutAttribute`](http://msdn.microsoft.com/library/system.runtime.interopservices.structlayoutattribute.aspx) documentation which states that: "C# [...] compilers apply the Sequential layout value to structures by default.", "For blittable types, LayoutKind.Sequential controls both the layout in managed memory and the layout in unmanaged memory." – ordag Jan 06 '12 at 16:46
  • @EricLippert And the [`Pack`](http://msdn.microsoft.com/library/system.runtime.interopservices.structlayoutattribute.pack.aspx) documentation says in the example that the first member "always starts at offset 0 (byte 0) regardless of the packing value." "Note that the default platform alignment will always pack identical types contiguously." – ordag Jan 06 '12 at 16:47
  • [Fixed performance penalties](http://stackoverflow.com/questions/8497018/what-is-the-overhead-of-c-sharp-fixed-statement-on-a-managed-unsafe-struct-conta) – Alexander Shukaev Jan 06 '12 at 16:54
  • 3
    @ordag: That guarantee might be made to you by some version of the runtime or some implementation of the C# compiler. My point is that the guarantee is not made *by the C# language*. Is the same guarantee made by Mono? By the compact framework? By some future implementation? I would not feel comfortable making those assumptions, but my opinions are coloured by many years of debugging godawful bugs caused by people -- some of whom were past versions of myself -- who made unwarranted assumptions about implementation details of structure layouts. Assumptions that turned out to be violated. – Eric Lippert Jan 06 '12 at 16:55
  • 6
    @Haroogan You're worrying about the costs associated with `fixed`, but it's still not clear why you need to use unsafe code here. Why not just make your `struct` an array to begin with? Or use a `switch` statement in the indexer? Or make `X`, `Y` and `Z` properties that are backed by indexes into a `private` array? These all sound like better options. – dlev Jan 06 '12 at 16:57
  • Because it is supposed to be high-performance code which is based on Mono's SIMD. – Alexander Shukaev Jan 06 '12 at 17:04
  • I'm pretty sure that there are `fixed` penalties that are big enough to not have a `fixed` statement in a function that's called inside very tight loop. Your getter seems like such a function, so I'd avoid `fixed` in there. – CodesInChaos Jan 06 '12 at 23:21
5

You mean something like this?

get
{
    // (index validation omitted)
    fixed (Vector3f* thisPtr = &this)
    {
        return ((float*)thisPtr)[index];
    }
}
ordag
  • 2,497
  • 5
  • 26
  • 35
  • Honestly: I never `fixed` `this`. That may be completely wrong. – ordag Jan 06 '12 at 12:47
  • 1
    But is it really faster than the alternative? I did write some similar code, and in that case the unsafe fixed code wasn't faster than the safe code. – CodesInChaos Jan 06 '12 at 23:22
  • @CodeInChaos No, I'm sure that's not, but this should be a "how you questioned it" and not a "what would be better" answer. – ordag Jan 07 '12 at 00:35
1

I admit that it only solves this specific case and not the "general" case, but you could avoid unsafe by doing something like this (particularly as you state you're showing a simplified example - but it may be of use to others who visit this question):

public struct Vector3f
{
    public float x;
    public float y;
    public float z;

    public float this[int index]
    {
        get
        {
            switch (index)
            {
                case 0:
                    return x;
                case 1:
                    return y;
                case 2:
                    return z;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        set
        {
            switch (index)
            {
                case 0:
                    x = value;
                    break;
                case 1:
                    y = value;
                    break;
                case 2:
                    z = value;
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
    }
}
Rob
  • 45,296
  • 24
  • 122
  • 150
  • Well ordang's method works well. The one u suggest is actually what I wanted to avoid and what is strongly discouraged, since it is slow and boilerplate ;) – Alexander Shukaev Jan 06 '12 at 13:08
0

I agree with Eric that you probably don't want to do this, and I suspect Rob's solution does just as well (while avoiding the use of fixed). Still, it's worth noting that you can overlap your struct fields if you use LayoutKind.Explicit:

[StructLayout(LayoutKind.Explicit)]
public struct Vector3f
{
    [FieldOffset(0)]
    private float x;

    [FieldOffset(sizeof(float))]
    private float y;

    [FieldOffset(2 * sizeof(float))]
    private float z;

    [FieldOffset(0)]
    private unsafe fixed float indexed[3];

    public Vector3f(float x, float y, float z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public float X { get { return x; } set { x = value; } }
    public float Y { get { return y; } set { y = value; } } 
    public float Z { get { return z; } set { z = value; } }

    public unsafe float this[int index]
    {
        get
        {
            if (index < 0 || index >= 3)
                throw new IndexOutOfRangeException();

            fixed (float* b = indexed)
                return b[index];
        }
        set
        {
            if (index < 0 || index >= 3)
                throw new IndexOutOfRangeException();

            fixed (float* b = indexed)
                b[index] = value;
        }
    }
}
phoog
  • 42,068
  • 6
  • 79
  • 117