What is to be considered the "natural alignment" for OpCodes.Ldobj?

439 views Asked by At

I am playing around with "Reflection.Emit" and want to generate a static class that exposes a method with this signature in C#:

unsafe static void CopyStruct<T>(void * dest, ref T src) where T : struct
{
    // copy struct from src to dest using ldobj/stobj
}

This is in order to be able to very quickly (and unsafely) copy blittable structs to unmanaged memory. However, I ran in to some questions regarding OpCodes.Ldobj and alignment.

With the following two structs and methods:

struct Def { byte fill0; Int64 fill1; }            // size = 16
static void CopyDef(ref Def dest, ref Def src) { dest = src; }

[StructLayout(LayoutKind.Sequential, Pack = 1)]    // size = 9
struct Seq { byte fill0; Int64 fill1; }
static void CopySeq(ref Seq dest, ref Seq src) { dest = src; }

I would expect the methods to generate something like this:

{ // CopyDef
  ldarg.0
  ldarg.1
  ldobj      Def
  stobj      Def
  ret
}
{ // CopySeq
  ldarg.0
  ldarg.1
  unaligned. 1
  ldobj      Seq
  unaligned. 1
  stobj      Seq
  ret
}

Instead I get the same output for Seq as I do for Def, i.e. no unaligned x opcode anywhere. I expected ldobj/stobj to treat anything other than the natural machine alignment (e.g. 8/4) as unaligned, but I'm clearly mistaken.

My question with a followup:

Is "Pack = 1" somehow implicitly "taken care of" due to the fact that the ldobj/stobj opcodes must specify the type of the address which they are operating on?

If this is the case, does that mean a struct with "Pack = 4" is implicitly "taken care of" so long as the address does not refer to an offset lower than 4 (e.g. 2,1), in which case the opcode must be preceded by unaligned x? This would of course only occur with an unmanaged pointer as in the initial method signature.

Update:

From OpCodes.Ldind_I4 MSDN (1):

All of the ldind instructions are shortcuts for a Ldobj instruction that specifies the corresponding built-in value class.

From OpCodes.Unaligned MSDN (2):

Unaligned specifies that the address ... on the stack might not be aligned to the natural size of the immediately following ldind, stind, ... ldobj, stobj ... instruction. That is, for a Ldind_I4 instruction the alignment of the address may not be to a 4-byte boundary.

From OpCodes.Ldobj MSDN (3):

The number of bytes copied depends on the size of the class (as specified by the class parameter). The class parameter is a metadata token representing the value type.

From OpCodes.Ldind_I4 MSDN (4):

The results of all MSIL instructions that return addresses (for example, Ldloca and Ldarga) are safely aligned.

This suggests that:

  1. "Ldind_I4" == "Ldobj Int32"
  2. Ldobj on a 4 byte int considers a 4 byte offset to be aligned.
  3. Ldobj is "aware" of the size of the type it operates on.
  4. Ldelema cannot produce a unaligned address.

The worst (lowest) possible offset within an array of any blittable struct is:

int offset = sizeof(myStruct) & 7; // 3 for 32bit
// offset == 0     -> correct
// offset == 1,3,7 -> unaligned. 1
// offset == 2,6   -> unaligned. 2
// offset == 4     -> unaligned. 4

The size of Seq is 9 which gives us an offset of 7, which means we are (worst case) unaligned by 1.

The following CopySeq method generates no unaligned opcode

static void CopySeq(ref Seq dest, Seq[] src) { dest = src[1]; }

{ // CopySeq
    ldarg.0
    ldarg.1
    ldc.i4.1
    ldelema    Seq
    ldobj      Seq
    stobj      Seq
    ret
}

Unless ldelema does some magic to the succeeding ldobj, then ldobj ought to known how to handle any misalignment that is possible to occur in an array of the specified type, as can be determined by the size of that type.

0

There are 0 answers