作为构建.NET的标准,CLI Spec(ECMA-335)针对基元类型的对齐规则具有如下的描述。按照这个标准,我们是这么理解的:8字节的数据类型(int64、unsigned int64和float64)根据采用的机器指令架构选择4字节或者8字节对齐。进一步来说,它们在x86/x64机器上的对齐字节分别为4字节和8字节。
Built-in data types shall be properly aligned, which is defined as follows:
1-byte, 2-byte, and 4-byte data is properly aligned when it is stored at a 1-byte, 2-byte, or 4-byte boundary, respectively.
8-byte data is properly aligned when it is stored on the same boundary required by the underlying hardware for atomic access to a
native int
.Thus,
int16
andunsigned int16
start on even address;int32
,unsigned int32
, andfloat32
start on an address divisible by 4; andint64
,unsigned int64
, andfloat64
start on an address divisible by 4 or 8, depending upon the target architecture. The native size types (native int
,native unsigned int
, and&
) are always naturally aligned (4 bytes or 8 bytes, depending on the architecture). When generated externally, these should also be aligned to their natural size, although portable code can use 8-byte alignment to guarantee architecture independence. It is strongly recommended thatfloat64
be aligned on an 8-byte boundary, even when the size ofnative int
is 32 bits.
我们通过一个简单控制台程序来验证这个说法。为了在64位机器上模拟32位平台,我们按照如下的方式修改了.csproj文件,将PlatformTarget属性设置为x86(默认为Any CPU)。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
</Project>
在演示程序中,我们定义了如下一个名为Foobar的结构体Record。该结构体具有两个字段,类型分别为byte和ulong(unsigned int64)。我们将这两个字段分别设置为byte.Max(FF)和ulong.MaxValue(FF-FF-FF-FF-FF-FF-FF-FF-FF),并将在内存中的二进制形式输出来。为了进一步确定当前的环境与CLI Spec的描述一致,我们将Environment.Is64BitProcess属性(确定是不是64位处理器),ulong类型的字节数(确定这是一个”8-byte data”)和IntPtr.Size(确定native int类型的对其边界是4字节)。
unsafe
{
var bytes = new byte[sizeof(Foobar)];
var foobar = new Foobar(byte.MaxValue, ulong.MaxValue);
Marshal.Copy(new nint(Unsafe.AsPointer(ref foobar)), bytes, 0, bytes.Length);
Console.WriteLine(BitConverter.ToString(bytes));
Console.WriteLine($"Environment.Is64BitProcess = {Environment.Is64BitProcess}");
Console.WriteLine($"sizeof(ulong) = {sizeof(ulong)}");
Console.WriteLine($"IntPtr.Size = {IntPtr.Size}");
}
public record struct Foobar(byte Foo, ulong Bar);
从如下的输出可以看出,当前的环境与CLI Spec描述的32位处理器架构是一致的,但是ulong类型的字段Bar采用的对其长度是8字节而不是4字节(如果采用4字节对齐的话,二进制形式应该FF-00-00-00-FF-FF-FF-FF-FF-FF-FF-FF-FF,如果保证Foobar自身按照8字节对齐,结果也应该是FF-00-00-00-FF-FF-FF-FF-FF-FF-FF-FF-FF-00-00-00-00)。
对于这个问题,我们目前尚未找到一个权威的答案,莫不是我对CLI Spec的解读有误?还是我们的验证程序有问题?希望对此熟悉的朋友不吝赐教!我们目前Google如下这些相关的说法:
Memory alignment on a 32-bit Intel processor
The usual rule of thumb (straight from Intels and AMD's optimization manuals) is that every data type should be aligned by its own size. An
int32
should be aligned on a 32-bit boundary, anint64
on a 64-bit boundary, and so on. A char will fit just fine anywhere.Another rule of thumb is, of course "the compiler has been told about alignment requirements". You don't need to worry about it because the compiler knows to add the right padding and offsets to allow efficient access to data.
WHY IS THE DEFAULT ALIGNMENT FOR `INT64_T` 8 BYTE ON 32 BIT X86 ARCHITECTURE?
Interesting point: If you only ever load it as two halves into 32bit GP registers, then 4B alignment means those operations will happen with their natural alignment.
However, it's probably best if both halves of the variable are in the same cache line, since almost all accesses will read / write both halves. Aligning to the natural alignment of the whole thing takes care of that, even ignoring the other reasons below.
32bit x86 can load 64bit integers in a single 64bit-load using MMX or SSE2
movq
. Handling 64bit add/sub/shift/ and bitwise booleans using vector instructions is more efficient (single instruction), as long as you don't need immediate constants or mul or div. The vector instructions with 64b elements are still available in 32b mode.Atomic 64bit compare-and-exchange is also available in 32bit mode (
lock CMPXCHG8B m64
works just like 64bit mode'slock CMPXCHG16B m128
, using two implicit registers (edx:eax)). IDK what kind of penalty it has for crossing a cache-line boundary.Modern x86 CPUs have essentially no penalty for misaligned loads/stores unless they cross cache-line boundaries, which is why I'm only saying that, and not saying that misaligned 64b would be bad in general. See the links in the x86 wiki, esp. Agner Fog's guides.
Why is the "alignment" the same on 32-bit and 64-bit systems?
MSVC targeting 32-bit x86 gives
__int64
a minimum alignment of 4, but its default struct-packing rules align types within structs tomin(8, sizeof(T))
relative to the start of the struct. (For non-aggregate types only). That's not a direct quote, that's my paraphrase of the MSVC docs link from @P.W's answer, based on what MSVC seems to actually do. (I suspect the "whichever is less" in the text is supposed to be outside the parens, but maybe they're making a different point about the interaction on the pragma and the command-line option?)
做了如下的补充实验,证明ulong类型的对齐规则确实与CLI Spec一致的。莫非8-byte 数据类型本身和作为符合类型(struct/class)字段成员时采用不同的对齐规则?
x64:如下的断言总是成立的。
var random = new Random();
unsafe
{
long v = random.NextInt64();
Debug.Assert(new IntPtr(Unsafe.AsPointer(ref v)).ToInt64() % 8 == 0);
}
x86:如下的断言也总是成立的
var random = new Random();
unsafe
{
long v = random.NextInt64();
Debug.Assert(new IntPtr(Unsafe.AsPointer(ref v)).ToInt32() % 4 == 0);
}
x86:如下的断言就不能保证都成立
var random = new Random();
unsafe
{
long v = random.NextInt64();
Debug.Assert(new IntPtr(Unsafe.AsPointer(ref v)).ToInt32() % 8 == 0);
}